Skip to content

面向对象(上):把现实世界装进代码

难度:初级 | 预计时间:50 分钟 | 前置:控制流程


📖 万能工具类的崩塌

故事背景

你接手了一个遗留项目,里面有个 CommonUtils.java——3000 行,50 多个静态方法,从字符串处理到文件读写到日期格式化,应有尽有。改一行,崩一片。没有测试,也不敢重构。这时候你想起大学老师说过一个词——封装。把数据和行为绑在一起、隐藏内部细节,这才是面向对象的起点。这节课教你用 OOP 思维组织代码,告别上帝类。

💻 代码演示

一个图书借阅系统——左面是面向过程的写法,右边是面向对象的写法:

java
// ===== 面向对象版本 =====
// 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)的核心是把数据和对数据的操作封装在一起。三大特性:

  1. 封装(Encapsulation):隐藏内部状态,只暴露方法
  2. 继承(Inheritance):子类复用父类的代码
  3. 多态(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>