← Java 基础教程

第 23 / 40 课 · 免费教程

行为契约

抽象类打骨架,接口约定能力

90–110 分钟本章使用 JDK 21,无需预览功能。面向对象进阶

本课作品

制作三种能力合同 AbilityContracts

蝙蝠和鞘翅玩家并非同一家族,却都能飞。用接口约定“会什么”,用抽象类保存共同骨架,让能力合同小而清楚。 前置背包:继承、多态和方法重写。今天不按长相组队,而是按能力签合同。

开始前:建议先完成第 21–22 课

共同状态已经写好 差异行为交给子类
  1. Actor抽象共同骨架
  2. name / label已完成的部分
  3. role()留给子类回答

01 / 14

开始前:先预测,再让程序回答

本章使用 JDK 21,无需预览功能。故事借用方块世界,示例运行在普通 Java 控制台;数值和规则若是课程模型,都会明确说明。

每个普通示例都是完整独立文件,按框上标注的文件名保存。各示例放在不同文件夹,避免辅助类重名。完整项目分段时,按顺序接在同一个文件中。

终端命令:中文占位符换成实际文件名与类名

javac -encoding UTF-8 示例文件名.java
java 示例类名
  • 预测:先写预期输出,或画清引用和数据的变化。
  • 验证:运行后逐行核对,再只改一个条件。
  • 解释:说清结果来自 Java 规则、课程合同还是输入数据。

02 / 14

抽象类留下必须完成的空格

若所有角色都有名字,却没有统一的 role 实现,父类就不该假装给出一个无意义答案。抽象类允许写好共同部分,并明确留下未完成的方法。

共同状态已经写好 差异行为交给子类
  1. Actor抽象共同骨架
  2. name / label已完成的部分
  3. role()留给子类回答

BadAbstract.java · 故意错误示例

abstract class Actor {
    protected final String name;
    Actor(String name) { this.name = name; }
    abstract String role();
    public String label() { return role() + " " + name; }
}
public class BadAbstract {
    public static void main(String[] args) { Actor a = new Actor("未完成"); }
}

03 / 14

抽象方法强制子类回答

抽象方法像试卷上的必答题。子类若仍是具体类,就必须给出正确签名的方法体。

没有方法体 但名字参数和返回类型完整
  1. 具体子类必须完成抽象方法
  2. @Override核对方法签名
  3. Bat提供蝙蝠角色

L23Example02.java · 完整独立示例

abstract class Actor {
    protected final String name;
    Actor(String name) { this.name = name; }
    abstract String role();
    public String label() { return role() + " " + name; }
}
class Bat extends Actor {
    Bat(String name) { super(name); }
    @Override String role() { return "蝙蝠"; }
}
public class L23Example02 {
    public static void main(String[] args) {
        Actor a = new Bat("夜影");
        System.out.println(a.label());
    }
}

运行结果

蝙蝠 夜影

BadRole.java · 故意错误示例

abstract class Actor {
    protected final String name;
    Actor(String name) { this.name = name; }
    abstract String role();
    public String label() { return role() + " " + name; }
}
class Bat extends Actor { Bat(String name) { super(name); } }
public class BadRole { public static void main(String[] args) { } }

04 / 14

接口只描述会不会

接口适合约定跨家族能力。蝙蝠是动物,鞘翅玩家是玩家,但两者都能实现 Flyable;调用者只需知道对方会飞。

不要求同一家族 只要求相同能力
  1. Flyable约定飞行能力
  2. Bat实现飞行
  3. ElytraPlayer也实现飞行

L23Example03.java · 完整独立示例

interface Flyable { String fly(); }
class Bat implements Flyable {
    @Override public String fly() { return "蝙蝠飞行"; }
}
class ElytraPlayer implements Flyable {
    @Override public String fly() { return "玩家滑翔"; }
}
public class L23Example03 {
    public static void main(String[] args) {
        Flyable[] team = {new Bat(), new ElytraPlayer()};
        for (Flyable member : team) System.out.println(member.fly());
    }
}

运行结果

蝙蝠飞行
玩家滑翔

05 / 14

实现方法必须公开

接口方法默认是 public。实现类必须用 public 提供同样可见的入口。

接口承诺是公开入口
  1. 接口抽象方法默认 public
  2. 实现方法不能缩小可见性
  3. @Override核对同一合同

BadPublic.java · 故意错误示例

interface Flyable { String fly(); }
class Bat implements Flyable { String fly() { return "飞行"; } }
public class BadPublic { public static void main(String[] args) { } }

06 / 14

一个类可以签多份合同

Java 类只能直接继承一个父类,却可以实现多个接口。一个飞行炸弹可以同时遵守 Flyable 和 Explodable。

继承只有一个父类 接口可以有多个
  1. 一个父类类只能直接继承一个
  2. 多个接口能力可以组合
  3. FlyingMine同时会飞和引爆

L23Example05.java · 完整独立示例

interface Flyable { String fly(); }
interface Explodable { String explode(); }
class FlyingMine implements Flyable, Explodable {
    @Override public String fly() { return "飞向目标"; }
    @Override public String explode() { return "引爆"; }
}
public class L23Example05 {
    public static void main(String[] args) {
        FlyingMine mine = new FlyingMine();
        System.out.println(mine.fly());
        System.out.println(mine.explode());
    }
}

运行结果

飞向目标
引爆

07 / 14

接口引用调用能力

方法参数写接口类型,就能接收任何实现者。这是多态从家族关系扩展到能力关系。

工具方法只依赖所需合同
  1. 接口参数只要所需能力
  2. 任意实现者可接受相同触发
  3. null本课约定安全跳过

L23Example06.java · 第1/2段,顺序接在同一文件

interface Flyable { String fly(); }
interface Tradable { String trade(String item); }
interface Explodable { int blastPower(); String explode(); }
abstract class Actor {
    protected final String name;
    Actor(String name) { this.name = name; }
    abstract String role();
    public String label() { return role() + " " + name; }
}
class Bat extends Actor implements Flyable {
    Bat(String name) { super(name); }
    @Override String role() { return "蝙蝠"; }
    @Override public String fly() { return name + " 飞向高处"; }
}
class Villager extends Actor implements Tradable {
    Villager(String name) { super(name); }
    @Override String role() { return "村民"; }
    @Override public String trade(String item) {
        return item == null || item.isBlank()
            ? name + " 拒绝空交易" : name + " 收下" + item.strip();
    }
}
class Creeper extends Actor implements Explodable {
    Creeper(String name) { super(name); }
    @Override String role() { return "苦力怕"; }
    @Override public int blastPower() { return 3; }
    @Override public String explode() { return name + " 爆炸"; }
}

L23Example06.java · 第2/2段,顺序接在同一文件

public class L23Example06 {
    static void triggerFlight(Flyable target) {
        if (target != null) System.out.println(target.fly());
    }
    static void triggerTrade(Tradable target, String item) {
        if (target != null) System.out.println(target.trade(item));
    }
    static void triggerExplosion(Explodable target) {
        if (target != null)
            System.out.println(target.explode() + " 强度=" + target.blastPower());
    }
    public static void main(String[] args) {
        triggerFlight(new Bat("夜影"));
        triggerTrade(new Villager("村长"), "绿宝石");
        triggerExplosion(new Creeper("绿豆"));
        triggerFlight(null);
    }
}

运行结果

夜影 飞向高处
村长 收下绿宝石
绿豆 爆炸 强度=3

08 / 14

小接口比万能杂物袋清楚

把飞行、交易、爆炸塞进一个 MegaAbility,会迫使村民实现不会的 fly。小接口更容易理解和组合。

每份合同围绕一个稳定能力
  1. Flyable只负责飞行
  2. Tradable只负责交易
  3. Explodable只负责爆炸

L23Example07.java · 完整独立示例

interface Flyable { String fly(); }
interface Tradable { String trade(String item); }
interface Explodable { int blastPower(); String explode(); }
abstract class Actor {
    protected final String name;
    Actor(String name) { this.name = name; }
    abstract String role();
    public String label() { return role() + " " + name; }
}
class Bat extends Actor implements Flyable {
    Bat(String name) { super(name); }
    @Override String role() { return "蝙蝠"; }
    @Override public String fly() { return name + " 飞向高处"; }
}
class Villager extends Actor implements Tradable {
    Villager(String name) { super(name); }
    @Override String role() { return "村民"; }
    @Override public String trade(String item) {
        return item == null || item.isBlank()
            ? name + " 拒绝空交易" : name + " 收下" + item.strip();
    }
}
class Creeper extends Actor implements Explodable {
    Creeper(String name) { super(name); }
    @Override String role() { return "苦力怕"; }
    @Override public int blastPower() { return 3; }
    @Override public String explode() { return name + " 爆炸"; }
}
public class L23Example07 {
    public static void main(String[] args) {
        Tradable merchant = new Villager("村长");
        System.out.println(merchant.trade(""));
    }
}

运行结果

村长 拒绝空交易

09 / 14

抽象类与接口比较

抽象类可保存实例状态、构造器和共同实现;接口强调调用者可依赖的能力。一个类常常继承抽象父类,同时实现若干接口。

家族骨架和能力合同解决不同问题
  1. 抽象类状态、构造器、共同实现
  2. 接口调用者依赖的能力
  3. 组合使用继承骨架并实现接口
验收对照表
问题抽象类接口
表达同一家族骨架跨家族能力
实例字段可以不保存普通实例状态
构造器可以没有
数量只能继承一个可实现多个
典型例子ActorFlyable

10 / 14

合同测试与故障诊所

接口错误常在编译阶段暴露,行为错误仍要通过测试发现。

编译器检查签名 运行测试检查行为
  1. 签名匹配参数不能遗漏
  2. 空交易明确返回拒绝
  3. 爆炸强度本课约定3

BadTrade.java · 故意错误示例

interface Tradable { String trade(String item); }
class Villager implements Tradable {
    public String trade() { return "交易"; }
}
public class BadTrade { public static void main(String[] args) { } }
验收对照表
测试输入预期
飞行Bat返回飞行描述
交易空物品名返回拒绝提示
爆炸Creeper强度为3
空引用null安全提示或跳过

11 / 14

挑战 蓝图三种能力

挑战 程序叫 AbilityContracts。Bat 会飞,Villager 会交易,Creeper 会爆炸;三者都继承 Actor 并实现各自能力。

抽象角色提供名字 三份接口提供能力入口
  1. Bat飞行合同
  2. Villager交易合同
  3. Creeper爆炸合同

验收条件

  • Actor 不能直接 new。
  • 三个具体类都实现 role。
  • 接口实现方法全部 public。
  • 触发方法使用接口参数。
  • 交易空字符串时返回拒绝提示。

12 / 14

挑战 实现能力合同

新建 AbilityContracts.java,使用 Java 21 编译运行。

飞行 交易 爆炸通过接口统一触发
  1. 先声明三个接口与 Actor
  2. 再实现逐个完成具体类
  3. 后触发接口参数统一调用

AbilityContracts.java · 第1/2段,顺序接在同一文件

interface Flyable { String fly(); }
interface Tradable { String trade(String item); }
interface Explodable { int blastPower(); String explode(); }
abstract class Actor {
    protected final String name;
    Actor(String name) { this.name = name; }
    abstract String role();
    public String label() { return role() + " " + name; }
}
class Bat extends Actor implements Flyable {
    Bat(String name) { super(name); }
    @Override String role() { return "蝙蝠"; }
    @Override public String fly() { return name + " 飞向高处"; }
}
class Villager extends Actor implements Tradable {
    Villager(String name) { super(name); }
    @Override String role() { return "村民"; }
    @Override public String trade(String item) {
        return item == null || item.isBlank()
            ? name + " 拒绝空交易" : name + " 收下" + item.strip();
    }
}
class Creeper extends Actor implements Explodable {
    Creeper(String name) { super(name); }
    @Override String role() { return "苦力怕"; }
    @Override public int blastPower() { return 3; }
    @Override public String explode() { return name + " 爆炸"; }
}

AbilityContracts.java · 第2/2段,顺序接在同一文件

public class AbilityContracts {
static void triggerFlight(Flyable target) {
    if (target != null) System.out.println(target.fly());
}
static void triggerTrade(Tradable target, String item) {
    if (target != null) System.out.println(target.trade(item));
}
static void triggerExplosion(Explodable target) {
    if (target != null)
        System.out.println(target.explode() + " 强度=" + target.blastPower());
}
    public static void main(String[] args) {
        Bat bat = new Bat("夜影");
        Villager villager = new Villager("村长");
        Creeper creeper = new Creeper("绿豆");
        System.out.println(bat.label());
        triggerFlight(bat);
        triggerTrade(villager, "绿宝石");
        triggerTrade(villager, " ");
        triggerExplosion(creeper);
        triggerFlight(null);
    }
}

运行结果

蝙蝠 夜影
夜影 飞向高处
村长 收下绿宝石
村长 拒绝空交易
绿豆 爆炸 强度=3

13 / 14

拓展任务与通关复盘

每份接口都要附一句合同说明,写清输入、返回值和失败处理。

三档任务扩展能力合同
  1. 合同说明写清输入与输出
  2. 失败规则明确拒绝或跳过
  3. 编译与行为两种检查都需要

三档作业

  • 木镐:设计 Swimmable,让海豚和玩家都能实现 swim。
  • 铁镐:让一个类同时实现 Flyable 与 Tradable,并用两个接口引用调用。
  • 钻石镐:设计红石设备接口,分别实现门、灯和陷阱,并写统一触发器。
验收对照表
自评能还要练
我能区分抽象类和接口□□
我会使用 implements□□
我会设计小接口□□

14 / 14

自己的验收记录

漂亮的输出是起点,能解释边界才算掌握。把正常、边界与错误各选一个案例,运行前写预期,运行后留下证据。

为判断留下依据
类别你选择的操作核对重点
正常一个常用输入或一次成功动作结果是否逐行一致
边界范围端点与相邻值有没有多一步、少一项
错误非法数量、缺失对象或坏数据是否明确拒绝,状态是否保持
  • Bug 记录:触发条件、实际现象、原因、最小修复。
  • 图示记录:为每条箭头说明起点、终点和含义。
  • 改编记录:只改一个规则,同时修改预期和测试。

TAKE IT WITH YOU

下载本课 PDF

完整课文、代码、图解与练习,选择你需要的语言。