interface 用来定义类型之间的协作契约:调用方只依赖“能做什么”,实现类决定“如何完成”。它是 Java 多态、依赖倒置和函数式编程的基础。接口不是用来承载对象状态的轻量 class;一个接口应当表达一项稳定、可替换的能力。

接口的基本结构

接口可声明抽象方法、默认方法、静态方法、私有方法和常量。不同成员的默认修饰符不同:抽象方法是 public abstract,字段是 public static final。因此字段必须在声明时初始化,且不应保存可变状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface UserRepository {
Optional<User> findById(UserId id);

void save(User user);

default User getRequired(UserId id) {
return findById(id).orElseThrow(() ->
new NoSuchElementException("user not found: " + id));
}

static UserRepository inMemory() {
return new InMemoryUserRepository();
}
}

findByIdsave 是契约,具体实现必须提供它们。getRequired 是默认方法,给全部实现类一个可复用的派生行为。inMemory 是静态工厂方法,只能通过接口名调用,不属于实例的多态方法。

接口中的常量通常是设计异味。int TIMEOUT = 30; 实际等价于 public static final int TIMEOUT = 30;;它会把配置和命名空间暴露给所有实现类。应优先使用参数、配置对象或专门的常量类。

实现接口与面向接口编程

类使用 implements 实现接口;一个类可实现多个接口。变量、方法参数和返回值应尽可能使用接口类型,调用方因此不需要知道底层是内存实现、数据库实现还是远程实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class InMemoryUserRepository
implements UserRepository {
private final Map<UserId, User> users = new HashMap<>();

@Override
public Optional<User> findById(UserId id) {
return Optional.ofNullable(users.get(id));
}

@Override
public void save(User user) {
users.put(user.id(), user);
}
}

public final class UserService {
private final UserRepository repository;

public UserService(UserRepository repository) {
this.repository = Objects.requireNonNull(repository);
}
}

这里 UserService 的依赖方向是稳定的:它依赖 UserRepository 抽象,而不是 HashMap、JPA 或 HTTP 客户端。替换实现时,业务服务不需要修改。接口应由使用方的需求塑形,而不是为了给每个 class 都配一个“I 前缀接口”。只有存在真实的多实现、边界隔离或独立演进需求时,才引入接口。

默认方法、静态方法与私有方法

Java 8 的 default 方法允许接口在新增行为时保留已有实现类的二进制兼容性。Java 9 的 private 方法可提取多个默认方法共享的实现细节;它不被实现类继承或覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface RetryPolicy {
boolean shouldRetry(int attempts, Throwable failure);

default Duration nextDelay(int attempts) {
int capped = Math.min(attempts, 6);
return Duration.ofSeconds(1L << capped);
}

default boolean canRetry(int attempts, Throwable failure) {
return isTransient(failure) && shouldRetry(attempts, failure);
}

private boolean isTransient(Throwable failure) {
return failure instanceof IOException;
}
}

默认方法适合由已有抽象方法严格推导出的便利行为,例如 getRequired、组合操作和向后兼容的扩展。它不适合塞入依赖数据库、网络、时钟或可变全局状态的业务流程:这类行为难以由实现类控制,也会让接口同时承担契约和基础设施职责。

静态方法适合与接口概念紧密相关的工厂或组合器。JDK 的 Comparator.comparing(...)Predicate.not(...) 是典型例子。若静态方法只是无关工具函数,应放入具体领域类型,而不是把接口变成工具箱。

多继承与默认方法冲突

接口支持类型的多继承,但 class 只有单继承。多个接口出现同名默认方法时,Java 不会猜测应该调用哪一个:实现类必须显式覆盖。子接口的默认方法优先于父接口的默认方法;class 中已有的方法又优先于接口默认方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Auditable {
default String label() {
return "audit";
}
}

interface Traceable {
default String label() {
return "trace";
}
}

final class Job implements Auditable, Traceable {
@Override
public String label() {
return Auditable.super.label() + "+" +
Traceable.super.label();
}
}

这种限制避免了传统多重继承的状态与方法歧义。若两个接口的默认方法表达不同语义,不应靠“选择一个”掩盖冲突;应让实现类定义自己的领域行为,或拆分接口。

接口继承与泛型

接口可以 extends 多个接口。子接口既能增加能力,也能收窄返回类型。泛型接口把类型约束放入契约,能在编译期阻止错误组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Mapper<S, T> {
T map(S source);

default <R> Mapper<S, R> andThen(
Mapper<? super T, ? extends R> next) {
Objects.requireNonNull(next);
return source -> next.map(map(source));
}
}

Mapper<String, Integer> parse = Integer::parseInt;
Mapper<Integer, String> format = Object::toString;
Mapper<String, String> normalize = parse.andThen(format);

? super T 表示下一个映射器可以接收 T 或其父类型,? extends R 表示它产生 R 或其子类型。这个签名保留了组合的类型安全;调用方无需转换类型。

函数式接口与 Lambda

函数式接口(functional interface)只有一个抽象方法,因此可作为 lambda 表达式或方法引用的目标类型。defaultstaticprivate 方法不计入抽象方法数量;从 Object 继承的方法也不计入,例如 equals(Object)

1
2
3
4
5
6
7
8
9
10
11
@FunctionalInterface
public interface PriceRule {
Money apply(Order order);

default PriceRule thenApply(UnaryOperator<Money> operation) {
Objects.requireNonNull(operation);
return order -> operation.apply(apply(order));
}
}

PriceRule vipDiscount = order -> order.total().multiply(0.9);

@FunctionalInterface 不是使用 lambda 的必要条件,但应当添加。它让设计意图明确,并在后来意外新增第二个抽象方法时让编译器报错。优先复用 JDK 的 FunctionPredicateConsumerSupplierComparator 等接口;只有领域名称本身能提高可读性,或需要额外默认行为时才定义新的函数式接口。

编译存储与运行时模型

接口编译为独立的 .class 文件:类访问标志是 ACC_INTERFACE | ACC_ABSTRACT,父类固定为 java.lang.Object,没有实例字段,也没有构造器 <init>;常量字段在类初始化方法 <clinit> 中赋值。抽象方法没有 Code 属性;defaultprivatestatic 方法的字节码保存在接口自己的 class 文件中,不会复制进实现类。实现类只在常量池和接口表里记录“实现了哪个接口”,方法体仍由接口的 class 文件提供。

需要注意常量的编译期内联。接口字段是编译期常量,javac 会把值直接写进使用方的字节码:

1
2
3
public interface Limits {
int MAX_RETRIES = 3;
}

Limits.MAX_RETRIES 在使用方的 class 文件里被替换成字面量 3。只重新编译接口而不重新编译使用方时,使用方仍持有旧值。

调用在字节码层面与类方法分开。通过接口变量调用实例方法生成 invokeinterface,调用接口静态方法生成 invokestatic;实现类自身的多态调用仍是 invokevirtual

1
2
3
4
repo.findById(id);
// invokeinterface #2, 2
// InterfaceMethod UserRepository.findById:
// (LUserId;)Ljava/util/Optional;

运行时分派由 JVM 完成。HotSpot 为每个实现类在 Metaspace 的 InstanceKlass 里维护 itable(interface method table):invokeinterface 先读取接收对象的实际类型,再经 itable 跳到目标方法。未被覆盖的 default 方法,itable 槽位直接指向接口 class 文件中的默认实现;多接口默认方法的冲突在类链接阶段就已选定,运行期不再判断。单态或双态调用点会生成内联缓存,并可能去虚化后内联;高度多态的调用点退化为 itable 查找,比类继承的 vtable 分派多一次间接跳转。

内存层面,接口自身没有运行时实例。UserRepository repo 只是一个普通对象引用(64 位 JVM 开启压缩指针时占 4 字节),指向堆上某个实现对象;对象头、实例字段和分派表都来自实现类,接口不增加任何实例开销。接口的 Klass 元数据与各实现类的 itable 随类加载进入 Metaspace,同一个类加载器只加载一份。

lambda 是另一条编译路径。PriceRule rule = order -> ...; 编译为一条 invokedynamic 指令,首次执行时由 LambdaMetafactory 动态生成实现该接口的隐藏类(JDK 15 之前是匿名内部类)。不捕获外部变量的 lambda 在调用点缓存单例,之后复用同一实例;捕获变量的 lambda 每次求值都新建一个小对象,逃逸分析可能将这个分配消除。生成类的元数据同样驻留 Metaspace,lambda 体通常会被编译器内联进宿主方法。

Sealed interface:封闭的实现集合

默认情况下,任意可访问的 class 都可实现公开接口。当一个类型的所有合法变体都应该被穷尽列出时,使用 sealed interface。它特别适合事件、表达式树和领域结果等代数数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public sealed interface PaymentResult
permits Paid, Rejected, Pending {}

public record Paid(String receiptId) implements PaymentResult {}

public record Rejected(String reason) implements PaymentResult {}

public record Pending(Instant retryAt) implements PaymentResult {}

String message = switch (result) {
case Paid paid -> "receipt: " + paid.receiptId();
case Rejected rejected -> "rejected: " + rejected.reason();
case Pending pending -> "retry at: " + pending.retryAt();
};

编译器知道 PaymentResult 的全部直接实现,因此 switch 不需要 default 分支。新增一种结果类型时,所有穷尽匹配都会在编译期暴露,避免遗漏处理。sealed 降低扩展性,适合领域边界已关闭的模型;插件 API 和第三方扩展点通常不应密封。

interface 与 abstract class 的取舍

两者都能抽象行为,但解决的问题不同。

需求选择原因
描述可由不相关类型实现的能力interface支持多实现和多继承类型
共享可变状态、构造过程或受保护实现abstract class可声明实例字段、构造器和 protected 成员
需要向已有实现增加派生行为defaultinterface可保留二进制兼容性
类型层次封闭且需要穷尽匹配sealed interface编译器可检查全部变体
只是复用一小段代码组合避免人为建立类型关系

不要因为接口“更抽象”就把共同实现搬进去。若多个实现共享数据库连接、缓存或模板步骤,抽象类可能合适;若它们只共享一段可替换策略,接口加组合通常更清晰。

演进接口时的边界

接口是公开契约,改动成本会扩散到每个实现者。新增抽象方法会导致所有源码实现编译失败;对外发布的库还可能让已有二进制实现出现 AbstractMethodError。新增默认方法能避免这类直接破坏,但仍要确认默认语义对每个旧实现都成立。

删除或改变既有方法的含义同样是破坏性修改。更稳妥的顺序是:先新增语义清晰的方法或独立接口,迁移调用方和实现类,再在下一个主版本删除废弃契约。不要用默认方法为错误的抽象续命。

设计检查清单

  • 接口名称表达能力或角色,例如 RepositoryClockPriceRule,而非实现细节。
  • 每个方法都应对所有实现有一致语义;无法统一的行为应拆分为更小的接口。
  • 参数和返回值使用最小必要抽象,例如返回 List 而非 ArrayList
  • 默认方法只能依赖接口自身的稳定契约,不隐藏 I/O、事务或共享可变状态。
  • 需要 lambda 时标注 @FunctionalInterface,并优先复用 JDK 函数式接口。
  • 面向外部实现者发布接口前,先确认未来扩展方式;公开接口一旦被实现,就很难随意重塑。

接口的价值不在于减少几行代码,而在于把依赖方向固定在稳定的行为契约上。契约小、语义一致、演进路径明确时,实现可以替换,调用方也能保持简单。