Java中用组合代替继承实现委托,关键在于接口定义、字段封装和方法转发,委托类只暴露必要行为,需显式处理equals/hashCode/toString,泛型委托要注意类型擦除,且须厘清委托、代理与装饰器的职责边界。

Java里用组合代替继承做行为委托,关键在接口和字段设计
Java没有原生的委托语法(比如 Kotlin 的 by),但通过接口 + 成员字段 + 方法转发,就能干净实现委托模式。核心不是“怎么写”,而是“谁该暴露什么方法”——委托类只暴露被委托对象的**必要行为**,不泄露内部细节或冗余方法。
- 先定义清晰的接口(如
DataSource、Logger),这是委托契约的基础 - 委托者类(如
UserService)持有一个该接口的字段(private final DataSource dataSource) - 所有需要委托的方法,直接调用该字段对应方法,不做额外逻辑——有逻辑就说明不该委托,该继承或策略化
- 避免在委托方法里加空值检查或日志:那是代理(Proxy)或装饰器(Decorator)的事,不是委托
委托时绕不开的 equals/hashCode/toString 怎么处理
如果委托类重写了 equals 或 hashCode,又依赖被委托对象的实现,必须显式转发,否则默认是引用比较,会破坏逻辑一致性。JDK 本身不帮你做这件事。
public class UserService {
private final DataSource dataSource;
public UserService(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserService that = (UserService) o;
return Objects.equals(dataSource, that.dataSource); // 显式委托到 dataSource.equals()
}
@Override
public int hashCode() {
return Objects.hash(dataSource); // 同样委托
}
}
-
toString()同理:若需体现委托对象状态,应包含dataSource.toString(),而不是只写类名 - 不要盲目用 Lombok 的
@EqualsAndHashCode(of = "dataSource")——它生成的是字段值比较,但若dataSource本身没正确实现equals,结果仍错 - 如果委托对象是不可变且已正确定义了这些方法(如
String、UUID),可放心转发;否则得先确认它的契约
泛型委托类如何避免类型擦除导致的运行时问题
写一个通用委托容器(如 DelegatingList)时,构造时传入的 List 在运行时只剩 List,但多数场景下只要方法签名一致,不影响委托行为。真正出问题的是反射或序列化场景。
- 别在委托类里用
getClass().getGenericSuperclass()去提取T类型——擦除后拿不到,会得到Object - 如果必须保留类型信息(比如做 JSON 反序列化),构造时额外传入
TypeReference或- >
Class参数 - Spring 的
DelegatingFilterProxy就是典型泛型委托,但它靠 Bean 名称和上下文定位目标对象,不依赖泛型运行时信息
和代理(Proxy)、装饰器(Decorator)混用时,职责必须划清
委托(Delegation)只做「把请求转给另一个对象」,不改变行为语义;代理加控制逻辑(如权限、事务),装饰器增强行为(如缓存、日志)。三者代码结构相似,但意图不同,混用会导致维护困难。
- 用
java.lang.reflect.Proxy或 CGLIB 是动态代理,适合切面场景;委托是静态、编译期确定的 - 装饰器通常实现同一接口并持有被装饰对象,但会在方法前后加逻辑;委托则严格“原样转发”
- 一个类同时做委托 + 日志记录?那它已经不是委托者,是装饰器了——此时应拆成
LoggingUserService包裹UserService,后者再委托DataSource
委托真正的难点不在写法,而在于判断:这个行为,到底该由当前对象自己承担,还是交给别人?一旦委托关系过深(A→B→C→D),链路就难追踪,这时候就得考虑是否该用事件驱动或命令总线来解耦。










