Menu

Java 继承:extends、super 与方法重写

Java 子类如何用 extends 继承字段和方法、通过 super 调用父类、重写行为,以及常见的陷阱。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

继承是“是一个”式的复用

继承让新类可以在已有类的基础上构建。新类叫子类,它自动获得父类的字段和方法,再按需添加或修改。当一种类型是另一种类型的更具体的种类时,就该用继承:Dog 是一种 Animal,SavingsAccount 是一种 BankAccount。

继承用 extends 关键字来写。父类中所有 public 或 protected 的成员,子类都可以直接使用,不用重写一遍。

Dog 从来没有声明 name 或 eat(),却两者都有。这正是关键所在:共享的行为只放在一个地方。

super:调用父类

子类的构造函数必须确保父类先完成初始化。做法是使用 super(...),它调用父类的构造函数,并且必须是子类构造函数中的第一条语句。如果你省略它,Java 会悄悄插入一次对父类无参构造函数的调用;而如果父类没有这样的构造函数,代码就无法编译。

输出显示,父类构造函数在子类构造函数体之前运行:对象的构造从继承层次的顶部往下进行。

重写方法

子类可以用相同的签名重新定义继承来的方法,从而替换它。这叫重写,你应该始终用 @Override 标注它。这个注解不是必需的,但它会让编译器确认你确实匹配了父类的某个方法。它能发现像把 toString() 写成 tostring() 这样的拼写错误,否则这种错误会悄悄地创建一个全新的方法。

虽然数组的类型是 Animal,每个元素运行的却是它自己的 speak()。Java 在运行时根据真实的对象来选择方法,而不是根据变量声明的类型,这就是多态的基础。

用 super.method() 调用父类的版本

重写并不一定要丢掉父类的工作。用 super.method() 运行继承来的版本,然后再加上你自己的逻辑:

如果没有 super.,在 TimestampLogger.log 里调用 log 就会调用它自己,无限递归下去。super. 明确表示“父类的版本”。

继承的字段与访问权限

子类能看到父类的 public 和 protected 成员,但看不到它的 private 成员。private 字段仍然存在于对象中,父类自己的方法可以访问它们,但子类不能直接引用。当你希望子类可以访问某个成员、同时又对无关代码隐藏它时,就用 protected,它是 Java 的访问修饰符之一。

class Base {
    private int secret;      // invisible to subclasses
    protected int shared;    // visible to subclasses
}

class Derived extends Base {
    void demo() {
        shared = 5;          // OK
        // secret = 5;       // compile error - private to Base
    }
}

这也是为什么子类的构造函数经常必须调用 super(...):这是初始化父类私有状态的唯一途径。

用 final 阻止继承

有时一个类根本不应该被继承,String 就是出于这个原因被声明为 final 的。把类标记为 final 会禁止子类化;把方法标记为 final 会禁止重写这个方法,但类本身仍然可以被继承。

final class Constants { }            // cannot be subclassed

class Config {
    final void load() { }            // subclasses may extend Config
                                     // but cannot override load()
}

当一个类的行为必须在整个程序中得到保证、不可改变时,就用 final。它是一个有意为之的“不要继承”信号,而不是默认做法。

常见陷阱:不是“是一个”关系时,优先用组合

继承很诱人,因为它能复用代码,但它会把子类和父类紧紧绑在一起。如果两者之间不是真正的“是一个”关系,比如一辆 Car 恰好需要一个 Engine,就不要写 Car extends Engine。汽车拥有一个引擎,而不是一个引擎。应该改用字段(组合)来建模:

class Car {
    private Engine engine = new Engine();   // Car HAS-A Engine

    void start() { engine.ignite(); }
}

只有当子类确实是父类的一种特殊形式,并且你既想继承又想替换它的行为时,才使用继承。

下一篇:接口

用 extends 实现的继承给你一个父类和共享的实现。但一个类只能继承一个类,那么如何让彼此无关的类拥有共同的能力呢?这正是接口的用途:一份可以被许多类实现的契约,也是下一篇的主题。

常见问题

Java 中的继承是什么?

继承让一个类(子类)通过 extends 关键字复用另一个类(父类)的字段和方法。子类自动获得父类的 public 和 protected 成员,可以添加新成员,也可以通过重写替换继承来的行为。它描述的是“是一个”(is-a)关系:Dog 是一种 Animal。

Java 中的 super 关键字有什么作用?

super 指向父类。构造函数中的 super(...) 调用父类的构造函数(必须是第一条语句),而 super.method() 调用你已重写的方法在父类中的版本。它让子类可以在父类逻辑的基础上扩展,而不是把它完全替换掉。

Java 中重写和重载有什么区别?

重写(overriding)是在子类中用相同的签名重新定义继承来的方法,从而改变其行为,要用 @Override 标注。重载(overloading)是在同一个类中定义多个同名但参数列表不同的方法。重写关乎继承和运行时分派;重载只是两个碰巧同名的方法。

Coddy 编程语言插图

和 Coddy 一起学编程

开始