跳到内容

元数据卡

  • 前置知识:第7章(类与对象、封装)
  • 预计时间:15 分钟
  • 阅读模式:概念理解
  • 完成标志:能用 extends 消除重复代码,理解 is-a 关系

你的进度

你在变量村的铁匠铺里蹲了一个星期了。老陈师傅教了你如何把属性(字段)和行为(方法)打包成"类"这种工具箱,还教会了你用 private 锁住关键零件——你已经有封装的概念了。

但这两天你在做一个麻烦的活儿。

村口需要一套"通信系统":驿站跑马传信、信鸽从空中送信、村头大喇叭喊话。嗯,你手上已经有了几个类——HorsePigeonLoudspeaker。每个类都有自己的 send() 方法,但代码里面有大量重复的东西:

  • 都有"消息内容"
  • 都要"格式化"
  • 都要"记录发送时间"

你复制粘贴了三次,现在 Horse 里改了一行,得去 PigeonLoudspeaker 里同样改一遍。你已经忘了昨晚改了哪个没改哪个。

"你这孩子,"老陈师傅叹了口气,拿起一块铁胚,"不学会站在别人的基础上造东西,你一辈子都在打一模一样的锄头。"

他把那块铁胚放在你面前:"这叫继承。"

你的任务

旧办法:复制粘贴做三个相似的类。问题在于——改一处要改三处,加一处要加三处,一个 bug 可以潜伏两份你没改的副本里。

这一章要解决的问题很简单:能不能只写一次公共逻辑,让其他类型说"我继承它"?


第一幕:extends——站在前人的肩膀上

你有三个类:HorsePigeonLoudspeaker,它们都共享"消息"这个公共概念。你把公共部分抽出来,放到一个叫 MessageSender 的类里:

java
// MessageSender.java — 公共消息发送能力
public class MessageSender {
    String message;
    long timestamp;

    public MessageSender(String message) {
        this.message = message;
        this.timestamp = System.currentTimeMillis();
    }

    public void send() {
        System.out.println("[LOG] 发送时间: " + timestamp);
        System.out.println("消息: " + message);
    }
}

语言:Java 17+ 如何运行:保存为 .java 文件,用 javac 编译后,用 java 运行

现在,Horse 不再从零开始写了——它 extends(继承)MessageSender

java
// Horse.java — 继承 MessageSender
public class Horse extends MessageSender {
    private String riderName;

    public Horse(String message, String riderName) {
        super(message); // 调用父类构造方法,必须先写
        this.riderName = riderName;
    }

    @Override
    public void send() {
        super.send(); // 先执行父类的公共行为
        System.out.println("骑士 " + riderName + " 策马奔腾!");
    }
}

语言:Java 17+ 预期输出:调用 new Horse("敌军来袭", "张三").send() 时,会输出时间戳、消息,然后输出"骑士 张三 策马奔腾!" 你试试:把 super(message) 注释掉,看编译器报什么错

super 就是你喊的那声"爸,干活了"——它引用父类。super(message) 调用父类构造器,super.send() 调用父类被重写前的方法。

信鸽也是继承:

java
// Pigeon.java — 继承 + 不同的 send 实现
public class Pigeon extends MessageSender {
    private int speedLevel; // 1 = 普通, 2 = 加急

    public Pigeon(String message, int speedLevel) {
        super(message);
        this.speedLevel = speedLevel;
    }

    @Override
    public void send() {
        super.send();
        String level = speedLevel == 2 ? " 加急!" : " 普通";
        System.out.println(level + " 信鸽起飞!");
    }
}

预期输出new Pigeon("天气转晴", 2).send() → 先打印时间戳和消息,再打印" 加急!信鸽起飞!"


常见陷阱

陷阱一:继承滥用——"我是你的子类"不等于"我是你的"

新手最大的坑:为了复用方法而继承。

java
// 错误示范
public class Dog extends ArrayList<Toy> { // 狗是一堆玩具???
    public void bark() { System.out.println("汪!"); }
}

狗有玩具,但狗不是玩具的列表。继承表达的是 "is-a"(是一个)关系,不是 "has-a"(有一个)关系

正确做法是用组合(composition):

java
// 正确示范
public class Dog {
    private List<Toy> toys = new ArrayList<>(); // 狗有玩具
    public void bark() { System.out.println("汪!"); }
}

陷阱二:super 链——构造方法要按顺序来

java
public class Loudspeaker extends MessageSender {
    public Loudspeaker(String message) {
        // 如果不写 super(message),编译器调用 super()——但 MessageSender 没有无参构造器
        // 编译错误!
    }
}

规则:子类构造方法必须在第一行调用父类构造方法。如果你不写,编译器会插入 super()(调用父类无参构造)。如果父类没有无参构造,编译报错。

链式调用顺序:当你 new Horse(...) 时,实际调用链是:

  1. MessageSender 的构造器先执行
  2. 然后 Horse 的构造器再执行

这叫构造链——从最顶层的父类一路往下。


旅人笔记

继承消除重复代码。 super 调用父类构造器或父类方法。 继承表达 is-a,组合表达 has-a。 构造链从父类到子类自上而下执行。

下一站预告

继承让你消除了重复代码。但更强大的能力藏在它的背后——多态:写一段代码,让它自动适配不同的子类类型。

Built with VitePress | Software Systems Atlas