Skip to content

元数据卡

  • 前置知识:第2章(变量与类型)、第5章(方法与栈)
  • 预计时间:65 分钟
  • 核心难度:
  • 阅读模式: 稳步推进
  • 完成标志:能定义类、创建对象、理解引用与值的区别;会用构造方法、this、static、封装、访问修饰符;知道什么是"面向对象"

本章分层

  • 必读:类与对象的定义、字段与方法、构造方法、this 关键字、封装与访问修饰符、static 的用法
  • 选读:方法重载与构造方法链、static 工具类的设计模式
  • 深水区:值传递 vs 引用传递的深入、Java Bean 与框架约定

本章不会要求你掌握

  • 防御性复制的完整实现(进阶安全话题)
  • 构造方法中调用可重写方法的陷阱(继承章详述)

你在哪

阿花在路上截住你,问你那个图书管理系统写得怎么样了。你给她看了一眼五个并排的数组——她皱起眉头:

  • int age = 25; —— 一个变量,一间小房子
  • int[] scores = {85, 92, 78}; —— 一整条街,全是整数

但问题来了——"人"不是由单个整数或者单个字符串能描述的。

你在工坊接到了一个任务:给图书馆写一个"图书管理系统"。一本书有书名、作者、ISBN、价格、是否借出……你可以用五个数组来存:

java
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 里也是一样。

java
// 定义一个 "书" 的类——这是一个"蓝图"
class Book {
    // 字段(属性):描述一本书有哪些数据
    String title;
    String author;
    String isbn;
    double price;
    boolean isBorrowed;
}

语言:Java 如何运行:保存为 Book.java,编译通过。先不用写 main,这只是一个"类型定义"。 发生了什么:你定义了一个新类型 Book。它可以像 intdouble 一样用来声明变量,只是它是一个引用类型。

"蓝图"有了,怎么才能变成一本真正的"书"?

java
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() 做了三件事:

  1. 声明一个可装 Book 地址的变量 myBook(栈上)
  2. 在堆内存上为 Book 对象分配空间,所有字段初始化为默认值(null, 0, false
  3. 把堆内存的地址赋给 myBook

变量不"包含"对象——它指向对象。对象在堆里,变量在栈里。

你可以创建多个对象——每个独立:

java
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++:

cpp
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:

python
class Book:
    pass  # Python 不需要显式定义字段

b = Book()  # Python 不需要 new
b.title = "Java 核心技术"  # 动态添加属性,不需要在类中预定义
b.author = "Horstmann"

print(b.title)  # Java 核心技术

Python 的对象可以动态添加新属性——不需要在类中预先声明。Java 不行,所有字段必须在类定义中列出,否则访问时编译报错。

第二幕:降生时的洗礼——构造方法

每次 new Book() 之后,你都得手动给每个字段赋值:

java
Book b = new Book();
b.title = "Java 核心技术";
b.author = "Horstmann";
b.isbn = "978-7-111-12345-6";
b.price = 99.00;

太啰嗦了。而且如果忘记给某个字段赋值,它保持默认值(null、0、false)——这可能不是你想要的。

这时候就需要构造方法——一个在对象创建时自动调用的特殊方法。

java
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(...) 时自动执行

有了构造方法,创建对象变成了这样:

java
Book b = new Book("Java 核心技术", "Horstmann", "978-7-111-12345-6", 99.00);

// 如果试图不传参数:
Book empty = new Book();  // ❌ 编译错误!因为没有无参构造方法

为什么报错? 一旦你定义了任何带参数的构造方法,Java 就不会再自动提供那个默认的无参构造方法

java
class Book {
    String title;
    // 如果定义了一个构造方法,又想保留无参构造,必须显式写:
    Book() {
        // 可以什么都不做,或者提供默认值
    }
    
    Book(String title) {
        this.title = title;
    }
}

// 现在两个都 OK
Book b1 = new Book();
Book b2 = new Book("深入理解计算机系统");

无参构造方法:如果你没有定义任何构造方法,Java 自动提供一个无参构造方法,字段全部初始化为默认值。一旦你定义了任何构造方法,这个"免费午餐"就没了。

🪟 差异窗口

C++:

cpp
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:

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 关键字

你注意到一个细节——上一段代码里我故意没给构造方法的参数加前缀:

java
Book(String bookTitle, String bookAuthor, String bookIsbn, double bookPrice) {
    title = bookTitle;       // 这里 title 是字段,bookTitle 是参数
    author = bookAuthor;
    isbn = bookIsbn;
    price = bookPrice;
}

但多数 Java 开发者会写成这样:

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 指向调用这个方法的当前对象。它解决了参数名和字段名冲突的问题。

java
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 还有一个用途——调用同一个类的其他构造方法

java
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

但有些东西不属于单个对象,而属于整个类

比如——你开的是一家图书馆,你需要知道总共有多少本书被创建了(不管哪本)。这个"计数器"不能放在单个对象上——每个对象不该管自己是不是第几个创建的。

java
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 在内存里只有一份,不随对象创建而复制
  • 推荐通过类名.静态字段访问,而不是通过对象.静态字段
  • 静态字段在类加载时初始化,在所有对象之前存在

静态方法也一样:

java
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 的核心规则

  1. 静态方法只能访问静态的字段和方法——不能直接访问实例字段或调用实例方法
  2. 实例方法可以访问所有——既可以是静态的,也可以是非静态的
  3. 静态方法中没有 this —— this 指向当前对象,但静态方法不属于任何对象

为什么 main 方法必须是 static?因为程序启动时还没有任何对象——JVM 调用 main 时,不能先创建什么对象,所以 main 必须是静态的。

java
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)。工具类通常会私有化构造方法,防止被实例化:

java
class MathUtils {
    // 私有构造方法——外面不能 new
    private MathUtils() {}

    public static int max(int a, int b) { ... }
    public static int min(int a, int b) { ... }
}

🪟 差异窗口

C++:

cpp
class MathUtils {
public:
    static int max(int a, int b);  // 声明 static

    static int counter;  // 静态成员变量——需要在类外单独定义
};

// 在 .cpp 中定义(分配存储空间)
int MathUtils::counter = 0;

C++ 的静态成员变量必须在类外单独定义一次,否则链接会报"未定义引用"错误。Java 不需要这个步骤。

Python:

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())  # 2

Python 用 @staticmethod 装饰器声明静态方法,而且类和实例属性是动态的,没有严格的"静态 vs 实例"的编译时检查。

第五幕:不要直接碰我的东西——封装与 getter/setter

你还记得一开始的时候直接访问字段吗?

java
myBook.price = -50.0;  // ❌ 一本书价格是负数?这不合逻辑
myBook.isbn = null;     // ❌ ISBN 不能为空
myBook.isBorrowed = false;  // 这里不一定符合业务规则

问题出在哪?调用者可以直接操作字段,没有任何约束。

面向对象的第一原则 —— 封装:把数据藏起来,只暴露安全的访问方式。

java
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 —— 读取和修改都要经过你写的代码
  • 在校验代码里,你可以拒绝不合法的数据

这样的好处:

java
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 namegetName()setName(String name)
boolean activeisActive()setActive(boolean active)
int countgetCount()setCount(int count)

为什么要遵守? Spring、Jackson(JSON 序列化)、MyBatis(数据库映射)——所有框架都通过反射调用 getter/setter 来读写对象。你写 getTitle(),框架就调用它读取 title。你不遵守约定,框架就找不到。

第六幕:代码的组织——包与访问修饰符

你写了几十个类,全放在同一个目录下。万一有两个类同名呢?为了不和别人的类名撞车呢?

包(package) 就是 Java 的命名空间机制。

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
java
// 在另一个文件中使用
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 接口、工具类方法)
java
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 的模式:

java
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 章数组提过引用的概念,现在你真正面对它了。

java
Book b1 = new Book("Java 核心技术", "Horstmann");
Book b2 = b1;         // ❌ 不是复制对象,是复制引用!

b2.setTitle("不同书名");
System.out.println(b1.getTitle());  // 不同书名 —— b1 也变了!

为什么? b1b2 指向堆里的同一个对象。通过 b2 改,b1 看到的是同一个东西。

要复制对象怎么办? 需要显式实现"拷贝"逻辑——在类里写一个复制方法或拷贝构造方法:

java
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 是值传递还是引用传递?

答案:值传递

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 没有影响
    }
}

理解的关键点:

  1. book 存的是对象的地址(比如 0x1234)
  2. 调用 changeTitle(book) 时,复制的是 0x1234 这个值给 b
  3. 通过 b 修改对象是 OK 的——bbook 指向同一个东西
  4. b = new Book(...) 改变的是 b 本地副本的指向——改成 0x5678 了,book 仍然是 0x1234

Java 传递的永远是值的副本。当参数是基本类型时,副本就是数值本身。当参数是引用类型时,副本是引用的地址。

这不是"引用传递"(reference passing)——真正的引用传递是把变量的地址本身传进去,才能修改它指向的内容。

陷阱三:构造方法里调用可被重写的方法

java
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 返回可变对象的引用

java
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("恶意课程");  // ✅ 编译通过,被改动了!

安全做法:返回不可修改的视图或复制品:

java
public List<String> getCourses() {
    return Collections.unmodifiableList(courses);  // 外部不能修改
    // 或者:return new ArrayList<>(courses);      // 返回副本,修改不影响内部
}

陷阱五:static 方法的自引用

java
class Util {
    private String name;
    public static void print() {
        System.out.println(name);  // ❌ 编译错误
        System.out.println(this);  // ❌ 编译错误
    }
}

静态方法没有 this,不能访问实例字段。如果你在静态方法里需要实例数据,你必须通过参数传入。

通关挑战

  • 🗡 热身(10 分钟,必做)
  1. 定义一个 Student 类,包含字段 nameagescore,全部 private,提供构造方法和 getter/setter。
java
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;
    }
}
  1. main 方法中创建三个 Student 对象,存入数组,遍历计算平均分。

  2. Student 类添加一个 static 字段 count 跟踪创建了多少个学生,在构造方法中递增。

  • 挑战(20 分钟,选做)
  1. 写一个 BankAccount
java
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) { ... }  // 转账
}

实现这个类。测试:创建两个账户,存钱、取钱、转账、尝试透支。

  1. 分析下面的代码输出
java
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 个"?因为 countstatic —— 类级别的,只有一个。

  • 排障:下面每段代码的问题在哪?
java
// 问题 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 字段、public getter/setter,理解封装的意图
  • 你会编写构造方法,理解无参构造方法何时消失
  • 你会用 this 区分字段和参数,以及调用其他构造方法
  • 你理解 static 字段和方法属于类本身,不是对象
  • 你理解 Java 的值传递——引用复制不复制对象
  • 你知道 private/默认/protected/public 的可见范围和含义
  • 你知道 package 的作用和目录结构的对应关系

常见卡点

"构造方法和方法有什么区别?"

构造方法没有返回类型(连 void 都没有),方法名必须和类名相同,并且只能通过 new 调用(普通方法通过 对象.方法名() 调用)。构造方法不能被继承。

"为什么我改了拷贝的值原始对象也变了?"

因为你是在修改引用指向的同一个对象。需要显式复制——拷贝构造方法或 clone 方法。

"getter/setter 不就是对字段的包装吗?为什么它们不直接是 public 字段?"

因为有了 getter/setter:

  1. 你可以添加校验逻辑(拒绝负数价格)
  2. 你可以改变内部实现(把字段改名、改成计算值)
  3. 你可以添加日志、权限检查等横切逻辑
  4. 框架(Spring、Jackson)通过 getter/setter 反射操作对象——没有它们框架就断了

"静态方法里不能访问非静态字段,我该怎么做?"

把对象作为参数传入静态方法,或者先创建对象再通过对象访问。静态方法只处理传入的数据,不依赖任何隐含的对象状态。

"到底什么时候用 static?"

  1. 方法只依赖参数、不依赖实例状态(工具方法)
  2. 常量(static final
  3. 计数器(所有对象共享同一个计数)
  4. 单例模式中的 getInstance()
  5. 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 章
  • equalshashCode 的覆写——第 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 有作者,作者也是 PersonBankAccount 有交易记录,交易是一个更复杂的东西。

不同类之间的关系——继承、组合、多态——将在下一章展开。准备好全面进入面向对象的世界了吗?

下一站:继承与多态

Built with VitePress | Software Systems Atlas