第 08 课:类、对象、构造方法、this 与封装

4 阅读12分钟

第 08 课:类、对象、构造方法、this 与封装

本课目标

学完后,你应该能够:

  1. 区分类、对象和对象引用。
  2. 在类中定义字段和实例方法。
  3. 使用 new 与构造方法创建并初始化对象。
  4. 理解无参构造、带参构造和构造方法重载。
  5. 理解 this.fieldthis(...) 的不同用途。
  6. 使用 private、getter 和 setter 实现基本封装。
  7. 理解多个对象状态独立、多个引用可能指向同一对象。
  8. 让一个对象通过方法与另一个对象协作。

1. 什么是类,什么是对象

类是对一类事物共同特征和行为的描述,可以把它理解为“图纸”。对象是按照类创建出的具体实例。

例如学生类可以描述:

  • 特征:姓名、年龄、成绩;
  • 行为:学习、判断是否及格、显示信息。
public class Student {
    String name;
    int age;
    double score;
​
    public void study() {
        System.out.println(name + " 正在学习");
    }
}

这个类本身不是某个真实学生。创建对象后才有具体数据:

Student student = new Student();
student.name = "小明";
student.age = 20;
student.study();

语法拆解:

Student student = new Student();
  │       │           │
类型    引用变量     创建对象并调用构造方法

student 保存的是对象引用,可以通过点号访问对象允许外部访问的成员。

2. 字段、局部变量与参数

public class Student {
    private String name; // 字段
​
    public void study(String subject) { // subject 是参数
        int minutes = 60;               // minutes 是局部变量
    }
}
类型声明位置生命周期/作用范围
字段类内部、方法外部跟随对象存在,由对象保存状态
参数方法括号内只在本次方法调用期间有效
局部变量方法或代码块内部只在所属代码块内有效

对象字段有默认值:数字为 0、booleanfalse、引用为 null。局部变量没有自动可用的默认值,使用前必须明确赋值。

3. 实例方法

实例方法描述对象能够执行的行为:

public boolean isPassed() {
    return score >= 60;
}

它可以直接读取当前对象的字段。调用时必须通过对象引用:

boolean passed = student.isPassed();

mainstatic 方法,不能在里面不经过对象就直接访问实例字段。当前先记住:实例成员属于具体对象,静态成员属于类;后面会单独详解 static

4. 构造方法

构造方法在 new 创建对象时自动执行,主要负责初始化对象:

public Student(String name, int age, double score) {
    this.name = name;
    this.age = age;
    this.score = score;
}

构造方法规则:

  • 名称必须与类名完全相同;
  • 没有返回值类型,连 void 都不能写;
  • 可以带参数;
  • 可以重载多个版本;
  • 调用由 new Student(...) 完成。

错误写法:

public void Student() { }

因为写了 void,这只是一个名为 Student 的普通方法,不是构造方法。

5. 默认构造方法与构造器重载

如果一个类完全没有编写构造方法,Java 会提供一个隐式无参构造方法:

Student student = new Student();

一旦自己编写任何构造方法,编译器就不再自动提供无参构造。如果仍想支持 new Student(),必须明确写出:

public Student() {
}

本课使用两个重载构造方法:

public Student() {
    this("未命名", 18, 0.0);
}
​
public Student(String name, int age, double score) {
    setName(name);
    setAge(age);
    setScore(score);
}

参数列表不同,因此构成重载。

6. this 的两种用途

this.field 表示当前对象的字段

public void setName(String name) {
    this.name = name;
}
  • 左侧 this.name:当前对象的字段;
  • 右侧 name:传入 setter 的参数。

如果写成:

name = name;

左右两边都会被理解为离得更近的参数,相当于把参数赋值给自己,对象字段不会改变。

this(...) 调用当前类的另一个构造方法

public Student() {
    this("未命名", 18, 0.0);
}

this(...) 必须是构造方法的第一条语句。这样可以复用初始化逻辑,避免不同构造方法各写一套校验。

7. 封装

直接公开字段会让任何代码随意写入无效状态:

student.age = -500;
student.score = 999;

封装的基本做法:

  1. 字段设为 private
  2. 通过公共方法读取和修改;
  3. 在修改方法中执行校验。
private double score;
​
public double getScore() {
    return score;
}
​
public void setScore(double score) {
    if (score < 0 || score > 100) {
        this.score = 0.0;
        return;
    }
    this.score = score;
}

封装不是“每个字段机械生成 getter/setter”。如果某个状态不应该由外部任意修改,就不应提供对应 setter。例如银行余额只能通过存款、取款和转账改变,因此只提供 getBalance()

8. 多个对象与多个引用

Student first = new Student("小明", 20, 88);
Student second = new Student("小红", 19, 70);

这里创建了两个独立对象。修改 second 的成绩不会影响 first

但下面没有创建新对象:

Student alias = first;
alias.setAge(21);

aliasfirst 指向同一个对象:

first ──┐
        ├──> Student 对象
alias ──┘

所以通过 alias 修改年龄后,first.getAge() 也会得到 21。这与上一课数组引用的行为一致。

9. null 引用

Student student = null;
student.printInfo();

null 表示没有指向任何对象。通过它调用方法会抛出 NullPointerException

安全检查:

if (student != null) {
    student.printInfo();
}

这里 &&|| 的短路规则依然重要,例如:

student != null && student.isPassed()

左侧为 false 时不会调用右侧实例方法。

10. 完整源码一:Student.java

public class Student {
    // private 字段只能由 Student 类内部直接访问,保护对象状态。
    private String name;
    private int age;
    private double score;
​
    // 无参数构造方法,通过 this(...) 调用三参数构造方法。
    // this(...) 必须是构造方法中的第一条语句。
    public Student() {
        this("未命名", 18, 0.0);
    }
​
    // 构造方法与类同名,没有返回值类型,创建对象时自动执行。
    public Student(String name, int age, double score) {
        // 复用 setter 的校验逻辑,避免构造方法与 setter 写两份规则。
        setName(name);
        setAge(age);
        setScore(score);
    }
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        if (name == null || name.isBlank()) {
            this.name = "未命名";
            return;
        }
        this.name = name.strip();
    }
​
    public int getAge() {
        return age;
    }
​
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            this.age = 0;
            return;
        }
        this.age = age;
    }
​
    public double getScore() {
        return score;
    }
​
    public void setScore(double score) {
        if (score < 0 || score > 100) {
            this.score = 0.0;
            return;
        }
        this.score = score;
    }
​
    public boolean isPassed() {
        return score >= 60;
    }
​
    public void study(String subject) {
        if (subject == null || subject.isBlank()) {
            System.out.println(name + " 暂未选择学习内容。");
            return;
        }
        System.out.println(name + " 正在学习 " + subject.strip() + "。");
    }
​
    public void printInfo() {
        System.out.println("姓名:" + name);
        System.out.println("年龄:" + age);
        System.out.println("成绩:" + score);
        System.out.println("是否及格:" + isPassed());
    }
}

Student 设计解读

  • 三个字段全部 private,外部不能直接写入。
  • 两个构造方法形成重载,无参版本委托给三参数版本。
  • 构造方法通过 setter 初始化,因此名称、年龄和成绩使用同一套校验规则。
  • getter 只返回值;setter 在修改前验证。
  • isPassed() 把“及格规则”放在对象内部,外部不用重复写 score >= 60
  • study() 使用短路保护空主题,并使用 strip() 清理输入。

本例对无效值采用“替换为默认值”。真实项目也可能选择抛出异常;等学到异常处理后会改进。

11. 完整源码二:StudentDemo.java

public class StudentDemo {
    public static void main(String[] args) {
        // new Student(...) 创建对象,构造方法负责初始化字段。
        Student firstStudent = new Student("小明", 20, 88.5);
        Student secondStudent = new Student("小红", 19, 56.0);
​
        System.out.println("===== 第一个学生 =====");
        firstStudent.printInfo();
        firstStudent.study("Java 面向对象");
​
        System.out.println();
        System.out.println("===== 第二个学生 =====");
        secondStudent.printInfo();
​
        // 通过公共 setter 修改状态,而不是直接访问 private 字段。
        secondStudent.setScore(72.0);
        System.out.println("修改成绩后是否及格:" + secondStudent.isPassed());
​
        // 两个对象拥有独立状态;修改 secondStudent 不影响 firstStudent。
        System.out.println("第一个学生的成绩仍为:" + firstStudent.getScore());
​
        // 使用无参数构造方法创建带默认值的对象。
        Student defaultStudent = new Student();
        System.out.println();
        System.out.println("===== 默认学生 =====");
        defaultStudent.printInfo();
​
        // 两个引用可以指向同一个对象;通过 alias 修改会影响原对象。
        Student alias = firstStudent;
        alias.setAge(21);
        System.out.println("alias 修改后 firstStudent 年龄:" + firstStudent.getAge());
    }
}

StudentDemo 执行流程

  1. 创建两个不同对象,每次 new 都调用一次构造方法。
  2. 通过实例方法读取当前对象自己的字段。
  3. 修改第二个对象的成绩,第一个对象不受影响。
  4. 无参构造得到默认学生。
  5. alias = firstStudent 不执行 new,所以只是新增一个指向同一对象的引用。

12. 独立练习:BankAccount.java 完整答案

需求:

  • 账户保存所有人和余额;
  • 初始余额不能为负数;
  • 存款必须大于 0;
  • 取款不能超过余额;
  • 禁止向 null 或自己转账;
  • 余额不能被外部任意 setter 修改。
public class BankAccount {
    private String owner;
    private double balance;
​
    public BankAccount(String owner, double initialBalance) {
        setOwner(owner);
​
        if (initialBalance < 0) {
            this.balance = 0.0;
        } else {
            this.balance = initialBalance;
        }
    }
​
    public String getOwner() {
        return owner;
    }
​
    public void setOwner(String owner) {
        if (owner == null || owner.isBlank()) {
            this.owner = "未知用户";
            return;
        }
        this.owner = owner.strip();
    }
​
    public double getBalance() {
        return balance;
    }
​
    public boolean deposit(double amount) {
        if (amount <= 0) {
            return false;
        }
​
        balance += amount;
        return true;
    }
​
    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) {
            return false;
        }
​
        balance -= amount;
        return true;
    }
​
    public boolean transferTo(BankAccount target, double amount) {
        if (target == null || target == this || amount <= 0 || amount > balance) {
            return false;
        }
​
        this.balance -= amount;
        target.balance += amount;
        return true;
    }
​
    public void printInfo() {
        System.out.println("账户所有人:" + owner + ",余额:" + balance);
    }
}

银行账户封装解读

余额没有 setter,因为 setBalance(-999999) 会绕过所有业务规则。状态只能通过有语义的方法改变:

  • deposit():验证后增加余额;
  • withdraw():验证正数和余额后减少;
  • transferTo():同时修改两个账户。

target == this 判断目标账户是不是当前对象本身。target.balance 虽然是 private,但当前代码仍在 BankAccount 类内部,所以能访问另一个 BankAccount 对象的私有字段。

本课为简化语法使用 double 表示金额。真实金融系统不能依赖二进制浮点数精确表示十进制金额,后面会使用 BigDecimal

13. 独立练习运行入口:BankAccountDemo.java

public class BankAccountDemo {
    public static void main(String[] args) {
        BankAccount firstAccount = new BankAccount("小明", 1000.0);
        BankAccount secondAccount = new BankAccount("小红", 500.0);
​
        System.out.println("===== 初始状态 =====");
        firstAccount.printInfo();
        secondAccount.printInfo();
​
        // 存入 200 元,返回 true 表示操作成功。
        boolean deposited = firstAccount.deposit(200.0);
        System.out.println("存款是否成功:" + deposited);
​
        // 取出金额超过余额,方法拒绝修改状态并返回 false。
        boolean withdrawn = secondAccount.withdraw(1000.0);
        System.out.println("超额取款是否成功:" + withdrawn);
​
        // 从第一个账户向第二个账户转账 300 元。
        boolean transferred = firstAccount.transferTo(secondAccount, 300.0);
        System.out.println("转账是否成功:" + transferred);
​
        System.out.println();
        System.out.println("===== 操作后状态 =====");
        firstAccount.printInfo();
        secondAccount.printInfo();
​
        // null 目标会被 transferTo 中的短路条件安全拒绝。
        boolean invalidTransfer = firstAccount.transferTo(null, 100.0);
        System.out.println("向 null 转账是否成功:" + invalidTransfer);
    }
}

状态变化推演:

操作小明余额小红余额结果
初始1000500
小明存款 2001200500成功
小红取款 10001200500失败,状态不变
小明转给小红 300900800成功
小明转给 null 100900800失败,状态不变

14. 常见错误

给构造方法写返回值

写了 void 就变成普通方法,不会在 new 时自动执行。

自定义构造后仍以为无参构造存在

只写了三参数构造时,new Student() 会失败。需要自己补充无参构造。

忘记 this 导致字段没被修改

当参数与字段同名时使用 this.field = field

所有字段都公开

public 字段会使外部绕过校验。优先使用 private,再根据业务需要提供方法。

为每个字段机械生成 setter

有些字段只应通过业务方法改变,例如余额、订单状态和库存数量。

对 null 调用实例方法

对象引用可能为 null,调用前使用明确校验或短路逻辑。

把引用复制误认为复制对象

Student b = a; 不会创建新学生;两个变量指向同一个学生。

15. 检查题与答案

问题

  1. 类和对象有什么区别?
  2. Student student = new Student() 中,student 保存的是什么?
  3. 构造方法能否声明为 void
  4. 自己编写带参构造后,Java 还会自动提供无参构造吗?
  5. this.name = name 两边分别代表什么?
  6. this(...) 在构造方法中有什么限制?
  7. 为什么字段通常设为 private
  8. getter 和 setter 必须为每个字段成对出现吗?
  9. Student alias = original 会创建新对象吗?
  10. 两次执行 new Student() 创建的对象是否共享字段状态?
  11. 为什么银行余额不提供 setBalance()
  12. target == null || target == this 如何利用短路?
  13. 一个 BankAccount 方法为什么能访问另一个账户的 private balance
  14. null 引用调用方法会发生什么?

答案

  1. 类是描述共同字段和行为的模板;对象是根据类创建的具体实例。
  2. 指向 Student 对象的引用。
  3. 不能。写了返回类型后就是普通方法,不是构造方法。
  4. 不会,必须自己明确编写无参构造。
  5. 左侧是当前对象的字段,右侧是方法或构造方法参数。
  6. 必须是构造方法中的第一条语句,用于调用当前类的另一个构造方法。
  7. 防止外部绕过规则随意破坏对象状态,并隐藏内部实现。
  8. 不必须。只提供业务真正需要的访问方式。
  9. 不会,只复制引用;两个变量指向同一对象。
  10. 不共享,每个对象拥有自己的实例字段状态。
  11. 余额只能通过受规则约束的存款、取款和转账操作改变。
  12. 目标为 null 时左侧已经为真,后续条件不会继续计算;随后方法返回失败。
  13. private 限制的是类外访问,不是同一类不同对象之间的访问;代码仍位于 BankAccount 类内部。
  14. 抛出 NullPointerException

16. 本课完成标准

  • 能定义一个包含私有字段、构造方法、getter、setter 和实例方法的类。
  • 能解释 new、对象引用和构造方法的执行关系。
  • 能区分 this.fieldthis(...)
  • 能根据业务决定哪些字段不应该提供 setter。
  • 能解释独立对象与共享引用的区别。
  • 能读懂两个对象之间的转账操作和状态变化。

下一课学习继承、super、方法重写、多态、抽象类,以及为什么业务代码更愿意依赖父类型而不是具体实现。