元数据卡
- 前置知识:第2章(变量与类型)、第5章(方法与栈)
- 预计时间:65 分钟
- 核心难度:
- 阅读模式: 稳步推进
- 完成标志:能定义类、创建对象、理解引用与值的区别;会用构造方法、this、static、封装、访问修饰符;知道什么是"面向对象"
本章分层
- 必读:类与对象的定义、字段与方法、构造方法、
this关键字、封装与访问修饰符、static的用法- 选读:方法重载与构造方法链、
static工具类的设计模式- 深水区:值传递 vs 引用传递的深入、Java Bean 与框架约定
本章不会要求你掌握
- 防御性复制的完整实现(进阶安全话题)
- 构造方法中调用可重写方法的陷阱(继承章详述)
你在哪
阿花在路上截住你,问你那个图书管理系统写得怎么样了。你给她看了一眼五个并排的数组——她皱起眉头:
int age = 25;—— 一个变量,一间小房子int[] scores = {85, 92, 78};—— 一整条街,全是整数
但问题来了——"人"不是由单个整数或者单个字符串能描述的。
你在工坊接到了一个任务:给图书馆写一个"图书管理系统"。一本书有书名、作者、ISBN、价格、是否借出……你可以用五个数组来存:
String[] bookTitles = new String[1000];
String[] bookAuthors = new String[1000];
String[] bookIsbns = new String[1000];
double[] bookPrices = new double[1000];
boolean[] bookBorrowed = new boolean[1000];但这样,你添加一本书要操作五个数组,删除一本书要维护五个数组的同步,排序按书名——另五个数组全乱了。
你皱着眉头看着代码,老陈师傅路过,看了一眼屏幕。
"五个数组?"他笑了。"你忘了最重要的一件事——数据是有结构的。一本书就是一个整体。你不该把它的属性拆开扔进五个数组里。"
"你要做的是——先定义'书'这个概念,然后每次遇到一本书,就根据这个概念创建对应的对象。"
"这个概念叫类,这个概念的具体实例叫对象。这就是面向对象编程的第一步。"
你的任务
这章之后,你的思维方式会发生一个转变——从"我用变量存数据"变成"我定义类型来描述数据"。
你将学会:
- 什么是类,什么是对象——两者的区别是面试题里的万年经典
- 如何给一个类添加字段(属性)和方法(行为)
- 构造方法——创建对象时的初始化逻辑
this关键字——解决名字冲突,链式调用static——属于类的而不是属于对象的- 封装——为什么不该直接暴露字段
- getter/setter——Java Bean 的约定,控制访问的守门人
- 包与访问修饰符——如何组织代码,控制可见性
面向对象三大特征——封装、继承、多态——这里我们将深入第一块基石:封装。后面两章是继承和多态。
遭遇战 → 获得技能
第一幕:从蓝图到实物——类与对象的诞生
造物主按一个模样创造了无数个人。人和人不同,但共享同一个"人的模板"。
在 Java 里也是一样。
// 定义一个 "书" 的类——这是一个"蓝图"
class Book {
// 字段(属性):描述一本书有哪些数据
String title;
String author;
String isbn;
double price;
boolean isBorrowed;
}语言:Java 如何运行:保存为 Book.java,编译通过。先不用写 main,这只是一个"类型定义"。 发生了什么:你定义了一个新类型 Book。它可以像 int、double 一样用来声明变量,只是它是一个引用类型。
"蓝图"有了,怎么才能变成一本真正的"书"?
public class Library {
public static void main(String[] args) {
// 根据 Book 蓝图创建一个具体的对象
Book myBook = new Book();
// 给这个对象的字段赋值
myBook.title = "Java 核心技术";
myBook.author = "Horstmann";
myBook.isbn = "978-7-111-12345-6";
myBook.price = 99.00;
myBook.isBorrowed = false;
// 访问字段
System.out.println(myBook.title); // Java 核心技术
System.out.println(myBook.price); // 99.0
}
}语言:Java 预期输出:
Java 核心技术
99.0语法拆解:
Book myBook—— 声明一个Book类型的变量(这是一个引用变量)new Book()—— 在堆内存上实际创建一个Book对象=—— 把新对象的地址赋给myBook
核心理解:
Book myBook = new Book()做了三件事:
- 声明一个可装
Book地址的变量myBook(栈上)- 在堆内存上为
Book对象分配空间,所有字段初始化为默认值(null,0,false)- 把堆内存的地址赋给
myBook变量不"包含"对象——它指向对象。对象在堆里,变量在栈里。
你可以创建多个对象——每个独立:
Book b1 = new Book();
Book b2 = new Book();
b1.title = "Java 核心技术";
b2.title = "深入理解计算机系统";
System.out.println(b1.title); // Java 核心技术
System.out.println(b2.title); // 深入理解计算机系统
b1 = null; // 断开引用——堆上的那个对象失去了指向,等待 GC 回收🪟 差异窗口
C++:
class Book {
public:
std::string title;
std::string author;
Book() {} // 默认构造函数——不给的话 Java 自动提供,C++ 在某些情况下不提供
};
int main() {
Book book; // 栈上分配——变量直接包含对象
Book* bookPtr = new Book(); // 堆上分配——需要手动 delete
delete bookPtr;
return 0;
}C++ 的对象可以在栈上(自动释放)或堆上(手动管理)。Java 的对象全部在堆上。
Python:
class Book:
pass # Python 不需要显式定义字段
b = Book() # Python 不需要 new
b.title = "Java 核心技术" # 动态添加属性,不需要在类中预定义
b.author = "Horstmann"
print(b.title) # Java 核心技术Python 的对象可以动态添加新属性——不需要在类中预先声明。Java 不行,所有字段必须在类定义中列出,否则访问时编译报错。
第二幕:降生时的洗礼——构造方法
每次 new Book() 之后,你都得手动给每个字段赋值:
Book b = new Book();
b.title = "Java 核心技术";
b.author = "Horstmann";
b.isbn = "978-7-111-12345-6";
b.price = 99.00;太啰嗦了。而且如果忘记给某个字段赋值,它保持默认值(null、0、false)——这可能不是你想要的。
这时候就需要构造方法——一个在对象创建时自动调用的特殊方法。
class Book {
String title;
String author;
String isbn;
double price;
boolean isBorrowed;
// 构造方法:和类同名,没有返回值(连 void 都没有)
Book(String bookTitle, String bookAuthor, String bookIsbn, double bookPrice) {
title = bookTitle;
author = bookAuthor;
isbn = bookIsbn;
price = bookPrice;
isBorrowed = false; // 新书默认未借出
}
}语法:Book(String title, ...) —— 方法名和类名完全一致,没有返回类型 调用时机:在 new Book(...) 时自动执行
有了构造方法,创建对象变成了这样:
Book b = new Book("Java 核心技术", "Horstmann", "978-7-111-12345-6", 99.00);
// 如果试图不传参数:
Book empty = new Book(); // ❌ 编译错误!因为没有无参构造方法为什么报错? 一旦你定义了任何带参数的构造方法,Java 就不会再自动提供那个默认的无参构造方法。
class Book {
String title;
// 如果定义了一个构造方法,又想保留无参构造,必须显式写:
Book() {
// 可以什么都不做,或者提供默认值
}
Book(String title) {
this.title = title;
}
}
// 现在两个都 OK
Book b1 = new Book();
Book b2 = new Book("深入理解计算机系统");无参构造方法:如果你没有定义任何构造方法,Java 自动提供一个无参构造方法,字段全部初始化为默认值。一旦你定义了任何构造方法,这个"免费午餐"就没了。
🪟 差异窗口
C++:
class Book {
public:
Book(); // 默认构造
Book(const std::string& title, const std::string& author);
// C++ 还有拷贝构造、移动构造、析构函数
Book(const Book& other); // 拷贝构造
Book(Book&& other) noexcept; // 移动构造
~Book(); // 析构——Java 没有,靠 GC
};C++ 有多个特殊成员函数。最重要的区别是析构函数——Java 有垃圾回收器自动回收,不需要析构函数。C++ 需要手动管理资源。
Python:
class Book:
def __init__(self, title, author, isbn, price):
self.title = title # Python 的 self 等价于 Java 的 this
self.author = author
self.isbn = isbn
self.price = price
self.is_borrowed = False
b = Book("Java 核心技术", "Horstmann", "978-7-111-12345-6", 99.00)Python 的 __init__ 不是真正的构造方法——对象已经被创建了,它只是初始化方法。而且 Python 有 __del__ 但不保证何时调用。
第三幕:我指的是我自己——this 关键字
你注意到一个细节——上一段代码里我故意没给构造方法的参数加前缀:
Book(String bookTitle, String bookAuthor, String bookIsbn, double bookPrice) {
title = bookTitle; // 这里 title 是字段,bookTitle 是参数
author = bookAuthor;
isbn = bookIsbn;
price = bookPrice;
}但多数 Java 开发者会写成这样:
Book(String title, String author, String isbn, double price) {
this.title = title; // this.title 是字段,title 是参数
this.author = author;
this.isbn = isbn;
this.price = price;
}this 就是"这个对象本身"——在方法内部,this 指向调用这个方法的当前对象。它解决了参数名和字段名冲突的问题。
class Student {
String name;
int age;
// 不冲突时 this 可省
void setName(String newName) {
name = newName; // 等价于 this.name = newName
}
// 冲突时必须用 this
void setName(String name) { // 参数 name 和字段 name 同名
this.name = name; // 不加 this,name 指的是参数
}
}关于省略 this 的原则:
- 当参数名和字段名不同时 —— Java 能区分,可以省略
- 当参数名和字段名相同时 —— 必须用
this,否则参数覆盖了字段
大多数 IDE 会用相同的参数名和字段名——所以大多数时候你都会看到 this.xxx = xxx;。这不是多余的啰嗦,是准确表达。
this 还有一个用途——调用同一个类的其他构造方法:
class Book {
String title;
String author;
double price;
Book(String title, String author) {
this(title, author, 0.0); // 调用下面的三参数构造方法
}
Book(String title, String author, double price) {
this.title = title;
this.author = author;
this.price = price;
}
}规则:this(...) 调用必须写在构造方法的第一行。这叫"构造方法链"。
第四幕:属于类的,不属于个体的——static
到目前为止,字段和方法都属于"对象"的——每个 Book 对象有自己的 title、自己的 author。
但有些东西不属于单个对象,而属于整个类。
比如——你开的是一家图书馆,你需要知道总共有多少本书被创建了(不管哪本)。这个"计数器"不能放在单个对象上——每个对象不该管自己是不是第几个创建的。
class Book {
String title;
String author;
// 静态字段——属于类,所有对象共享
static int totalCount = 0;
Book(String title, String author) {
this.title = title;
this.author = author;
totalCount++; // 每创建一个对象,总数加 1
}
}
public class Library {
public static void main(String[] args) {
Book b1 = new Book("Java 核心技术", "Horstmann");
Book b2 = new Book("深入理解计算机系统", "Bryant");
Book b3 = new Book("设计模式", "GoF");
// 通过类名访问静态字段
System.out.println("已创建图书:" + Book.totalCount); // 3
}
}语言:Java 预期输出:已创建图书:3 关键理解:
totalCount在内存里只有一份,不随对象创建而复制- 推荐通过
类名.静态字段访问,而不是通过对象.静态字段 - 静态字段在类加载时初始化,在所有对象之前存在
静态方法也一样:
class MathUtils {
// 静态方法——不依赖任何对象的状态
static int max(int a, int b) {
return a > b ? a : b;
}
// 实例方法——需要对象
String concat(String a, String b) {
return a + b; // 这里可以用 this(如果有实例字段)
}
}
public class Main {
public static void main(String[] args) {
// 调用静态方法——无需对象
int m = MathUtils.max(10, 20);
System.out.println(m); // 20
// 调用实例方法——需要对象
MathUtils utils = new MathUtils();
String s = utils.concat("Hello", " World");
}
}static 的核心规则:
- 静态方法只能访问静态的字段和方法——不能直接访问实例字段或调用实例方法
- 实例方法可以访问所有——既可以是静态的,也可以是非静态的
- 静态方法中没有
this——this指向当前对象,但静态方法不属于任何对象
为什么 main 方法必须是 static?因为程序启动时还没有任何对象——JVM 调用 main 时,不能先创建什么对象,所以 main 必须是静态的。
public class Main {
String greeting = "Hello";
public static void main(String[] args) {
System.out.println(greeting); // ❌ 编译错误:不能从静态上下文访问非静态字段
// 正确做法:先创建对象
Main m = new Main();
System.out.println(m.greeting); // ✅ Hello
}
}💡 静态方法 vs 工具类
Math.max()、Arrays.sort()——这些类的方法是静态的。它们只接收输入、处理、输出,不依赖任何对象的状态。这类类叫工具类(utility class)。工具类通常会私有化构造方法,防止被实例化:
class MathUtils {
// 私有构造方法——外面不能 new
private MathUtils() {}
public static int max(int a, int b) { ... }
public static int min(int a, int b) { ... }
}🪟 差异窗口
C++:
class MathUtils {
public:
static int max(int a, int b); // 声明 static
static int counter; // 静态成员变量——需要在类外单独定义
};
// 在 .cpp 中定义(分配存储空间)
int MathUtils::counter = 0;C++ 的静态成员变量必须在类外单独定义一次,否则链接会报"未定义引用"错误。Java 不需要这个步骤。
Python:
class Book:
total_count = 0 # 类变量——相当于 static 字段
def __init__(self, title):
self.title = title # 实例变量
Book.total_count += 1
@staticmethod
def get_count():
return Book.total_count
def __repr__(self):
return f"Book({self.title})"
b1 = Book("Java")
b2 = Book("Python")
print(Book.get_count()) # 2Python 用 @staticmethod 装饰器声明静态方法,而且类和实例属性是动态的,没有严格的"静态 vs 实例"的编译时检查。
第五幕:不要直接碰我的东西——封装与 getter/setter
你还记得一开始的时候直接访问字段吗?
myBook.price = -50.0; // ❌ 一本书价格是负数?这不合逻辑
myBook.isbn = null; // ❌ ISBN 不能为空
myBook.isBorrowed = false; // 这里不一定符合业务规则问题出在哪?调用者可以直接操作字段,没有任何约束。
面向对象的第一原则 —— 封装:把数据藏起来,只暴露安全的访问方式。
class Book {
// 私有字段——外部不能直接访问
private String title;
private String author;
private String isbn;
private double price;
private boolean isBorrowed;
// 构造方法
public Book(String title, String author, String isbn, double price) {
// 在构造时就做校验
if (title == null || title.isEmpty()) {
throw new IllegalArgumentException("书名不能为空");
}
if (price < 0) {
throw new IllegalArgumentException("价格不能为负数");
}
this.title = title;
this.author = author;
this.isbn = isbn;
this.price = price;
this.isBorrowed = false;
}
// getter——公开读
public String getTitle() {
return title;
}
// setter——公开写,带约束
public void setTitle(String title) {
if (title == null || title.isEmpty()) {
throw new IllegalArgumentException("书名不能为空");
}
this.title = title;
}
public double getPrice() {
return price;
}
// setter 可以比 getter 更严格
public void setPrice(double price) {
if (price < 0) {
throw new IllegalArgumentException("价格不能为负数");
}
this.price = price;
}
// 有些字段可能只读——只提供 getter,不提供 setter
public String getIsbn() {
return isbn;
}
// 没有 setIsbn()——ISBN 一旦创建不可修改
public boolean isBorrowed() { // boolean 的 getter 习惯用 is 开头
return isBorrowed;
}
// 业务方法(不一定是简单的 getter/setter)
public void borrow() {
if (isBorrowed) {
throw new IllegalStateException("这本书已经被借出了");
}
this.isBorrowed = true;
}
public void returnBook() {
this.isBorrowed = false;
}
}语言:Java 关键改变:
- 字段全部
private—— 外部无法直接访问book.price - 提供
public的 getter/setter —— 读取和修改都要经过你写的代码 - 在校验代码里,你可以拒绝不合法的数据
这样的好处:
Book book = new Book("Java 核心技术", "Horstmann", "978-7-111-12345-6", 99.00);
// book.price = -50; ❌ 编译错误!price 是 private 的
// book.title = ""; ❌ 编译错误!title 是 private 的
book.setPrice(-50); // ✅ 编译通过,但运行时抛 IllegalArgumentException
book.setTitle(""); // ✅ 编译通过,但运行时抛 IllegalArgumentException💡 更重要的一层理解:封装的价值不只在"校验数据"——它在于解耦。
你内部可以修改字段名、改变存储方式(比如把摄氏温度转成华氏度存储)、增加缓存——只要 getter/setter 的签名不变,调用方的代码一行都不用改。
这就是信息隐藏的力量。
📎 进阶话题:Java Bean 命名约定是框架(Spring、Jackson)依赖的规范, 初学时只需知道 getter/setter 的命名规则即可,无需深入理解反射调用的细节。
🪟 Java Bean 命名约定
Java 对 getter/setter 有约定(不是语法强制,但几乎所有框架依赖这个约定):
| 字段类型 | getter 命名 | setter 命名 |
|---|---|---|
String name | getName() | setName(String name) |
boolean active | isActive() | setActive(boolean active) |
int count | getCount() | setCount(int count) |
为什么要遵守? Spring、Jackson(JSON 序列化)、MyBatis(数据库映射)——所有框架都通过反射调用 getter/setter 来读写对象。你写 getTitle(),框架就调用它读取 title。你不遵守约定,框架就找不到。
第六幕:代码的组织——包与访问修饰符
你写了几十个类,全放在同一个目录下。万一有两个类同名呢?为了不和别人的类名撞车呢?
包(package) 就是 Java 的命名空间机制。
// 文件:com/atlas/library/model/Book.java
package com.atlas.library.model;
public class Book {
// ...
}规则:
package声明放在文件第一行- 包名通常反转域名:
com.atlas.library.model - 包的目录结构和包名一一对应:
com/atlas/library/model/Book.java - 全限定名是
com.atlas.library.model.Book
// 在另一个文件中使用
import com.atlas.library.model.Book; // 导入后可以直接用 Book
// 或者可以直接用全限定名:
// com.atlas.library.model.Book b = new com.atlas.library.model.Book(...);访问修饰符控制"谁能看见谁":
| 修饰符 | 本类 | 同包 | 子类(不同包) | 任意 |
|---|---|---|---|---|
private | ||||
| 默认(不写) | ||||
protected | ||||
public |
日常理解:
private—— 我自己的私事(字段几乎全是这个)- 默认 —— 邻居可以进来(同包下的类)
protected—— 家人可以进来(子类可以访问)public—— 谁都可以进来(API 接口、工具类方法)
package com.atlas.library.model;
public class Book {
private String title; // 只有本类内部能访问
String internalCode; // 默认(package-private):同包能访问
protected String category; // 同包 + 不同包子类能访问
public String getTitle() { // 所有人能访问
return title;
}
}一个正确的 Java Bean 的模式:
package com.atlas.library.model;
public class Book {
// 字段:private
private String title;
private String author;
// 构造方法:public
public Book(String title, String author) {
this.title = title;
this.author = author;
}
// getter/setter:public
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
}包管理的最佳实践:
- 一个包内放功能相关的类
- 项目结构通常按"层级"或"功能"组织:
model(数据模型)、service(业务逻辑)、controller(API 入口) - 同一个包不要塞太多类(十几个是红线,几十个说明该拆了)
常见陷阱
陷阱一:把对象赋值给另一个变量——引用拷贝
前面第 6 章数组提过引用的概念,现在你真正面对它了。
Book b1 = new Book("Java 核心技术", "Horstmann");
Book b2 = b1; // ❌ 不是复制对象,是复制引用!
b2.setTitle("不同书名");
System.out.println(b1.getTitle()); // 不同书名 —— b1 也变了!为什么? b1 和 b2 指向堆里的同一个对象。通过 b2 改,b1 看到的是同一个东西。
要复制对象怎么办? 需要显式实现"拷贝"逻辑——在类里写一个复制方法或拷贝构造方法:
class Book {
// 拷贝构造方法
Book(Book other) {
this.title = other.title;
this.author = other.author;
this.isbn = other.isbn;
this.price = other.price;
this.isBorrowed = other.isBorrowed;
}
}
// 使用
Book b1 = new Book("Java 核心技术", "Horstmann");
Book b2 = new Book(b1); // ✅ 真正的独立副本
b2.setTitle("不同书名");
System.out.println(b1.getTitle()); // "Java 核心技术" —— 没变陷阱二:传引用还是传值的困惑
Java 面试高频题:Java 是值传递还是引用传递?
答案:值传递。
public class Main {
public static void main(String[] args) {
Book book = new Book("原书名", "作者");
changeTitle(book);
System.out.println(book.getTitle()); // 新书名 —— 对象被修改了
}
static void changeTitle(Book b) { // b 是 book 引用的值拷贝
b.setTitle("新书名"); // 通过拷贝的引用修改了同一个对象
b = new Book("另一本", "某"); // ❌ 这个操作对外面的 book 没有影响
}
}理解的关键点:
book存的是对象的地址(比如 0x1234)- 调用
changeTitle(book)时,复制的是 0x1234 这个值给b - 通过
b修改对象是 OK 的——b和book指向同一个东西 - 但
b = new Book(...)改变的是b本地副本的指向——改成 0x5678 了,book仍然是 0x1234
Java 传递的永远是值的副本。当参数是基本类型时,副本就是数值本身。当参数是引用类型时,副本是引用的地址。
这不是"引用传递"(reference passing)——真正的引用传递是把变量的地址本身传进去,才能修改它指向的内容。
陷阱三:构造方法里调用可被重写的方法
class Parent {
Parent() {
print(); // ❌ 危险:构造方法中调用了可被重写的方法
}
void print() { System.out.println("Parent"); }
}
class Child extends Parent {
String value = "Hello";
Child() { super(); }
@Override
void print() { System.out.println("Child: " + value.length()); }
}
new Child(); // 猜猜输出什么?
// ❌ NullPointerException —— 因为 Child 的 value 还没初始化!教训:构造方法中不要调用可被子类重写的方法。子类对象还没初始化完,重写的方法会读到 null。这种问题极其难调试。
陷阱四:getter 返回可变对象的引用
class Student {
private List<String> courses = new ArrayList<>();
public List<String> getCourses() {
return courses; // ❌ 外部可以直接修改内部的 courses!
}
}
// 外部:
Student s = new Student();
List<String> list = s.getCourses();
list.add("恶意课程"); // ✅ 编译通过,被改动了!安全做法:返回不可修改的视图或复制品:
public List<String> getCourses() {
return Collections.unmodifiableList(courses); // 外部不能修改
// 或者:return new ArrayList<>(courses); // 返回副本,修改不影响内部
}陷阱五:static 方法的自引用
class Util {
private String name;
public static void print() {
System.out.println(name); // ❌ 编译错误
System.out.println(this); // ❌ 编译错误
}
}静态方法没有 this,不能访问实例字段。如果你在静态方法里需要实例数据,你必须通过参数传入。
通关挑战
- 🗡 热身(10 分钟,必做)
- 定义一个
Student类,包含字段name、age、score,全部private,提供构造方法和 getter/setter。
public class Student {
private String name;
private int age;
private double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
// score 在校验范围内
if (score < 0 || score > 100) {
throw new IllegalArgumentException("成绩必须在 0-100 之间");
}
this.score = score;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public double getScore() { return score; }
public void setScore(double score) {
if (score < 0 || score > 100) throw new IllegalArgumentException("成绩必须在 0-100 之间");
this.score = score;
}
}在
main方法中创建三个Student对象,存入数组,遍历计算平均分。给
Student类添加一个static字段count跟踪创建了多少个学生,在构造方法中递增。
- 挑战(20 分钟,选做)
- 写一个
BankAccount类:
public class BankAccount {
private String accountNumber; // 不可修改
private String ownerName;
private double balance; // 余额不能为负
public BankAccount(String accountNumber, String ownerName, double initialDeposit) {
// 校验:账户号不为空、初始存款不小于 0
}
// getters:所有字段
// 没有 setAccountNumber——账号一旦创建不能改
// 没有 setBalance——余额只能通过 deposit/withdraw 修改
public void deposit(double amount) { ... } // 存款:金额必须 > 0
public boolean withdraw(double amount) { ... } // 取款:金额必须 > 0 且余额充足
public void transfer(BankAccount target, double amount) { ... } // 转账
}实现这个类。测试:创建两个账户,存钱、取钱、转账、尝试透支。
- 分析下面的代码输出:
class Counter {
static int count = 0;
int id;
Counter() {
count++;
id = count;
}
void display() {
System.out.println("我是第 " + id + " 个对象。总共创建了 " + count + " 个。");
}
}
public class Main {
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();
c1.display();
c2.display();
c3.display();
}
}预期输出:
我是第 1 个对象。总共创建了 3 个。
我是第 2 个对象。总共创建了 3 个。
我是第 3 个对象。总共创建了 3 个。为什么 c1.display() 也显示"总共创建了 3 个"?因为 count 是 static —— 类级别的,只有一个。
- 排障:下面每段代码的问题在哪?
// 问题 1:
public class Person {
private String name;
public Person(String name) {
name = name; // 意图是给 this.name 赋值
}
}
// 问题 2:
public class MathCalc {
public static void main(String[] args) {
System.out.println(PI); // 需要什么?
}
static final double PI = 3.14159;
}
// 问题 3:
public class Data {
private int value;
public int getValue() { return value; }
public void setValue(int value) { value = value; }
public void display() {
System.out.println("值:" + value);
}
}
// 问题 4:
public class Config {
private static String setting = "default";
public static String getSetting() { return setting; }
public static void setSetting(String s) { setting = s; }
}验收标准
- 你能清楚说出类和对象的区别——类蓝图,对象实例
- 你能定义
private字段、publicgetter/setter,理解封装的意图 - 你会编写构造方法,理解无参构造方法何时消失
- 你会用
this区分字段和参数,以及调用其他构造方法 - 你理解
static字段和方法属于类本身,不是对象 - 你理解 Java 的值传递——引用复制不复制对象
- 你知道
private/默认/protected/public的可见范围和含义 - 你知道
package的作用和目录结构的对应关系
常见卡点
"构造方法和方法有什么区别?"
构造方法没有返回类型(连 void 都没有),方法名必须和类名相同,并且只能通过 new 调用(普通方法通过 对象.方法名() 调用)。构造方法不能被继承。
"为什么我改了拷贝的值原始对象也变了?"
因为你是在修改引用指向的同一个对象。需要显式复制——拷贝构造方法或 clone 方法。
"getter/setter 不就是对字段的包装吗?为什么它们不直接是 public 字段?"
因为有了 getter/setter:
- 你可以添加校验逻辑(拒绝负数价格)
- 你可以改变内部实现(把字段改名、改成计算值)
- 你可以添加日志、权限检查等横切逻辑
- 框架(Spring、Jackson)通过 getter/setter 反射操作对象——没有它们框架就断了
"静态方法里不能访问非静态字段,我该怎么做?"
把对象作为参数传入静态方法,或者先创建对象再通过对象访问。静态方法只处理传入的数据,不依赖任何隐含的对象状态。
"到底什么时候用 static?"
- 方法只依赖参数、不依赖实例状态(工具方法)
- 常量(
static final) - 计数器(所有对象共享同一个计数)
- 单例模式中的
getInstance() main方法——JVM 启动入口
"包名为什么总是 com.xxx.yyy 的形式?"
这是 Java 社区的反向域名约定。com.google.common.collect 一看就知道是 Google 的公共集合库。如果不这样,两个独立项目都叫 util.List 就会冲突。域名在全球唯一,反向域名做前缀就解决了包名冲突。
现在不需要理解
final修饰类(类不能继承)和final修饰方法(方法不能重写)——下一章继承- 抽象类和接口——下一章
- 枚举(
enum)——第 9 章 - Record 类(Java 14+)——自动生成构造方法、getter、equals、hashCode、toString——第 9 章
equals和hashCode的覆写——第 10 章集合- Java 内存模型(堆/栈/方法区到底怎么分工)——理解对象在堆里、变量在栈里就够了
- 垃圾回收机制的具体算法——知道有 GC 就行,细节 Vol 3
- 内部类、匿名类——第 8 章 Lambda
import static——静态导入:import static java.lang.Math.max;之后可以只写max(1,2)
旅人笔记
面向对象的第一课很简单:把数据和操作数据的代码放在一起。
类 = 蓝图,对象 = 根据蓝图造出来的具体实例。蓝图在编译时,实例在运行时。
构造方法 = 对象降生时的洗礼仪式——必调、同名、无返回值。
this = 指向自己——解决名字冲突,链式构造。
static = 属于类的,不属于个体的——所有对象共享一份。
private = 藏起来,public 方法 = 安全的门。封装 = 信息隐藏 + 控制访问。
getter/setter = 带校验的读写口——不止是字段包装,是接口契约。
package = 命名空间,防止名字撞车。
private > 默认 > protected > public。
这段旅程最难习惯的点不是语法——是从"我写一段代码"到"我定义一个类型"的思维转变。以前你是按步骤写过程,现在你是设计世界的概念模型。
一句话总结:面向对象思维 = 用"什么类型"来描述世界,而不是用"怎么做"来描述流程。
→ 下一站预告
现在你能设计和创建单个类了——Book、Student、BankAccount。它们各自封装了数据和操作。
但现实世界不是一座孤岛——Student 是人,人是 Person 的一种;Book 有作者,作者也是 Person;BankAccount 有交易记录,交易是一个更复杂的东西。
不同类之间的关系——继承、组合、多态——将在下一章展开。准备好全面进入面向对象的世界了吗?
下一站:继承与多态。