第 08 课:类、对象、构造方法、this 与封装
本课目标
学完后,你应该能够:
- 区分类、对象和对象引用。
- 在类中定义字段和实例方法。
- 使用
new与构造方法创建并初始化对象。 - 理解无参构造、带参构造和构造方法重载。
- 理解
this.field与this(...)的不同用途。 - 使用
private、getter 和 setter 实现基本封装。 - 理解多个对象状态独立、多个引用可能指向同一对象。
- 让一个对象通过方法与另一个对象协作。
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、boolean 为 false、引用为 null。局部变量没有自动可用的默认值,使用前必须明确赋值。
3. 实例方法
实例方法描述对象能够执行的行为:
public boolean isPassed() {
return score >= 60;
}
它可以直接读取当前对象的字段。调用时必须通过对象引用:
boolean passed = student.isPassed();
main 是 static 方法,不能在里面不经过对象就直接访问实例字段。当前先记住:实例成员属于具体对象,静态成员属于类;后面会单独详解 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;
封装的基本做法:
- 字段设为
private; - 通过公共方法读取和修改;
- 在修改方法中执行校验。
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);
alias 和 first 指向同一个对象:
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 执行流程
- 创建两个不同对象,每次
new都调用一次构造方法。 - 通过实例方法读取当前对象自己的字段。
- 修改第二个对象的成绩,第一个对象不受影响。
- 无参构造得到默认学生。
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);
}
}
状态变化推演:
| 操作 | 小明余额 | 小红余额 | 结果 |
|---|---|---|---|
| 初始 | 1000 | 500 | — |
| 小明存款 200 | 1200 | 500 | 成功 |
| 小红取款 1000 | 1200 | 500 | 失败,状态不变 |
| 小明转给小红 300 | 900 | 800 | 成功 |
| 小明转给 null 100 | 900 | 800 | 失败,状态不变 |
14. 常见错误
给构造方法写返回值
写了 void 就变成普通方法,不会在 new 时自动执行。
自定义构造后仍以为无参构造存在
只写了三参数构造时,new Student() 会失败。需要自己补充无参构造。
忘记 this 导致字段没被修改
当参数与字段同名时使用 this.field = field。
所有字段都公开
public 字段会使外部绕过校验。优先使用 private,再根据业务需要提供方法。
为每个字段机械生成 setter
有些字段只应通过业务方法改变,例如余额、订单状态和库存数量。
对 null 调用实例方法
对象引用可能为 null,调用前使用明确校验或短路逻辑。
把引用复制误认为复制对象
Student b = a; 不会创建新学生;两个变量指向同一个学生。
15. 检查题与答案
问题
- 类和对象有什么区别?
Student student = new Student()中,student保存的是什么?- 构造方法能否声明为
void? - 自己编写带参构造后,Java 还会自动提供无参构造吗?
this.name = name两边分别代表什么?this(...)在构造方法中有什么限制?- 为什么字段通常设为
private? - getter 和 setter 必须为每个字段成对出现吗?
Student alias = original会创建新对象吗?- 两次执行
new Student()创建的对象是否共享字段状态? - 为什么银行余额不提供
setBalance()? target == null || target == this如何利用短路?- 一个
BankAccount方法为什么能访问另一个账户的private balance? null引用调用方法会发生什么?
答案
- 类是描述共同字段和行为的模板;对象是根据类创建的具体实例。
- 指向
Student对象的引用。 - 不能。写了返回类型后就是普通方法,不是构造方法。
- 不会,必须自己明确编写无参构造。
- 左侧是当前对象的字段,右侧是方法或构造方法参数。
- 必须是构造方法中的第一条语句,用于调用当前类的另一个构造方法。
- 防止外部绕过规则随意破坏对象状态,并隐藏内部实现。
- 不必须。只提供业务真正需要的访问方式。
- 不会,只复制引用;两个变量指向同一对象。
- 不共享,每个对象拥有自己的实例字段状态。
- 余额只能通过受规则约束的存款、取款和转账操作改变。
- 目标为
null时左侧已经为真,后续条件不会继续计算;随后方法返回失败。 private限制的是类外访问,不是同一类不同对象之间的访问;代码仍位于BankAccount类内部。- 抛出
NullPointerException。
16. 本课完成标准
- 能定义一个包含私有字段、构造方法、getter、setter 和实例方法的类。
- 能解释
new、对象引用和构造方法的执行关系。 - 能区分
this.field与this(...)。 - 能根据业务决定哪些字段不应该提供 setter。
- 能解释独立对象与共享引用的区别。
- 能读懂两个对象之间的转账操作和状态变化。
下一课学习继承、super、方法重写、多态、抽象类,以及为什么业务代码更愿意依赖父类型而不是具体实现。