接口定义必须聚焦契约而非实现细节,只声明表达业务意图的public abstract方法,避免工具方法、set操作、常量字段;命名用名词或动词短语;参数优先用不可变或接口类型;实现须遵循liskov替换原则,行为边界由javadoc明确约束;复用优先组合而非继承;泛型需警惕类型擦除,必要时显式传入class参数。

接口定义必须聚焦契约,而非实现细节
Java接口本质是类与类之间的协作协议,不是为了“多态”而多态,而是为了解耦调用方与实现方。接口中只应声明public abstract方法(Java 8+ 可含default和static方法),且每个方法名、参数、返回值都需明确表达业务意图。
常见错误是把接口写成“工具集合”,比如定义StringUtils类的静态方法塞进接口;或在接口里暴露setXXX这类破坏封装的操作。这会让实现类被迫承担不该管的责任。
- 接口命名用名词或动词短语,如
OrderProcessor、canRefund,避免IOrderService这种带前缀的冗余命名 - 方法参数尽量用不可变类型或接口类型,如
Collection extends Product>优于ArrayList<product></product> - 不要在接口中定义常量字段——那是
public static final的陷阱区,容易引发硬依赖
实现类需严格遵循Liskov替换原则
一个类实现接口后,任何依赖该接口的代码,不加修改就能安全使用这个实现类。违反这点最典型的场景是:实现类在process()里抛出UnsupportedOperationException,或对某个参数值做非接口声明范围内的强校验。
例如接口声明Optional<user> findById(Long id)</user>,实现类就不能在id 时直接<code>throw new IllegalArgumentException——因为接口没约定id必须为正,调用方无法预知此限制。
立即学习“Java免费学习笔记(深入)”;
- 所有
@Override方法的行为边界,必须被接口的 Javadoc 明确约束(比如“返回null表示未找到”,而非“可能抛异常”) - 如果某实现需要额外配置(如超时、重试),应通过构造函数或
Builder注入,而非在方法签名里加Duration timeout这种破坏契约的参数 -
default方法不能访问实现类的私有状态,否则会诱使后续实现类绕过统一逻辑
协作链中优先用组合而非继承来复用接口实现
当多个类需要共享同一组接口行为(比如都支持retry和log),不要让它们共同继承一个抽象基类,而应把共性逻辑封装成独立实现类,再通过字段组合进来。
例如:LoggingOrderProcessor和RetryingOrderProcessor都实现OrderProcessor,但各自职责单一;真实业务类WebOrderService持有一个LoggingOrderProcessor委托对象,再叠加RetryingOrderProcessor包装——这样协作关系清晰,也方便单元测试替换成MockOrderProcessor。
- 避免实现类之间互相持有对方引用(如
AImpl里 newBImpl()),这会造成隐式耦合和循环依赖风险 - Spring等框架中,优先用
@Autowired注入接口类型,而不是具体实现类名;若需多实现并存,用@Qualifier或@Primary显式标记 - 测试时,用
Mockito.mock(YourInterface.class)代替new YourImpl(),才能真正验证协作契约是否稳固
泛型接口要警惕类型擦除带来的运行时协作失效
Java泛型在编译后会被擦除,所以Repository<user></user>和Repository<order></order>在JVM里是同一个类型。这意味着你无法在运行时靠instanceof区分它们,也不能在switch中按泛型参数分发逻辑。
典型问题:想给不同实体类型注册不同的序列化器,结果全被当成Repository>处理,导致User数据被Order的格式器序列化。
- 泛型接口的方法签名中,若需运行时识别类型,请显式传入
Class<t></t>参数,如<t> T find(Class<t> type, String id)</t></t> - 不要在泛型接口的
default方法里写if (t instanceof User)——擦除后永远为false - Spring Data JPA 的
JpaRepository<t id></t>能工作,是因为它结合了反射+泛型声明位置(类声明处)+Bean工厂上下文,不是单靠接口泛型本身
save()在并发场景下是否保证原子性问清楚。










