# 接口：为什么 Java 除了继承还留了另一条路

从单继承的限制讲到“能做什么”的契约式设计：implements 与多实现怎么写，接口当类型用时编译和运行各看什么

> 接口与契约 · 多态与类型 · 继承 · 约 6 分钟 · 10 月 17 日

## 本篇要点

1. 继承表达“是一种”，一个类只能继承一个父类，因此无法给彼此无关的类（如 Account 与 Receipt）装同一种能力。
2. 接口只描述“能做什么”：只写方法签名，不写方法体，不带对象状态，也不能被 new。
3. 类用 implements 接下一个或多个接口，必须给出接口里每个方法的实现，签名要一致（包括 public），否则编译不过，除非类声明为 abstract。
4. 接口名可以当类型用：形参或变量写成 Reportable 时，可以接收任何实现类的对象，但调用时只能使用接口清单里列出的方法。
5. 运行时执行哪一版方法由对象实际类型决定，不看变量声明类型；这是重写机制在接口上的同一种表现。
6. 接口字段自动是 public static final（常量，不属于对象），接口也没有构造器——正因为接口不携带对象状态，Java 才允许一个类实现多个接口。
7. 继承与接口是配合关系：是一条继承链上共享字段和代码用 extends，跨继承链只共享能力用接口。
8. Java 8 起接口可以有带方法体的 default 方法，两个接口给的默认实现冲突时编译器会报错，要求类自己重写。

---

上一篇的 `Account` 和 `SavingsAccount` 是典型的“是一种”关系：储蓄账户本来就是账户，所以用 `extends` 共享余额逻辑很顺。现在换一个需求。

系统要把账户信息导出一行文本给报表用。同一份报表里还要放收据（`Receipt`）、对账单（`Statement`）。它们都要能被导出，但它们彼此不是一类东西——你不能让收据去继承账户，那在业务上是荒谬的。这一篇讲清楚：继承为什么在这里帮不上忙，接口是怎样补上这个缺口的，以及用接口类型的变量去操作不同实现类的对象时，究竟发生了什么。

## 继承卡在哪里

一个类只能 `extends` 一个父类。

```java
class Receipt extends Account, Voucher { }   // 编译错误
```

Java 为什么不让？假设允许同时继承两个父类，而这两个父类各自带着字段。新对象里就会出现两套来源不同的状态：同名字段谁先初始化、父类方法看到的到底是哪一个，都说不清。官方文档把这类麻烦叫“状态的多重继承”（multiple inheritance of state），并明确说这是 Java 不允许继承多个类的原因之一 [1]。

所以想让 `Receipt` 具备账户那种输出能力，“改它的父类”这条路走不通。更麻烦的是，能导出这件事跟继承链根本不对齐：账户、收据、对账单唯一的共同点只是“能被导出成一行字”，强行给它们找一个共同父类，等于逼它们共享一堆彼此无关的字段。

## 换个问法：不问“它是什么”，问“它能做什么”

继承描述的是身份（是不是一种），接口描述的是能力（能不能做）。`Account` 是账户，这是身份；`Account` 能被导成一行文本，这是能力。收据没有账户这个身份，但它具备这个能力。

接口（interface）的写法：

```java
public interface Reportable {
    String toReportLine();
}
```

和类的区别在于：这里只写了方法签名——叫什么名字、收什么参数、返回什么，最后用分号结束，没有花括号包起来的方法体。至于这件事具体怎么做，交给实现它的类去写。接口里的方法默认就是 `public`，通常不用把 `public` 写出来。接口也不能被 `new`：`new Reportable()` 编译不过，因为它只是一份“能做什么”的清单，本身不是一个完整的对象。

## `implements`：接下这份承诺

让一个类具备接口描述的能力，用 `implements`：

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

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

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

`Account implements Reportable` 读作“Account 实现 Reportable”。实现意味着接下承诺：接口列出的每个方法，这个类都得给出方法体。少实现一个，编译就不过，除非你把这个类声明成 `abstract`——那等于说“我还没兑现完，交给我的子类” [2]。方法签名也必须对上，包括 `public`：接口方法本来就是 `public`，实现时漏掉这个关键字，编译器会报“正在降低访问权限”。

`@Override` 不是必须写的，但写上以后编译器会帮你核对：这个方法是不是真的实现了接口里的某个方法，或者重写了父类的方法。拼错名字时它会立刻报错，值得养成习惯。

## 一个类可以有多个接口

```java
public class Receipt implements Reportable, Auditable { ... }
```

多个接口用逗号隔开。接口和继承也能同时用，`extends` 写在前面：

```java
public class SavingsAccount extends Account implements Auditable { ... }
```

这里正好接上上一篇的结论。`Account` 已经 `implements Reportable`，那么 `SavingsAccount extends Account` 之后，它自动也是 `Reportable`——因为 `toReportLine()` 的实现本来就跟着继承下来了。如果它嫌父类那版输出不够详细，在自己文件里再写一遍 `toReportLine()`，它仍然是 `Reportable`，只是运行时执行的是子类这版。

如果两个接口碰巧有同名同签名的方法，写一个实现就同时满足两者，不用写两遍。

## 接口是一种类型：统一操作不同对象

这是接口最有用的地方。接口名可以像类名一样当类型用：

```java
public class ReportPrinter {
    public static void print(Reportable item) {
        System.out.println(item.toReportLine());
    }
}
```

形参的类型是 `Reportable`，所以传进来的必须是实现了 `Reportable` 的对象：`Account` 对象、`SavingsAccount` 对象、`Receipt` 对象都行——哪怕它们分属两条完全无关的继承链。

编译器怎么检查这个方法体？只看 `Reportable` 这份清单。`item.toReportLine()` 合法，因为清单上有它。而 `item.getBalance()` 编译不过，因为参数类型是 `Reportable`，编译器并不知道传进来的对象其实是账户 [4]。

那运行时到底执行谁的 `toReportLine()`？看 `item` 变量指向的对象的实际类型，不看变量的声明类型 [5]。传 `Account` 对象就跑 `Account` 那版；传覆盖过这个方法的 `SavingsAccount` 对象，就跑子类那版。这和上一篇讲的重写是同一套机制，只是这次方法的“出处”是接口而不是父类。

这样写的好处立刻显现：`print` 只写一次，就能服务现在和以后所有能导出的类。以后新增一个能导出的类，`ReportPrinter` 一个字都不用改。标准库里大量能力就是这么组织的——`List` 是接口，`ArrayList` 和 `LinkedList` 都是它的实现，代码里用接口类型就能在两者之间自由替换。

用一张图看这个结构：

```mermaid
flowchart LR
  R["Reportable 接口<br/>toReportLine()"]
  A["Account<br/>（继承链上层）"] -->|"implements"| R
  S["SavingsAccount<br/>extends Account"] --> A
  C["Receipt<br/>（与账户无关的类）"] -->|"implements"| R
  P["ReportPrinter.print(Reportable item)"] --> R
```

## 边界和分工

接口既然这么好用，为什么不干脆用它取代继承？因为它不带对象状态。接口里写字段是允许的，但那个字段自动是 `public static final`——它是常量，不属于任何一个对象。接口也没有构造器。账户的余额、户名这类随对象变化的数据，它托管不了。

这也解释了 Java 为什么敢放开“一个类实现多个接口”：多个接口不会同时往同一个对象里塞各自的字段，前面说的状态冲突根本不会发生 [1]。类不能多继承，接口可以多实现，差别的根就在“带不带对象状态”这一点上。

选型时用一句话判断：

- 两个类之间是“是一种”，而且要共享字段和已有的代码 → 用 `extends`。
- 两个类之间只是“都具备某种能力”，彼此不在一条继承链上 → 抽一个接口，各自 `implements`。

还有一个边界值得知道：从 Java 8 起，接口里的方法可以带方法体，叫 default 方法，目的是让接口在不改动已有实现类的前提下新增方法。这等于把“实现的多重继承”又引了回来，于是两个接口可能给出同名的默认实现，编译器解决不了就报错，要求类自己重写一遍。这一层现在只需要知道“有这回事、看得懂编译器的报错”就够了。

检验自己有没有真的理解：拿到两个类，你能说出该用继承还是接口；能解释为什么 `Reportable r = new Account("张三", 1000);` 之后再写 `r.getBalance()` 会编译失败，而 `r.toReportLine()` 可以。

## 术语表

- 接口（interface）：一份只写方法签名、不写方法体、不带对象状态的“能力清单”，本身不能被 new。
- 契约：接口规定“有这个方法、参数和返回值是这样”，实现类负责兑现具体做法；编译器按契约检查。
- implements：类声明自己实现某个接口的关键字，后面可以跟多个接口名，用逗号隔开。
- 多实现：一个类可以同时实现多个接口（类只能继承一个父类），因为接口不携带每个对象各自的字段。
- 接口作为类型：把变量或形参声明为接口类型，就能接收任何实现该接口的类的对象；此时只能调用接口里出现过的方法。
- default 方法：Java 8 起接口里可以带方法体的方法，实现类不重写就沿用接口给的默认实现。

## 来源

1. [Oracle Java Tutorials: Multiple Inheritance of State, Implementation, and Type — 状态、实现与类型的多重继承，以及 Java 不允许继承多个类的原因](https://docs.oracle.com/javase/tutorial/java/IandI/multipleinheritance.html)
2. [Oracle Java Tutorials: Interfaces — 接口能包含什么，以及实现类必须为每个方法提供方法体](https://docs.oracle.com/javase/tutorial/java/IandI/createinterface.html)
3. [Oracle Java Tutorials: Implementing an Interface — implements 子句的写法与多个接口用逗号分隔的约定](https://docs.oracle.com/javase/tutorial/java/IandI/usinginterface.html)
4. [Oracle Java Tutorials: Using an Interface as a Type — 把接口当类型使用，变量可引用任何实现类的对象](https://docs.oracle.com/javase/tutorial/java/IandI/interfaceAsType.html)
5. [Oracle Java Tutorials: Polymorphism — 多态与虚方法调用：JVM 按对象实际类型决定执行哪个方法](https://docs.oracle.com/javase/tutorial/java/IandI/polymorphism.html)
6. [Oracle Java Tutorials: Abstract Methods and Classes — 未实现全部接口方法时类必须声明为 abstract](https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html)

---

原文：https://pangzhengboyin.com/articles/java-interface-vs-inheritance-contract-ad4463df

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