8.5 实验:继承、接口与多态
前置知识:继承、重写、动态分派、抽象类和接口
预计时间:40 分钟
练习环境:JDK 17+ 与 Bash;所有文件写入临时目录
完成标志:能通过正向与失败测试证明子类型契约,而不只证明代码可以编译
chapter8_oop_root=$(mktemp -d)
readonly chapter8_oop_root
mkdir -p "$chapter8_oop_root/out" "$chapter8_oop_root/broken"
cd "$chapter8_oop_root"热身 1:动物叫声与多态
保存为 Animal.java:
abstract class Animal {
private final String name;
Animal(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("blank name");
}
this.name = name;
}
final String name() { return name; }
abstract String sound();
}
final class Dog extends Animal {
Dog(String name) { super(name); }
@Override String sound() { return "汪"; }
}
final class Cat extends Animal {
Cat(String name) { super(name); }
@Override String sound() { return "喵"; }
}
final class Zoo {
static String introduce(Animal animal) {
java.util.Objects.requireNonNull(animal, "animal");
return animal.name() + ":" + animal.sound();
}
}测试 Zoo.introduce(new Dog("小黑")) 和 Cat,并确认空白名字、null 参数按契约失败。再增加一种动物,Zoo 不应修改。
热身 2:验证 @Override 和 final
把故意失败的最小源码保存为 BrokenOverride.java:
abstract class Animal {
abstract String sound();
}
class Dog extends Animal {
@Override String sounds() { return "汪"; }
}它同时缺少 sound 实现,且 sounds 没有可覆盖目标。实际编译并确认失败状态:
broken_override_status=0
javac --release 17 -encoding UTF-8 -d broken BrokenOverride.java \
> broken-override.log 2>&1 || broken_override_status=$?
test "$broken_override_status" -ne 0
sed -n '1,12p' broken-override.log再把可重写方法设为 final,确认子类覆盖也得到编译错误。失败示例必须由 javac 验证,不能只靠注释猜测。
挑战:整数分支付契约
接口返回结果,不让调用者解析打印文本。保存为 PaymentMethod.java:
interface PaymentMethod {
PaymentResult pay(long cents);
}
final class PaymentResult {
private final boolean successful;
private final String reference;
private PaymentResult(boolean successful, String reference) {
this.successful = successful;
this.reference = reference;
}
static PaymentResult success(String reference) {
if (reference == null || reference.isBlank()) {
throw new IllegalArgumentException("blank reference");
}
return new PaymentResult(true, reference);
}
static PaymentResult declined() {
return new PaymentResult(false, "");
}
boolean successful() { return successful; }
String reference() { return reference; }
}实现一个只能支付现有余额以内金额的测试支付方式,保存为 TestWallet.java:
final class TestWallet implements PaymentMethod {
private long balanceCents;
TestWallet(long balanceCents) {
if (balanceCents < 0) {
throw new IllegalArgumentException("negative balance");
}
this.balanceCents = balanceCents;
}
long balanceCents() { return balanceCents; }
@Override
public PaymentResult pay(long cents) {
if (cents <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
if (cents > balanceCents) {
return PaymentResult.declined();
}
balanceCents -= cents;
return PaymentResult.success("test-" + cents);
}
}
final class Checkout {
static PaymentResult charge(PaymentMethod method, long cents) {
return java.util.Objects.requireNonNull(method, "method").pay(cents);
}
}测试成功、余额不足、0、负数和 null 方法,并检查失败调用没有扣款。test-金额 只是演示标签,多次同额支付会重复,不能当作唯一交易号。真实支付还需要幂等键、超时、重试边界、外部失败、审计和并发事务;多态只替换实现,不自动解决这些问题。
边界练习:模式匹配只放在适当位置
static String describe(Object value) {
if (value == null) return "null";
if (value instanceof Animal animal) {
return "animal:" + animal.name();
}
if (value instanceof String text) {
return "text:" + text;
}
return value.getClass().getSimpleName();
}这种代码适合日志或外部输入分类。若核心支付流程按 CreditCard、Wallet、Cash 不断增加 instanceof 分支,应把共同动作放回 PaymentMethod 契约。
Object 方法练习
为一个不可变值类型实现 toString、equals 和 hashCode,验证:
- 自反、对称、传递;
- 相等对象 hashCode 相同;
- null 不相等;
- 放入 HashSet 后相等副本不增加大小。
只要对象仍作为哈希集合的元素或哈希映射的键,就不要修改参与 equals/hashCode 的字段;这不是学完集合后就能放宽的规则。
统一测试入口
保存为 Oop2Lab.java。匿名类给小鸟提供一次性实现;Runnable 参数接收待执行的检查动作,后面的 Lambda 一讲会解释 () -> ...。入口先检查断言开关,防止漏写 -ea 却报告通过:
public class Oop2Lab {
public static void main(String[] args) {
boolean assertionsEnabled = false;
assert assertionsEnabled = true;
if (!assertionsEnabled) {
throw new IllegalStateException("run with -ea");
}
assert Zoo.introduce(new Dog("小黑")).equals("小黑:汪");
assert Zoo.introduce(new Cat("小白")).equals("小白:喵");
expectIllegal(() -> new Dog(" "));
expectNull(() -> Zoo.introduce(null));
Animal bird = new Animal("小灰") {
@Override String sound() { return "啾"; }
};
assert Zoo.introduce(bird).equals("小灰:啾");
TestWallet wallet = new TestWallet(5_000);
PaymentResult paid = Checkout.charge(wallet, 2_000);
assert paid.successful();
assert paid.reference().equals("test-2000");
assert !Checkout.charge(wallet, 4_000).successful();
expectIllegal(() -> Checkout.charge(wallet, 0));
expectIllegal(() -> Checkout.charge(wallet, -1));
expectNull(() -> Checkout.charge(null, 100));
assert wallet.balanceCents() == 3_000;
assert describe(null).equals("null");
assert describe(new Dog("小黑")).equals("animal:小黑");
assert describe("notice").equals("text:notice");
System.out.println("inheritance lab passed");
}
static String describe(Object value) {
if (value == null) return "null";
if (value instanceof Animal animal) {
return "animal:" + animal.name();
}
if (value instanceof String text) {
return "text:" + text;
}
return value.getClass().getSimpleName();
}
private static void expectIllegal(Runnable action) {
try {
action.run();
throw new AssertionError("expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// 参数契约按预期拒绝输入。
}
}
private static void expectNull(Runnable action) {
try {
action.run();
throw new AssertionError("expected NullPointerException");
} catch (NullPointerException expected) {
// null 边界按预期失败。
}
}
}javac --release 17 -encoding UTF-8 -Xlint:all,-auxiliaryclass -d out \
Animal.java PaymentMethod.java TestWallet.java Oop2Lab.java
java -ea -cp out Oop2Lab这些教学文件为控制篇幅合并了数个包级辅助类型,因此命令只排除由文件布局产生的 auxiliaryclass lint,其余警告仍开启。预期输出为 inheritance lab passed。保存实验记录后清理:
cd
ls -ld -- "$chapter8_oop_root"
rm -r -- "$chapter8_oop_root"离开这一单元前
新增 Dog、Cat 之外的动物时,Zoo 不应修改;错误的 @Override 与 final 覆盖必须由编译器确认。支付测试要分开记录成功、余额不足、非法金额和 null 方法,金额使用整数最小单位。多态能替换实现,却不会自动提供幂等、事务或线程安全。
完成对象单元后,再把短行为作为值传递。