多态接口设计须遵循里氏替换原则(lsp),核心是行为可靠可预期,而非仅编译通过;接口需明确定义行为契约(输入范围、输出语义、异常等),实现类不得削弱前置条件、不得加强后置条件,避免语义冲突接口组合,推荐泛型限定或聚合接口提升类型安全与可替换性。

多态接口设计遵循里氏替换原则(LSP),核心不是“能不能编译通过”,而是“用接口的地方换实现类后,行为是否依然可靠、可预期”。关键在于接口契约的严谨性与实现类对契约的忠实履行。
接口定义必须明确行为契约,而非仅方法签名
接口不只是声明方法名和参数,更要隐含或显式约定:输入范围、输出语义、异常类型、线程安全性、null处理、副作用等。例如:
- 若接口方法声明
List<user> findAll()</user>,契约应明确“返回不可变空列表而非 null”——所有实现类都必须遵守,不能有的返回 null、有的抛 NPE; - 若接口有
void save(User user),契约需说明“user 为 null 时抛 IllegalArgumentException”,则任何实现都不能静默忽略或改为返回 false; - 避免在接口中定义模棱两可的方法,如
process()——没有前置/后置条件约束,各实现自由发挥,必然导致 LSP 失效。
实现类不得削弱前置条件,不得加强后置条件
LSP 要求子类型(即接口实现类)的行为范围不窄于父类型(接口)。具体到接口实现:
- 前置条件(输入约束)只能放宽:比如接口契约是“id > 0”,实现类可以接受 id ≥ 0,但不能要求 id > 100;
- 后置条件(输出保证)只能增强:比如接口承诺“返回非 null 列表”,实现类可返回不可修改列表(更强保证),但不能有时返回 null;
- 异常策略必须兼容:接口方法未声明受检异常,实现类不得抛出新的受检异常;已声明的异常,子类可抛更具体的子类,但不能抛更宽泛的异常(如接口声明 IOException,实现类改抛 RuntimeException 就破坏契约)。
避免让一个类实现多个语义冲突的接口
当类同时实现 Runnable 和 Closeable 没问题,因为二者职责正交;但若强行让 Ostrich 同时实现 Flyable 和 Walkable,而 Flyable.fly() 的契约是“升空并移动”,Ostrich 却只能抛 UnsupportedOperationException,这就违背 LSP——调用方按契约使用 Flyable 时会崩溃。
- 解决办法是拆分接口:只让真正能飞的类实现
Flyable; - 或采用组合:把飞行能力抽成
FlightBehavior,由具体鸟类型注入,而非强制继承/实现; - 切忌用
instanceof或类型转换来绕过接口契约——那是设计缺陷的补丁,不是解决方案。
用泛型限定 + 接口组合提升类型安全与可替换性
当某个方法需要对象同时满足多种能力时,与其让类实现多个接口再强转,不如用泛型边界清晰表达需求:
- 错误写法:
void handle(Interface1 obj) { if (obj instanceof Interface2) ((Interface2)obj).doX(); }—— 破坏透明替换,调用方必须知道内部实现细节; - 推荐写法:
<t extends interface1 interface2> void handle(T obj)</t>—— 编译期确保传入对象天然支持两种行为,无需运行时判断,完全符合 LSP; - 也可定义聚合接口:
interface Processable extends Interface1, Interface2 {},让符合条件的类直接实现它,保持语义统一。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











