组合与聚合是面向对象中“整体-部分”关系的两种建模方式:组合强调part随whole销毁(强生命周期依赖),聚合则part独立存在(弱依赖);关键依据是业务语义与所有权归属,而非代码写法。

Java 中对象的组合与聚合不是设计模式,而是面向对象关系建模的基本方式;组合强调“整体-部分”的强生命周期依赖,聚合则是弱依赖——关键看 part 对象是否随 whole 的销毁而销毁。
组合:成员变量直接 new 出来,且无外部引用
当一个类内部通过 new 创建另一个类的实例,并且不对外暴露该实例的引用(即没有 getter 返回可变对象),就构成组合关系。此时 part 的生命周期完全由 whole 控制。
常见错误现象:
- 外部能拿到 part 引用并修改其状态,破坏封装
- part 被多个 whole 共享,实际却是组合语义
实操建议:
- 在构造方法中初始化
part,避免延迟创建导致空指针 - 不提供返回可变
part的getPart()方法;如必须提供,返回副本(如new ArrayList(this.part))或不可变包装(Collections.unmodifiableList(this.part)) - 在
whole的finalize()或清理方法中显式释放part持有的资源(如线程、文件句柄)
public class Car {
private final Engine engine; // 组合:Car 一销毁,Engine 就失去唯一引用
public Car() {
this.engine = new Engine(); // 直接 new,无外部传入
}
// ❌ 错误:暴露可变内部对象
// public Engine getEngine() { return engine; }
// ✅ 安全:仅暴露只读能力
public void start() {
engine.ignite();
}}
立即学习“Java免费学习笔记(深入)”;
聚合:成员变量由外部传入,可被共享或复用
聚合表现为“has-a”,但 part 的生命周期独立于 whole。典型场景是依赖注入、工厂返回对象、或参数传入。
使用场景:
- 多个对象共用同一
Logger实例 -
Order持有由 Spring 管理的PaymentService - 测试时用 mock 对象替换真实依赖
参数差异:
- 组合:构造器内
new,无参数传递part - 聚合:构造器或 setter 接收已存在的
part引用(类型为接口更佳)
public class Department {
private final List employees; // 聚合:employees 可能被其他部门共享或单独管理
// 由外部传入,Department 不负责创建也不负责销毁
public Department(List employees) {
this.employees = Objects.requireNonNull(employees);
}
// ✅ 合理:Employee 生命周期不由 Department 控制
public void addEmployee(Employee e) {
employees.add(e);
}
}
立即学习“Java免费学习笔记(深入)”;
如何判断该用组合还是聚合?看销毁时机和所有权
核心依据不是代码写法,而是业务语义和对象所有权归属。JVM 不会自动帮你销毁聚合对象,也不会阻止你把组合对象传出去——这些都靠人来约束。
容易踩的坑:
- 误把数据库连接池里的
Connection 当作组合:它其实是聚合,因为连接要归还池子,不是 DAO 自己 close() 就完事
- 在组合类中返回
this 引用(如流式 API),导致外部持有了本该私有的 part,悄悄破坏了组合契约
- 用
final 修饰聚合字段,误以为“不可变”等于“组合”——final 只保证引用不变,不改变生命周期语义
性能影响:组合对象通常栈上分配少、堆上局部性强,GC 压力小;聚合若跨模块持有长生命周期对象(如静态缓存),易引发内存泄漏。
IDE 和 Lombok 不能掩盖语义错误
用 @Data 自动生成 getter/setter,或用 @Builder 快速构造,很容易无意中把组合写成聚合(比如加了 setEngine()),或者把聚合写死成组合(比如 @Builder.Default private final Logger logger = LoggerFactory.getLogger(...))。
实操建议:
- 组合字段一律用
private final + 构造器注入,禁用 setter
- 聚合字段也优先用
final,但允许构造器/Builder 参数传入
- 在类注释里明确写上
// @composition: Engine is owned by Car 或 // @aggregation: DataSource is shared across services
真正难的从来不是怎么写,而是团队对同一个类的生命周期预期是否一致——文档、评审、以及不滥用自动工具,比语法技巧更重要。










