🐸 青蛙编程网
首页 / Java / 面向对象 / 3. 继承与多态

继承与多态:代码复用与千变万化

这是什么:继承(Inheritance) = 子类直接拿到父类的代码;多态(Polymorphism) = 同一句调用跑出不同的行为。

为什么用:上一章封装把数据锁好了,可猫、狗、鸟都有名字都会叫、叫声却不同——不搞面向对象,就得给每种动物各写一遍「名字、叫声」,重复到怀疑人生。继承和多态正是冲着这份重复来的。

如何用:本章先用 extends 写继承,再试一次 @Override 重写,最后体会一句调用跑出多种面孔的多态。

继承:extends

class Cat extends Animal 意思是:猫是一种动物,自动获得父类的成员变量和方法。构造方法里用 super(name) 调用父类构造方法,先把继承来的部分初始化好。

判断"该不该继承"有个黄金标准——is-a 测试:把关系读成一句话,"猫是一种动物",通顺,可以继承;"汽车是一种发动机",不通顺,那就不是继承关系,该用"汽车有一个发动机"的组合思路。新手最常见的滥用就是拿 extends 表达"部分与整体",读一句 is-a 就能当场识破。

继承还有两条硬规则:

规则 说明
Java 只能 extends 一个类 一个儿子只有一个亲爹,多重继承靠接口补(下一章)
所有类都是 Object 的子孙 你不写 extends,默认继承 Object——toString、equals 就是从这来的

构造方法的执行顺序也值得一记:先父后子。创建 new Cat("咪咪") 时,super(name) 先把 Animal 那部分(name 字段)安顿好,再回来执行 Cat 自己的构造逻辑——就像装修房子先打地基再砌墙,顺序反不了。

方法重写:@Override

子类对父类的方法不满意,可以重写(override)同名方法,换成自己的实现。@Override 是个检查注解:如果你不小心把方法名拼错,编译器立刻报错,帮你当场抓 bug。

重写的合格标准:方法名相同、参数列表相同、返回值类型相同(或其子类),三同缺一不可。访问权限还有一条"就高不就低"的规矩:父类方法没加 private 或 static 这类限制的,才能被子类重写——构造方法和 private 方法不在重写赛道上。

初学者最绕的重写 vs 重载,一张表拆干净:

对比项 重写 override 重载 overload
位置 父子类之间 同一个类里(也可以跨继承)
方法名 相同 相同
参数列表 必须完全相同 必须不同
返回值 相同(或子类型) 任意(但它自己不算重载依据)
一句话 子类"改台词" 同名方法"多条台词"

@Override 不写也能重写成功,但强烈建议永远写上——它让编译器替你把关"这真的是在重写吗"。设想你把 makeSound 手滑敲成 makeSond:不写注解,Java 以为你在定义一个全新方法,程序照跑,猫突然不叫了,你还得排查半天;写了注解,编译当场报错,一秒破案。

多态:一句调用,多种面孔

父类引用指向子类对象:Animal a = new Cat("咪咪");。调用 a.makeSound() 时,Java 看对象实际是谁——是猫就喵,是狗就汪。代码只写一遍,行为千变万化,这就是多态。

记这句口诀:编译看左边,运行看右边。Animal a = new Cat(...) 里,左边声明类型 Animal 决定"这个引用能调哪些方法"(编译器按它做安检),右边实际类型 Cat 决定"这些方法怎么跑"(运行时执行猫的版本)。

由此推出一个重要限制:父类引用调不到子类特有的方法。Cat 有个 scratch()(挠沙发),Animal a 这个引用看不见它——安检表上只登记了 Animal 的成员。想用就得"还原身份":

Animal a = new Cat("咪咪");

if (a instanceof Cat) {      // 先问:你真是猫吗?
    Cat c = (Cat) a;         // 强转还原
    c.scratch();             // 现在能调猫的特技了
}

instanceof 是"验明正身"的运算符,返回 boolean。先验后转是铁纪律——不验直接转,转错了运行时会抛异常。多态的真实工作流往往是:手里攥着一堆 Animal,先 instanceof 甄别,再各施各法。

完整示例

保存为 Main.java 运行(文件名跟 public 类名走):

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void makeSound() {
        System.out.println(name + " 在叫");
    }

    @Override
    public String toString() {   // 所有类默认继承 Object,重写它打印更好看
        return "Animal(" + name + ")";
    }
}

class Cat extends Animal {
    Cat(String name) {
        super(name);   // 调用父类构造方法
    }

    @Override
    void makeSound() {
        System.out.println(name + ":喵喵喵");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + ":汪汪汪");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal[] zoo = { new Cat("咪咪"), new Dog("旺财") };
        for (Animal a : zoo) {
            a.makeSound();   // 同一句调用,行为不同:多态
            System.out.println(a);   // 打印对象,自动调用重写后的 toString
        }
    }
}

运行后输出:

咪咪:喵喵喵
Animal(咪咪)
旺财:汪汪汪
Animal(旺财)

这段代码藏着一个精妙的点:Animal[] zoo 数组里能同时装猫和狗——因为"猫是一种动物"成立,子类对象可以放进父类数组。然后增强 for 挨个喊 a.makeSound(),喊到猫出猫声、喊到狗出狗声。设想动物园加一只鸟:只需要新增一个 Bird 类重写 makeSound,主循环一行不改——这就是多态对"扩展开放"的承诺:加新东西不加新 if。

再体会一下 toString 的地位:System.out.println(a) 打印的明明是个对象,出来的却是友好文本,因为 println 内部会自动调用对象的 toString()。它是最常被重写的方法,重写它等于给每个类配一张"名片"。

逐行解读

代码 作用
class Cat extends Animal 继承:猫自动拥有 name、makeSound,只写自己的差异
super(name) 调用父类构造方法,先初始化继承来的部分
@Override 重写标记:拼错方法名编译器会立刻指出
Animal a = new Cat("咪咪") 多态:父类引用指向子类对象
a.makeSound() 实际类型说了算:是猫执行猫版,是狗执行狗版
System.out.println(a) println 自动调用 toString,重写后输出友好文本

补充一个 super 的隐藏用法:super 不只能在构造方法里用。在子类方法里写 super.makeSound(),可以调用父类的原版实现——典型场景是"先照父类的做,再补点自己的":

@Override
void makeSound() {
    super.makeSound();           // 先执行父类版:"咪咪 在叫"
    System.out.println("(撒娇中)喵喵喵");   // 再加点自己的戏
}

这种"扩展式重写"比"推倒重来"更省代码,父类逻辑有更新时子类自动跟着受益。

❓ 常见问题

Q1:Java 为什么不让一个类继承多个父类?

想象两个父类都有 makeSound,子类到底听谁的?这叫"菱形冲突"。Java 干脆规定单继承,把"多份标准"的需求交给接口(下一章)——接口只定标准不含实现,冲突小得多。

Q2:父类的 private 成员,子类继承得到吗?

继承得到但看不见:private 字段确实跟着对象一起存在,但子类代码不能直接访问,只能通过父类的 public 方法间接操作。这也呼应封装章的思想——私密的部分锁在父类自己家里。

Q3:Cat c = new Cat(...) 也能调 makeSound,多态多此一举吗?

单个对象看不出差别,差别在"批处理"。把猫、狗、鸟全塞进一个 Animal[],用一个循环统一调度——用一种类型管理千种子类,才是多态的主战场。真实项目里,"订单、退款、物流都实现了消息接口,一个循环统一推送"就是这么玩的。

Q4:构造方法能被继承或重写吗?

都不能。构造方法名字必须和类名一致,子类和父类名字都不同,天生无重写一说;子类要初始化父类部分,只能显式 super(...) 或依赖默认规则。

⚠️ 新手翻车现场

翻车一:不写 @Override 还拼错了方法名

class Cat extends Animal {
    void makeSond() {                     // 手滑拼错,又没写注解
        System.out.println(name + ":喵喵喵");
    }
}

运行后输出:

咪咪 在叫

问题:猫不叫了!makeSond 是个全新的方法,父类的 makeSound 原封未动,多态悄悄失效——程序不报错,行为却错得离谱,这类"安静的 bug"最难排查。

修正:给重写方法统一加 @Override,拼错编译器当场报警。

翻车二:父类引用调子类特有方法

Animal a = new Cat("咪咪");
a.scratch();   // ❌ 编译报错

完整报错:

错误: 找不到符号
  符号: 方法 scratch()
  位置: 类型为Animal的变量 a

问题:编译器按"左边的声明类型 Animal"做安检,安检表上没有 scratch 这个方法,直接拦下。

修正:instanceof 验明正身后强转成 Cat 再调用。

翻车三:父类只有带参构造方法,子类忘了 super

class Animal {
    Animal(String name) { this.name = name; }
}

class Cat extends Animal {
    Cat() {                    // ❌ 编译报错
        System.out.println("猫来了");
    }
}

完整报错:

错误: 无法将类 Animal中的构造器 Animal应用到给定类型

问题:子类构造方法默认会先调父类的无参构造 super(),但父类只定义了带参版本,无参的"失踪"了,子类找不到人接班。

修正:子类构造里显式写 super("咪咪");,或者给父类补一个无参构造方法。

📝 本章小结

  • 继承用 extends,先过 is-a 测试;Java 单继承,万类之祖是 Object
  • 构造顺序先父后子,super(...) 负责调用父类构造方法
  • 重写三同:方法名、参数、返回值都要同;@Override 永远写上,拼错当场报
  • 重写是父子类改实现,重载是同类内换参数,别混
  • 多态口诀:编译看左边(能调什么),运行看右边(怎么执行);父类引用调不到子类特有方法,要用先 instanceof 再强转
  • 记忆口诀:extends 继家产,super 问爹要,@Override 显身手,父引用指向子对象,跑起来是多态。

记忆锚点:继承是"子承父业"——家产(字段)和手艺(方法)自动到手,手艺不满意的可以重修(重写);多态是"同一个岗位,各人各干法"——老板(循环)只喊开工,不关心你是猫工还是狗工;上岗前看合同(编译看左边),干活时看本人(运行看右边)。

动手改

  1. 加一个 Bird 子类,makeSound 打印"叽叽叽",放进 zoo 数组再跑一遍
  2. 在 Cat 里重写 toString 返回 "猫咪(" + name + ")",看打印结果怎么变
  3. 试试让父类引用 a 调用子类特有的方法,感受"父类引用看不到子类特有方法"的限制
  4. 给 Dog 的 makeSound 加一句 super.makeSound(),观察"扩展式重写"的输出顺序
  5. 写一个循环统计 zoo 里猫的数量(提示:a instanceof Cat 计数)

下一章:接口与常用集合,认识 Java 的内置工具箱 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。