# 继承：子类从父类那里拿到了什么，又拿不到什么

extends 让子类得到哪些成员、private 与构造器为什么拿不到，new 子类对象时构造器链的执行顺序，以及重写怎样改变行为

> 继承 · 构造器 · 重写与多态 · 约 6 分钟 · 10 月 17 日

## 本篇要点

1. 子类用 extends 声明，自动获得父类所有非 private 的字段和方法，可以直接当自己的成员使用；每个子类对象各带一份父类字段，不是共享。
2. private 成员不被子类继承：子类代码不能按名字访问它，但对象里仍有这块空间，可通过继承来的公开方法间接读写；访问控制限制的是“能否按名字碰”，不是“数据是否存在”。
3. 构造器不属于类的成员，所以不被继承。子类想按特定参数创建对象，必须自己写构造器。
4. new 子类对象时执行顺序为：堆上分配内存并把全部字段（含父类的）设为默认值 → 进入子类构造器执行 super(...) → 父类构造器完整跑完（若有更上层继续往上） → 回到子类，先执行子类字段初始值，再执行 super(...) 之后的语句。
5. super(...) 必须是子类构造器的第一条语句；不写则由编译器自动补 super()，因此父类若没有无参构造器，子类必须显式调用 super(...)，否则编译失败。
6. 重写要求方法名与参数列表完全相同、返回类型相同或是其子类型、访问权限不能收窄；实例方法与 static 不能互换。@Override 让编译器核对是否真的构成重写。
7. 调用哪个方法版本由运行时对象的实际类型决定（virtual method invocation），不由变量的声明类型决定；但要通过父类类型变量调用，父类必须声明过该方法。

---

上一篇的 `Account` 只有户名和余额。现在要加一种“储蓄账户”：在普通账户的基础上多一个年利率字段，多一个按利率算利息的方法。

最直白的做法是把 `Account` 整个文件复制一份，改名 `SavingsAccount`，再补上利率那几行。能跑。但余额的存取逻辑就被抄成了两份：以后 `Account` 改了规则，两个文件都得改，很容易漏掉一个。

**继承**（inheritance）解决的就是这种“我大部分和另一个类相同，只多一点自己的特殊性”的需求。这一篇讲清楚三件事：`extends` 到底给了子类什么、没给什么；`new` 一个子类对象时构造器按什么顺序跑；子类怎样把父类的某个行为换成自己的。

## `extends` 让子类得到什么

```java
public class SavingsAccount extends Account {
    // 这里只写你自己新增的东西
}
```

`SavingsAccount extends Account` 读作“SavingsAccount 继承 Account”。`SavingsAccount` 是**子类**（subclass），`Account` 是它的**父类**（superclass）。子类自动获得父类里所有非 private 的字段和方法，用起来跟自己写的一模一样。所以 `SavingsAccount` 的代码里可以直接调用继承来的 `deposit(...)`、`getBalance()`，尽管这两个方法一个字也没出现在它自己的文件里 [1]。

“继承来的字段”要理解准确：每个 `SavingsAccount` 对象都自带一份 `owner`、一份 `balance`，就像它自带自己的 `rate` 一样。父类的字段不是两边共享的公共变量，而是每个子类对象各有一份。

## 子类拿不到什么

先把 `Account` 的两个字段声明成 `private`。`private` 的意思是“只有本类内部的代码能访问”，它和 `public` 一样是访问修饰符。

```java
public class Account {
    private String owner;
    private int balance;

    public Account(String who, int start) {
        owner = who;
        balance = start;
    }

    public void deposit(int amount) {
        balance = balance + amount;
    }

    public int getBalance() {
        return balance;
    }

    public String describe() {
        return owner + " 的余额：" + balance;
    }
}
```

有两样东西不会原样交到子类手上。

第一，**private 成员**。在 `SavingsAccount` 里直接写 `balance = 0;` 会编译错误：编译器根本不认识这个名字。但要注意“拿不到 ≠ 不存在”。对象在内存里照样带着 `owner`、`balance` 这两块空间，只是子类源码不能按名字碰它。想读想改，得走父类提供的公开方法：`getBalance()`、`deposit(...)`。这是访问控制，不是数据没了 [1]。（另有一档 `protected`，介于 `public` 和 `private` 之间，专门为子类准备；你现在用 `public` 和 `private` 就够。）

第二，**构造器不被继承**。构造器不属于类的“成员”，所以根本谈不上继承 [1]。`new SavingsAccount("张三", 1000, 0.03)` 之所以能这么写，不是因为它从 `Account` 那儿继承了 `Account(String, int)`，而是因为你在子类里自己写了一个构造器——这是下面要讲的。

## `new` 一个子类对象时，构造器怎么排队

```java
public class SavingsAccount extends Account {
    private double rate;

    public SavingsAccount(String who, int start, double r) {
        super(who, start);      // 第一句：转去调父类构造器
        rate = r;
    }

    public double getRate() {
        return rate;
    }
}
```

执行 `new SavingsAccount("张三", 1000, 0.03)` 时，按这个顺序发生：

1. 在堆上划出空间，把**整个对象**的字段都放进去——父类的 `owner`、`balance`，加子类自己的 `rate`。先全部设默认值：`null`、`0`、`0.0`。
2. 进入 `SavingsAccount` 的构造器。第一句 `super(who, start)` 不是调用当前类的成员，而是转去执行父类的构造器。
3. 父类构造器完整跑完：`owner = who; balance = start;`。如果 `Account` 上面还有父类（其实每个类最终都继承自 `Object`），这一层会继续往上，一路顶到底再往下走。
4. 回到子类构造器，先执行子类字段的初始值（如果有 `private double rate = 0.03;` 这类写法），再执行 `super(...)` 之后的语句：`rate = r;`。
5. 构造完成，`new` 把引用交回来。

一句话概括：**构造器链从最顶上的父类一路往下，每层先把自己那一层安置好**。子类构造器里 `super(...)` 后面的语句，一定在父类构造器跑完之后才轮到 [2]、[5]。

有两条硬规则：

- `super(...)` 必须是子类构造器的**第一条语句**。你不写，编译器就自动补一句 `super()`，也就是调用父类的无参构造器 [2]。所以如果父类只写了带参构造器、没留无参的那个，而子类又没显式写 `super(...)`，程序编译不过——编译器补的那句找不到目标。
- 正因为父类构造器先跑，子类代码可以放心假设父类字段已经安顿好了。

有个副作用值得留意：如果父类构造器里调用了某个方法，而子类正好重写了它，那么第 3 步执行的已经是**子类的版本**——此时子类自己的字段还停在默认值 [5]。

## 重写：换掉从父类继承来的行为

`Account.describe()` 输出户名和余额。`SavingsAccount` 想让输出多一段利率，于是在自己文件里再写一个同名的：

```java
@Override
public String describe() {
    return super.describe() + "，年利率 " + rate;
}
```

这就叫**重写**（override）：子类写一个和父类**签名相同**的实例方法——名字一样，参数个数和类型一样，把父类那份实现替换掉。之后通过 `SavingsAccount` 对象调 `describe()`，走的是子类这一版 [3]。

判定条件要说准：

- 名字和参数列表必须完全一致，返回类型也必须一样（或是它的子类型）。名字一样但参数不同，那不是重写，而是另一个新方法。
- 访问权限不能收窄。父类是 `public`，子类就不能缩成 `private`。
- 不能把实例方法改成 `static`，反过来也不行，都会编译错误。
- 加上 `@Override` 是好习惯：它让编译器替你核对，你以为在重写、其实参数写岔了，它会当场报错 [3]。

子类里想复用父类那份实现，写 `super.describe()`。`super` 表示“父类那一层”，`this` 表示“当前这个对象”；`super` 不能单独拿来当对象用。

## 编译时看左边，运行时看右边

```java
Account a = new SavingsAccount("张三", 1000, 0.03);
System.out.println(a.describe());
```

变量 `a` 的**声明类型**是 `Account`，但它指向的对象是 `SavingsAccount`。这行会打印**子类**那一版的输出。

为什么？编译时，编译器只检查 `a` 的类型 `Account` 里有没有 `describe()`——有，通过。至于运行时真正执行哪一份实现，JVM 不看变量的声明类型，而是看 `a` 指向的那个对象**实际是什么类**，Oracle 把这个行为叫做 virtual method invocation [4]。这也是上一篇“变量里装的是门牌号”的直接延伸：门牌号指向什么房子，就执行什么房子的方法。

由此得出一条判断：要把子类对象当父类用，父类里必须声明过那个方法。子类新增的方法（比如 `getRate()`）通过声明为 `Account` 的变量是调不到的——编译器只认声明类型上有的东西。

## 小结

- `extends` 让子类获得父类非 private 的字段和方法；每个子类对象各带一份父类字段。
- private 成员不被继承：子类不能按名字直接访问，但仍可经由继承来的公开方法间接读写。
- 构造器不被继承。`new` 子类对象时，构造器链从最顶层父类往下依次执行，`super(...)` 必须是子类构造器的第一句；不写则自动补 `super()`。
- 重写要求签名相同、返回类型兼容、访问权限不收窄，`@Override` 让编译器把关；调用哪个版本由运行时对象的实际类型决定。

## 术语表

- 继承（inheritance）：用 extends 让子类自动获得父类非 private 的字段和方法，避免把共同代码抄两份。
- 父类与子类（superclass / subclass）：被继承的一方叫父类，继承的一方叫子类；每个类只有一个直接父类。
- private 成员与继承：private 修饰的字段或方法只有本类内部能访问，因此不被子类继承，但仍可通过父类的公开方法间接使用。
- 构造器链（constructor chaining）：new 子类对象时，从最顶层父类开始逐层向下执行各层构造器，子类构造器的其余语句在父类构造器之后运行。
- super(...)：写在子类构造器第一句，用来调用父类构造器；不写则编译器自动补无参的 super()。
- 重写（override）：子类写一个签名相同的实例方法替换父类实现；@Override 让编译器核对是否真的重写成功。
- super.方法()：在子类里调用父类那一层的实现，用来在重写时复用父类逻辑。
- virtual method invocation：运行时按对象的实际类型决定执行哪个方法版本，而非按变量的声明类型。

## 来源

1. [Oracle Java Tutorials: Inheritance — 子类继承哪些成员、private 成员不被继承、构造器不是成员](https://docs.oracle.com/javase/tutorial/java/IandI/subclasses.html)
2. [Oracle Java Tutorials: Using the Keyword super — super(...) 必须是子类构造器第一句，以及构造器链](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
3. [Oracle Java Tutorials: Overriding and Hiding Methods — 重写的判定条件、@Override 与访问权限限制](https://docs.oracle.com/javase/tutorial/java/IandI/override.html)
4. [Oracle Java Tutorials: Polymorphism — virtual method invocation：JVM 按对象实际类型调用方法](https://docs.oracle.com/javase/tutorial/java/IandI/polymorphism.html)
5. [JEP 513: Flexible Constructor Bodies — 构造器自上而下执行，父类构造器调用被重写方法时子类字段仍为默认值](https://openjdk.org/jeps/513)

---

原文：https://pangzhengboyin.com/articles/java-inheritance-extends-constructor-order-override-5fcb58ab

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
