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

基础语法

使用 interface 声明接口,class 使用 implements 兑现契约;接口继承接口时使用 extends。顶层接口可为 public 或包可见,不能声明为 final,因为它本身就是供其他类型实现的抽象契约。

接口成员的语法与含义如下:

成员语法与规则
抽象方法返回类型 方法(参数);,默认是 public abstract,无方法体。
默认方法default 返回类型 方法(参数) { ... },有方法体,实例可调用,子接口和实现类可覆盖。
静态方法static 返回类型 方法(参数) { ... },有方法体,只能通过接口名调用,不参与多态。
私有方法private 或 private static,Java 9 起可用,只供接口内的默认或静态方法复用。
字段默认是 public static final,必须初始化;不能声明实例字段。

接口不能声明构造器、实例初始化块或 protected 成员。抽象方法的实现必须保持 public 可见性,不能降低访问级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@FunctionalInterface
public interface TextFormatter {
int DEFAULT_WIDTH = 80;

String format(String text);

default String formatLine(String text) {
return normalize(format(text));
}

static TextFormatter plain() {
return text -> text;
}

private String normalize(String text) {
return text.trim();
}
}

public final class PlainFormatter implements TextFormatter {
@Override
public String format(String text) {
return text;
}
}

format 是唯一的抽象方法,所以 TextFormatter 可标注 @FunctionalInterface,plain 才能用 lambda 返回实现。formatLine 可通过接口变量调用;静态工厂只能通过声明它的接口名调用,常量也应通过声明它的接口名访问:

1
2
3
TextFormatter formatter = TextFormatter.plain();
String line = formatter.formatLine(" hello ");
int width = TextFormatter.DEFAULT_WIDTH;

实现类可以有构造器、实例字段和额外方法;它们都不是 TextFormatter 契约的一部分。调用方只需要格式化能力时,应保留 TextFormatter 类型,而不是依赖具体实现。

1
2
3
public interface CsvFormatter extends TextFormatter {
String formatRow(List<String> columns);
}

子接口会继承父接口的抽象方法和默认方法,并可继续增加契约。一个 class 可用逗号分隔实现多个接口,例如 class Report implements Printable, Exportable;它仍只能继承一个 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();
}
}

findById 和 save 是契约,具体实现必须提供它们。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();
}
}

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

泛型接口的用法

接口名后用尖括号声明类型参数,把“操作什么类型”写进契约本身。类型参数可以有多个,用逗号分隔,惯例命名为 T、R、K、V 这类单字母。声明变量时提供具体类型,编译器据此检查全部方法签名:

1
2
3
4
5
public interface Codec<T> {
T decode(String raw);

String encode(T value);
}

实现类有两种兑现方式。第一种是提供具体类型参数,抽象方法随之具体化,调用方拿到精确类型,不需要强转:

1
2
3
4
5
6
7
8
9
10
11
12
public final class MoneyCodec
implements Codec<Money> {
@Override
public Money decode(String raw) {
return Money.parse(raw);
}

@Override
public String encode(Money value) {
return value.toString();
}
}

第二种是保留类型参数,由实现类继续传递,必要时还可以引入自己的类型参数。由于类型擦除,运行时拿不到 T 的信息,需要具体类型时必须显式携带,例如把 Class<T> 存为字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class JsonCodec<T>
implements Codec<T> {
private final Class<T> type;

public JsonCodec(Class<T> type) {
this.type = type;
}

@Override
public T decode(String raw) {
return Json.read(raw, type);
}

@Override
public String encode(T value) {
return Json.write(value);
}
}

类型参数可用 extends 声明上界,让接口的签名和默认方法直接使用上界的成员:

1
2
3
4
5
6
7
public interface Ranked<T extends Comparable<? super T>> {
T rank();

default boolean outranks(T other) {
return rank().compareTo(other) > 0;
}
}

Comparable<? super T> 同时放宽了对实现类的要求:LocalDate 实现的是 Comparable<ChronoLocalDate>,因此 Ranked<LocalDate> 成立;若把上界写成 Comparable<T> 则编译失败。通配符在这里不是装饰,而是让契约覆盖更多合法类型。

接口的静态方法不能使用接口自身的类型参数——类型参数属于某个具体的实例化类型,而静态方法不属于任何实例。需要泛型的静态方法必须声明自己的类型参数:

1
2
3
static <T> Codec<T> json(Class<T> type) {
return new JsonCodec<>(type);
}

类型擦除决定了泛型接口的运行时形态。所有 Codec<Money> 与 Codec<User> 共用同一个原始接口 Codec,因此不能写 instanceof Codec<Money>(只能判断 Codec),不能在实现里 new T() 或 new T[],同一个类也不能同时实现 Codec<Money> 和 Codec<User>——擦除后二者是同一个接口。不带类型参数直接写 Codec 属于 raw type,编译器会放弃类型检查并发出警告,新代码不应出现。

接口继承与泛型

接口可以 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 表达式或方法引用的目标类型。default、static、private 方法不计入抽象方法数量;从 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 的 Function、Predicate、Consumer、Supplier、Comparator 等接口;只有领域名称本身能提高可读性,或需要额外默认行为时才定义新的函数式接口。

编译存储与运行时模型

接口编译为独立的 .class 文件:类访问标志是 ACC_INTERFACE | ACC_ABSTRACT,父类固定为 java.lang.Object,没有实例字段,也没有构造器 <init>;常量字段在类初始化方法 <clinit> 中赋值。抽象方法没有 Code 属性;default、private、static 方法的字节码保存在接口自己的 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 成员
需要向已有实现增加派生行为带 default 的 interface可保留二进制兼容性
类型层次封闭且需要穷尽匹配sealed interface编译器可检查全部变体
只是复用一小段代码组合避免人为建立类型关系

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

演进接口时的边界

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

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

设计检查清单

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

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