第 5 章 · 面向对象:类、继承与多态
本章目标:理解类、对象、成员变量与方法;掌握构造器与 this;熟练使用访问修饰符;理解继承、super 与方法重写;掌握多态(向上转型、动态绑定);能设计 toolkit-demo 的商品与折扣商品模型;对照 python-dev ch06 OOP。
学时建议:4~5 小时(含 2 小时跟练)
前置:完成 java-dev ch04;建议已学 python-dev ch06。
5.1 场景说明:商品模型
toolkit-demo 需要描述普通商品与促销商品:共享 SKU、名称、单价,促销商品额外折扣率并重写价格计算——类似 python-dev 中 Product 基类与 SaleProduct 子类。
learn-java/toolkit-demo/src/main/java/demo/model/
├── Product.java
├── DiscountProduct.java
└── CatalogDemo.java
| 概念 | 说明 |
|---|---|
| 封装 | 价格字段私有,通过方法访问 |
| 继承 | DiscountProduct extends Product |
| 多态 | Product p = new DiscountProduct(...) |
| 重写 | 子类 getPrice() 含折扣 |
5.2 类、对象与构造器
类是模板,对象由 new 创建。Java 显式声明类型,用 this 指当前对象(类比 Python self)。
| 术语 | 说明 |
|---|---|
| 字段 | 成员变量,通常 private |
| 构造器 | 与类同名、无返回类型,用于初始化 |
this(...) | 调用其他构造器,须在第一行 |
| 重载 | 同类多构造器参数不同 |
封装原则:字段 private,对外提供 getter/setter 或业务方法(见 5.7 Product 完整代码)。
5.3 访问修饰符
| 修饰符 | 类内 | 同包 | 子类 | 全局 |
|---|---|---|---|---|
private | ✓ | ✗ | ✗ | ✗ |
| 默认(包私有) | ✓ | ✓ | ✗ | ✗ |
protected | ✓ | ✓ | ✓ | ✗ |
public | ✓ | ✓ | ✓ | ✓ |
封装原则:字段通常 private,对外 getter/setter 或业务方法。
5.4 继承、多态与重写
子类 extends 父类,用 super 调父构造器/方法。@Override 标记重写;Java 单继承(接口多实现见 ch06)。
Product p2 = new DiscountProduct("B", "促销", 100, 0.8);
printPrice(p2); // 80 — 动态绑定子类 getUnitPrice()
static void printPrice(Product p) {
System.out.println(p.getSku() + " 售价:" + p.getUnitPrice());
}
向上转型:编译类型 Product,运行类型 DiscountProduct 时调用子类方法。
| Python | Java |
|---|---|
| duck typing | 编译期检查 + 运行时多态 |
class B(A) | class B extends A |
5.5 重写 vs 重载
| 重写 Override | 重载 Overload | |
|---|---|---|
| 位置 | 子类与父类 | 同类 |
| 签名 | 相同 | 参数不同 |
| 返回 | 可协变 | 可不同 |
| 注解 | @Override | 无 |
// 重载:同类多方法
public double calc(int qty) { return getUnitPrice() * qty; }
public double calc(int qty, double extra) { return calc(qty) + extra; }
5.6 toString、equals 与 hashCode:Object 的三大方法
所有类都直接或间接继承 java.lang.Object,其中有三个最常被重写的方法:
| 方法 | 默认行为 | 何时重写 |
|---|---|---|
toString() | 输出 demo.model.Product@6f496d9f 这类地址串 | 需要可读日志、调试输出时 |
equals(Object) | 与 == 相同,比较引用 | 需要「内容相等」语义时 |
hashCode() | 由对象地址计算 | 重写 equals 时必须同时重写 |
为什么 equals 与 hashCode 必须成对重写:ch08 的 HashSet/HashMap 先用 hashCode() 决定放进哪个「桶」,再用 equals() 在桶内判等。只重写 equals 不重写 hashCode,两个内容相等的对象会落入不同桶,Set 里出现重复元素——这是面试与生产 Bug 的高频点。
import java.util.Objects;
// 以下三个方法加入 5.7 的 Product 类中
@Override
public String toString() {
return "Product{sku='" + sku + "', name='" + name + "', unitPrice=" + unitPrice + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true; // 同一引用,直接相等
}
if (!(o instanceof Product other)) {
return false; // 类型不同或为 null
}
// 业务约定:sku 相同即同一商品
return Objects.equals(sku, other.sku);
}
@Override
public int hashCode() {
return Objects.hash(sku); // 与 equals 使用相同的字段
}
要点:
equals的参数必须是Object;写成equals(Product p)是重载而不是重写,集合框架永远不会调用它- 用
instanceof模式匹配(Java 16+)一步完成类型检查与转型 Objects.equals(a, b)与Objects.hash(...)内部处理了null,避免 NPEequals与hashCode必须使用相同的字段(这里是sku),否则契约被破坏- 对照 Python:等价于重写
__eq__与__hash__,规则同样是「成对出现」
5.7 Product.java 完整代码
package demo.model;
/**
* 商品基类 — toolkit-demo
*/
public class Product {
private String sku;
private String name;
private double unitPrice;
public Product() {
this.sku = "UNKNOWN";
this.name = "未命名";
this.unitPrice = 0.0;
}
public Product(String sku, String name, double unitPrice) {
this.sku = sku;
this.name = name;
setUnitPrice(unitPrice);
}
public String getSku() {
return sku;
}