# 接口

Source: https://codewiki.com/zh/java/interfaces/

> - **what**: 接口（interface）是只描述公开行为的引用类型。类通过 `implements` 承诺这些行为，调用方则可依赖接口类型替换具体实现。
> - **trap**: `default` 并非笼统的兼容性保证；新增默认方法可能与别的接口冲突，也可能悄悄改变已有实现的行为。
> - **fix**: 保持接口小而明确，写清输入、失败与并发契约，并用完整重编译和替代实现测试每次接口变更。

## 是什么，为什么存在

Java 接口是一种不能直接实例化的引用类型。它为一组操作命名，却不要求所有实现共享同一父类或对象布局。类可以实现多个接口，接口也可以继承多个接口，因此行为契约不必绑在单继承的类层次上。

Java 采用名义类型（nominal typing）。一个类即使恰好拥有同名方法，也不会自动成为某个接口的实现；源码必须通过 `implements` 或继承关系明确建立类型关系。这样，方法签名相同只是巧合还是有意履行契约，不需要由调用方猜测。

接口类型带来子类型多态（subtype polymorphism）。例如，构造器接收 `ShippingRule` 时，可以得到固定费率、按重量计费或测试替身，而无需知道各自的类。运行时仍在实际对象上分派实例方法；接口引用不是包裹对象的新容器。

你会在集合、比较器、监听器、服务边界和依赖注入中遇到接口。它最适合表达调用方真正依赖的能力，尤其是多个不相关类都能履行的契约。若设计主要目的是共享实例状态、构造流程或受保护的辅助代码，抽象类通常更直接。

方法签名没有写出全部行为。空值规则、参数单位、返回值含义、异常、线程安全要求和副作用都属于契约。接口位于跨团队或公共库边界时，应把这些约束写进文档和契约测试。

## 工作原理

类用 `implements` 实现接口，接口用 `extends` 组合其他接口。非抽象类必须为继承到的抽象实例方法提供实现，除非某个更具体的默认方法已经满足要求。接口变量可以引用任何兼容实例，但只能直接调用其静态类型公开的成员。

接口没有构造器和实例字段。字段声明隐式为 `public static final`，因此保存的是接口级常量引用；若该引用指向可变集合，集合本身仍可修改。把可变对象放进接口字段会制造公开的全局状态，不会因为引用是 `final` 就变安全。

| 成员形式 | 是否有方法体 | 是否继承到实现类 | 典型用途 |
| --- | --- | --- | --- |
| 抽象实例方法 | 否 | 是 | 定义实现必须提供的行为 |
| `default` 方法 | 是 | 是 | 基于现有契约提供通用实例行为 |
| `static` 方法 | 是 | 否 | 与接口概念相关的工厂或辅助操作 |
| `private` 方法 | 是 | 否 | 复用接口内部的默认或静态实现 |
| 字段 | 初始化表达式 | 通过限定名称访问 | 真正常量，而不是可变状态 |

抽象方法和默认方法隐式为 `public`；它们不能是 `protected` 或包可见。静态方法可以是 `public` 或 `private`，私有实例方法只能由接口自己的实例方法调用。实现类不能降低所实现方法的可见性。

默认方法（default method）拥有实例方法体，可以调用其他实例方法和私有实例辅助方法。它没有自己的实例字段，所以其行为只能依赖参数、常量和接收对象通过契约暴露的状态。默认方法适合从已有原语推导便捷操作，不适合偷偷引入新的状态要求。

多个候选实例方法相遇时，编译器按下面的规则决定是否需要显式覆盖：

1. 类或父类中的具体实例方法优先于接口默认方法。
2. 一个接口比另一个更具体时，子接口声明的方法优先。
3. 两个无关接口提供冲突的默认方法时，实现类必须覆盖；覆盖体可用 `InterfaceName.super.method()` 选择某个直接父接口实现。

静态接口方法不参与这些规则，因为它不被子接口或实现类继承。应通过声明它的接口调用，例如 `Rule.parse()`，不能假设 `ConcreteRule.parse()` 存在。私有方法同样不被继承，也不能从实现类调用。

函数式接口（functional interface）只有一个抽象方法契约，也称单一抽象方法（single abstract method，SAM）接口。默认、静态和私有方法不计入这个数量，签名与 `Object` 的公开实例方法对应的方法也有专门的排除规则。`@FunctionalInterface` 不是必须的，但它会让编译器在契约不再满足时立即报错。

Lambda 表达式和方法引用需要目标类型；目标函数式接口决定参数与返回类型。这种从函数值到 SAM 接口实例的适配常称为SAM 转换（SAM conversion）。Lambda 只提供那个函数方法的实现，接口中的默认方法仍然可用。

## 示例

下面三个程序从替代实现开始，再处理默认方法冲突，最后构造函数式接口组合。输出由本地 OpenJDK 21.0.12 使用 `javac --release 21 -Xlint:all` 编译并执行得到；所用语法和接口规则在目标 Java 25 中保持有效。

### 用接口隔离调用方与实现

计费函数只接收 `ShippingRule`。两个记录类保存不同数据并实现同一操作，而默认方法根据实际类给出诊断名称。

<!-- quick -->

```java
// file: ShippingQuote.java
import java.util.List;

public class ShippingQuote {
    interface ShippingRule {
        int feeInCents(int weightGrams);

        default String label() {
            return getClass().getSimpleName();
        }
    }

    record FlatRate(int fee) implements ShippingRule {
        @Override
        public int feeInCents(int weightGrams) {
            requirePositive(weightGrams);
            return fee;
        }
    }

    record WeightRate(int centsPerKilogram) implements ShippingRule {
        @Override
        public int feeInCents(int weightGrams) {
            requirePositive(weightGrams);
            int kilograms = (weightGrams + 999) / 1_000;
            return kilograms * centsPerKilogram;
        }
    }

    static void requirePositive(int weightGrams) {
        if (weightGrams <= 0) {
            throw new IllegalArgumentException("weight must be positive");
        }
    }

    public static void main(String[] args) {
        var rules = List.<ShippingRule>of(new FlatRate(499), new WeightRate(300));
        for (ShippingRule rule : rules) {
            System.out.println(rule.label() + ": " + rule.feeInCents(1_200));
        }
    }
}
```

```text
FlatRate: 499
WeightRate: 600
```


<!-- /quick -->

`List.of(...)` 明确把两个不同记录视为同一接口类型。循环中的调用仍落到各自的 `feeInCents()` 实现，`label()` 则复用接口中的默认实现。调用方无需用 `instanceof` 分支识别每个计费类。

这个接口尚未约束费率必须非负，第三个试验会暴露该缺口。约束属于记录构造器还是规则方法要由领域契约决定；仅把实现藏在接口后面不会自动验证对象状态。

### 解决默认方法的优先级与冲突

`Child` 同时继承父类方法和接口默认方法，类方法获胜。`Transfer` 实现两个无关接口，必须显式覆盖冲突的方法。

```java
// file: DispatchRules.java
public class DispatchRules {
    static class Parent {
        public String source() {
            return "class";
        }
    }

    interface Named {
        default String source() {
            return "interface";
        }
    }

    static final class Child extends Parent implements Named {}

    interface Fast {
        default String mode() {
            return "fast";
        }
    }

    interface Safe {
        default String mode() {
            return "safe";
        }
    }

    static final class Transfer implements Fast, Safe {
        @Override
        public String mode() {
            return Fast.super.mode() + "+" + Safe.super.mode();
        }
    }

    public static void main(String[] args) {
        System.out.println(new Child().source());
        System.out.println(new Transfer().mode());
    }
}
```

```text
class
fast+safe
```

删除 `Transfer.mode()` 会产生编译错误，而不是在运行时随意选择一个默认实现。`Fast.super.mode()` 只能限定直接父接口，不能把任意祖先接口当作普通静态工具类调用。

`Child` 没有覆盖 `source()`，但父类提供的具体方法已经满足 `Named` 契约。父类甚至不必声明 `implements Named`；只要继承到的方法签名与可见性符合要求，子类声明就能建立接口关系。

### 组合函数式接口

`Rule` 只有 `test()` 一个抽象方法，所以 Lambda 可以实现它。默认方法 `and()` 返回另一个 `Rule`，静态方法提供一个有名称的基础规则。

```java
// file: ValidationPipeline.java
import java.util.Objects;

public class ValidationPipeline {
    @FunctionalInterface
    interface Rule<T> {
        boolean test(T value);

        default Rule<T> and(Rule<? super T> other) {
            Objects.requireNonNull(other);
            return value -> test(value) && other.test(value);
        }

        static Rule<String> notBlank() {
            return value -> value != null && !value.isBlank();
        }
    }

    public static void main(String[] args) {
        Rule<String> accountName = Rule.<String>notBlank()
                .and(value -> value.startsWith("acct-"));

        System.out.println(accountName.test("acct-alice"));
        System.out.println(accountName.test("   "));
        System.out.println(accountName.test("guest"));
    }
}
```

```text
true
false
false
```

`and()` 使用短路求值。空白字符串在第一条规则处失败，不会执行 `startsWith()`；`null` 也不会进入第二条规则。这项行为是组合器契约的一部分，应像参数类型一样接受测试。

给 `Rule` 增加第二个不等价的抽象方法会让 `@FunctionalInterface` 声明和所有 Lambda 目标一起失效。增加默认或静态方法不会改变函数描述符，但新默认方法仍可能与其他接口产生名称冲突。

### 把静态工厂与私有辅助方法留在接口中

静态工厂可以把简单实现藏在接口后面，默认方法则可复用私有辅助逻辑。两种方法都不会增加函数式接口的抽象方法数量。

```java
// file: InterfaceHelpers.java
import java.util.Locale;
import java.util.Objects;

public class InterfaceHelpers {
    @FunctionalInterface
    interface AccountId {
        String raw();

        default String normalized() {
            return normalize(raw());
        }

        static AccountId of(String raw) {
            Objects.requireNonNull(raw);
            return () -> raw;
        }

        private static String normalize(String raw) {
            return raw.strip().toLowerCase(Locale.ROOT);
        }
    }

    public static void main(String[] args) {
        AccountId account = AccountId.of(" ACCT-42 ");
        System.out.println(account.normalized());
        System.out.println(AccountId.of(" JOB-7 ").raw().strip());
    }
}
```

```text
acct-42
JOB-7
```

`of()` 必须通过 `AccountId` 调用，因为静态接口方法不被实现对象继承。`normalize()` 只能由接口内部调用，它允许默认方法共享实现，却没有扩大实现类或调用方可见的 API。

## 陷阱

### 把可变对象当作接口常量

> **陷阱:** 接口字段隐式为 `public static final`，但 `final` 只阻止重新赋值。`List EVENTS = new ArrayList<>()` 仍是一份可被所有实现和调用方修改的公开全局列表。

**修复方法：** 接口只暴露真正不可变的值，并优先把配置与状态交给实现对象的构造器。若常量只供接口内部实现使用，放入私有静态方法或单独的内部实现类，避免扩大公共 API。

### 设计调用方无法履行的大接口

> **陷阱:** 一个接口同时要求读取、写入、删除、批量导出和订阅事件，会迫使只读实现抛出 `UnsupportedOperationException`。类型声称操作可用，运行时却拒绝它，替代关系因此失真。

**修复方法：** 按调用方需要拆分能力，并应用接口隔离原则（interface segregation principle）。让方法参数依赖最小接口；确需成组能力时，再用子接口组合它们。

### 用默认方法隐藏失败策略

> **陷阱:** 生成代码常在默认方法里捕获 `Exception` 并返回 `null`、空集合或成功状态。所有实现虽然获得相同代码，却也被强加了可能错误的重试、日志或降级策略。

**修复方法：** 默认方法只组合已有操作，并保留原契约的失败语义。跨网络重试、授权失败与事务回滚应由拥有上下文的调用层决定；为正常缺失设计显式返回类型，不要吞掉意外异常。

### 假设默认方法自动向后兼容

> **陷阱:** 新增默认方法通常不要求旧实现立刻重编译，但它可能让实现类已有的同签名方法变成覆盖方法，也可能与另一个接口的新默认方法碰撞。二进制兼容、源码兼容和行为兼容不是同一件事。

**修复方法：** 用旧实现二进制运行兼容测试，再对全部实现与调用方做干净重编译。检查同名方法、返回类型、异常与副作用；公共接口变更需要发布说明和迁移路径。

### 把静态方法当作继承成员

> **陷阱:** `Parser.create()` 声明在接口中时，`JsonParser.create()` 不会因为 `implements Parser` 就自动可用。通过实现类型寻找该方法会编译失败，也容易让 API 所有权变得含糊。

**修复方法：** 始终用声明接口限定静态方法。若工厂必须根据实现类型变化，把创建行为放进独立工厂接口、构造器或注册表，不要试图用静态方法模拟多态分派。

<!-- deep -->

## 设计稳定的契约边界

接口应从调用方的需求出发，而不是从某个现有类的全部公开方法复制。若调用方只需要 `quote()`，就不要同时暴露缓存清理、连接关闭和诊断状态。较小的接口让替代实现更容易履行完整契约，也减少未来新增方法的压力。

有些代码不需要接口。只有一个实现、没有替换边界且不会跨模块使用的内部助手，直接使用具体类可能更清楚。为了测试而给每个类机械创建接口，往往只把构造细节搬到另一层，并没有形成稳定契约。

接口与抽象类解决的约束不同：

| 设计约束 | 接口 | 抽象类 |
| --- | --- | --- |
| 一个类承担多种不相关能力 | 可以实现多个 | 只能继承一个类 |
| 共享每个实例的字段与构造流程 | 不支持 | 支持 |
| 提供基于契约的通用方法 | 用 `default` | 用具体实例方法 |
| 限制实现集合 | 可用 `sealed` 接口 | 可用 `sealed` 抽象类 |
| 表达一个 Lambda 目标 | 函数式接口可以 | 不可以 |

默认方法应建立在接口已经承诺的抽象操作上。例如，`Collection.isEmpty()` 可以从 `size()` 推导，不需要知道实现字段。若默认行为需要新状态、锁、网络客户端或数据库事务，它已经超出接口自身能够拥有的上下文，通常应进入实现类或协调服务。

方法的行为契约需要覆盖有效输入，也要覆盖失败边界。若实现允许 `null`、阻塞调用、重复执行或并发调用，接口应明确说明。契约测试可以作为每个实现都要通过的测试套件，从而验证替换性，而不是只确认方法能编译。

接口类型也可以带泛型参数，例如 `Comparator` 或 `Repository`。类型参数应表达输入与输出间真实关系；通配符和擦除的细节属于泛型主题。这里更重要的是，替换实现必须对同一个参数化契约保持相同语义。

## 接口成员的边界规则

接口中的嵌套成员类和成员接口隐式为 `public static`。它们不携带某个接口实例，也不能访问所谓的接口实例字段，因为这种字段不存在。若辅助类型只是实现细节，把它公开嵌套在接口中会无意扩大 API。

接口可以重新声明 `equals(Object)`、`hashCode()` 或 `toString()` 为抽象方法，以表达文档意图，但不能为与 `Object` 的非私有实例方法覆盖等价的方法提供 `default` 实现。这个限制避免接口默认实现压过所有对象已有的类方法语义。

函数式接口的抽象方法数量不是简单数分号。继承而来的覆盖等价方法可能合并成一个函数描述符，而与 `Object` 公开方法对应的声明不会单独增加计数。`@FunctionalInterface` 把这套规则交给编译器检查，比人工数方法可靠。

默认方法冲突也不只发生在两个直接接口都写了相同方法时。子接口可以覆盖父接口默认方法，抽象声明也可能使继承关系重新要求实现。审查时应沿完整接口图收集覆盖等价签名，不能只搜索实现类的 `implements` 那一行。

`InterfaceName.super.method()` 是冲突解决语法，不是一般的父类型反射机制。限定名称必须指向允许的直接父接口，而且调用发生在实现类的实例方法体中。选择某一默认实现后，覆盖方法仍应维护最终公开接口承诺。

## 演进已经发布的接口

发布后的接口同时面对源码调用方、已经编译的实现类和运行中的混合版本。一次修改可以保持二进制链接，却让源码重编译失败，也可以两者都通过但改变结果。评审接口变更时，必须分别记录这三个维度。

新增抽象方法会让现有实现源码在下次编译时缺少实现。Java 语言规范把某些这类修改定义为对既有二进制兼容，但旧实现真正调用到新抽象方法时仍可能失败；「可以链接」不能替代端到端运行测试。

新增默认方法通常让旧实现继续运行，因为接口提供了方法体。然而，新方法可能与另一个接口形成冲突；Java 语言规范明确说明，某些混合二进制会在调用时抛出 `IncompatibleClassChangeError`。干净重编译还能发现运行旧二进制时看不到的源码歧义。

新增私有或静态方法不会要求实现类提供方法体，通常比扩大实例契约风险小。即便如此，公开静态方法仍扩大了 API，名字和行为会成为调用方依赖。删除成员、改变签名、收窄可见性或改变异常与副作用需要单独的兼容性分析。

接口字段的常量值还有编译期内联问题。调用方使用编译期常量时，旧二进制可能保留旧值，即使运行时加载了更新后的接口。不要把会变化的配置、协议版本或开关发布为接口常量，再期待只替换一个类文件就更新所有调用方。

函数式接口对演进尤其敏感。新增抽象方法会删除其 SAM 性质，使 Lambda 和方法引用无法重新编译；新增默认方法虽然不改变 SAM 数量，仍需检查名称冲突和行为变化。保留 `@FunctionalInterface` 能把第一类破坏变成接口声明处的明确错误。

公共接口变更应至少执行两组验证：所有源码对新接口做干净重编译，以及旧实现二进制在新接口旁运行。再用契约测试比较行为、异常和副作用。只跑当前模块的单元测试，无法覆盖外部实现者或混合部署。

<!-- /deep -->

[检查点: java/interfaces](https://codewiki.com/zh/java/interfaces/#checkpoint)

## 延伸阅读

- [Java 语言规范 25：接口成员](https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.2)
- [Java 语言规范 25：继承覆盖等价签名的方法](https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.4.1.3)
- [Java 语言规范 25：函数式接口](https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.8)
- [Java 语言规范 25：接口方法演进](https://docs.oracle.com/javase/specs/jls/se25/html/jls-13.html#jls-13.5.7)
- [Dev.java：定义接口](https://dev.java/learn/interfaces/defining-interfaces/)
