Java Interface
interface 用来定义类型之间的协作契约:调用方只依赖“能做什么”,实现类决定“如何完成”。它是 Java 多态、依赖倒置和函数式编程的基础。接口不是用来承载对象状态的轻量 class;一个接口应当表达一项稳定、可替换的能力。
接口的基本结构
接口可声明抽象方法、默认方法、静态方法、私有方法和常量。不同成员的默认修饰符不同:抽象方法是 public abstract,字段是 public static final。因此字段必须在声明时初始化,且不应保存可变状态。
1 | |
findById 和 save 是契约,具体实现必须提供它们。getRequired 是默认方法,给全部实现类一个可复用的派生行为。inMemory 是静态工厂方法,只能通过接口名调用,不属于实例的多态方法。
接口中的常量通常是设计异味。int TIMEOUT = 30; 实际等价于 public static final int TIMEOUT = 30;;它会把配置和命名空间暴露给所有实现类。应优先使用参数、配置对象或专门的常量类。
实现接口与面向接口编程
类使用 implements 实现接口;一个类可实现多个接口。变量、方法参数和返回值应尽可能使用接口类型,调用方因此不需要知道底层是内存实现、数据库实现还是远程实现。
1 | |
这里 UserService 的依赖方向是稳定的:它依赖 UserRepository 抽象,而不是 HashMap、JPA 或 HTTP 客户端。替换实现时,业务服务不需要修改。接口应由使用方的需求塑形,而不是为了给每个 class 都配一个“I 前缀接口”。只有存在真实的多实现、边界隔离或独立演进需求时,才引入接口。
默认方法、静态方法与私有方法
Java 8 的 default 方法允许接口在新增行为时保留已有实现类的二进制兼容性。Java 9 的 private 方法可提取多个默认方法共享的实现细节;它不被实现类继承或覆盖。
1 | |
默认方法适合由已有抽象方法严格推导出的便利行为,例如 getRequired、组合操作和向后兼容的扩展。它不适合塞入依赖数据库、网络、时钟或可变全局状态的业务流程:这类行为难以由实现类控制,也会让接口同时承担契约和基础设施职责。
静态方法适合与接口概念紧密相关的工厂或组合器。JDK 的 Comparator.comparing(...)、Predicate.not(...) 是典型例子。若静态方法只是无关工具函数,应放入具体领域类型,而不是把接口变成工具箱。
多继承与默认方法冲突
接口支持类型的多继承,但 class 只有单继承。多个接口出现同名默认方法时,Java 不会猜测应该调用哪一个:实现类必须显式覆盖。子接口的默认方法优先于父接口的默认方法;class 中已有的方法又优先于接口默认方法。
1 | |
这种限制避免了传统多重继承的状态与方法歧义。若两个接口的默认方法表达不同语义,不应靠“选择一个”掩盖冲突;应让实现类定义自己的领域行为,或拆分接口。
接口继承与泛型
接口可以 extends 多个接口。子接口既能增加能力,也能收窄返回类型。泛型接口把类型约束放入契约,能在编译期阻止错误组合。
1 | |
? super T 表示下一个映射器可以接收 T 或其父类型,? extends R 表示它产生 R 或其子类型。这个签名保留了组合的类型安全;调用方无需转换类型。
函数式接口与 Lambda
函数式接口(functional interface)只有一个抽象方法,因此可作为 lambda 表达式或方法引用的目标类型。default、static、private 方法不计入抽象方法数量;从 Object 继承的方法也不计入,例如 equals(Object)。
1 | |
@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 | |
Limits.MAX_RETRIES 在使用方的 class 文件里被替换成字面量 3。只重新编译接口而不重新编译使用方时,使用方仍持有旧值。
调用在字节码层面与类方法分开。通过接口变量调用实例方法生成 invokeinterface,调用接口静态方法生成 invokestatic;实现类自身的多态调用仍是 invokevirtual:
1 | |
运行时分派由 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 | |
编译器知道 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 函数式接口。 - 面向外部实现者发布接口前,先确认未来扩展方式;公开接口一旦被实现,就很难随意重塑。
接口的价值不在于减少几行代码,而在于把依赖方向固定在稳定的行为契约上。契约小、语义一致、演进路径明确时,实现可以替换,调用方也能保持简单。






