面向对象(上):把现实世界装进代码
难度:初级 | 预计时间:50 分钟 | 前置:控制流程
📖 万能工具类的崩塌
故事背景
你接手了一个遗留项目,里面有个 CommonUtils.java——3000 行,50 多个静态方法,从字符串处理到文件读写到日期格式化,应有尽有。改一行,崩一片。没有测试,也不敢重构。这时候你想起大学老师说过一个词——封装。把数据和行为绑在一起、隐藏内部细节,这才是面向对象的起点。这节课教你用 OOP 思维组织代码,告别上帝类。
💻 代码演示
一个图书借阅系统——左面是面向过程的写法,右边是面向对象的写法:
// ===== 面向对象版本 =====
// Book.java — 书只是一个数据+行为的封装体
public class Book {
private String title;
private String isbn;
private boolean available = true; // 内部状态,外部不能直接改
public Book(String title, String isbn) {
this.title = title;
this.isbn = isbn;
}
public void borrow() {
if (!available) {
throw new IllegalStateException("书已被借出");
}
available = false;
System.out.println("《" + title + "》借出成功");
}
public void returnBook() {
available = true;
System.out.println("《" + title + "》归还成功");
}
public boolean isAvailable() { return available; }
public String getTitle() { return title; }
}
// LibraryCard.java — 借书证管理借阅记录
public class LibraryCard {
private String owner;
private List<Book> borrowed = new ArrayList<>();
public LibraryCard(String owner) { this.owner = owner; }
public void borrowBook(Book book) {
book.borrow(); // 书自己决定能不能借
borrowed.add(book);
}
public int borrowedCount() { return borrowed.size(); }
}运行输出:
注意:Book 的 available 字段是 private——外部无法直接 book.available = true。只能通过 borrow()/returnBook() 方法操作,方法内部可以做校验。这就是封装。🎯 三个核心问题
这是什么?
面向对象编程(OOP)的核心是把数据和对数据的操作封装在一起。三大特性:
- 封装(Encapsulation):隐藏内部状态,只暴露方法
- 继承(Inheritance):子类复用父类的代码
- 多态(Polymorphism):同一接口,不同实现
为什么需要它?
封装的意义不只是加 getter/setter。它的核心价值是保护不变量(invariant)。比如 Book 的 available 不能为 null,不能凭空从 false 变 true(必须通过 returnBook)。私有字段 + 公有方法可以保证这些规则不被破坏。10 个调用者不会用 10 种方式修改你的数据——都走同一条路。
如果没有它会怎样?
如果只用静态方法和公共字段(面向过程),任何代码都能随意修改任何数据——"散弹式修改"。改一个规则(比如增加借书次数限制),需要找到所有直接操作 available 的地方,漏一个就是 bug。封装把修改范围压缩到一个类内部。
📝 原理讲解
Java OOP 速查:
访问修饰符:
private(仅本类)、默认/包级(同包可见)、protected(同包+子类)、public(所有人)。新手法则:字段全 private,方法根据需要 public。构造器:
new Book("三体", "978-7-229")调用构造器,做初始化。如果没有显式写,Java 给你一个无参默认构造器。this:当前对象的引用。构造器里this.title = title是为了区分参数和字段。record(Java 14+):如果类只用来存数据(没有行为),用 record 可以一行搞定:record BookDTO(String title, String isbn) {}自动生成构造器、getter、equals、hashCode、toString。前端对比:Java 的 class ≈ TS 的 class + strict private(TS 的 private 只在编译期检查,Java 在运行时也强制)。
🎨 生活类比
类比理解
类就像手机——你只知道按开机键(方法),不需要知道主板上的电容怎么充电(内部状态)。 封装就是手机外壳——保护脆弱的元件不被随便碰触。如果电池可以随便抠出来正负极反接(就像直接修改 private 字段),手机就废了。 公共方法就是手机按钮——只有那几个,按了就触发预设好的行为,不会让你搞坏内部。
✏️ 动手练习
练习 1
写一个 BankAccount 类:字段 balance(private double),方法 deposit(double) 和 withdraw(double)。withdraw 时判断余额是否足够,不足时抛出异常。
<details> <summary>💡 查看提示</summary>
在 withdraw 里检查 if (amount > balance) throw new IllegalArgumentException("余额不足")
</details>
练习 2
给 BankAccount 加一个 List<String> history 记录每笔交易("存入:100.0"、"取出:50.0"),提供 getHistory() 方法。注意 history 不能暴露内部引用。
<details> <summary>💡 查看提示</summary>
getter 不要直接返回 this.history(外部可以随意修改),应该返回 Collections.unmodifiableList(history) 或 new ArrayList<>(history)。
</details>
练习 3
用 record 定义一个 TransactionRecord(类型、金额、时间),替代 history 的字符串列表。
<details> <summary>💡 查看提示</summary>
record TransactionRecord(String type, double amount, LocalDateTime time) {}
</details>
✅ 自检站
<details> <summary><strong>封装的核心目的不是隐藏数据,那是什么?</strong></summary>
封装的核心目的是保护不变量(invariant)——确保对象状态始终合法。比如 BankAccount 的余额不能为负(除非允许透支),这个规则通过 withdraw 方法强制,外部无法绕过。
</details>
<details> <summary><strong>record 和普通 class 有什么区别?什么时候用哪个?</strong></summary>
record 是不可变的(字段都是 final),自动生成构造器、getter、equals、hashCode、toString。适合 DTO、值对象等"纯数据"场景。普通 class 适合有行为、有可变状态的业务对象。前端类比:record ≈ 加了 readonly 的 TS interface,class ≈ 普通的 TS class。
</details>