java集合无计算属性,需用装饰器模式封装派生计算(如sum),通过缓存或重写写操作保证一致性;高频重复计算且变更不频繁时适用,否则优先用stream。

Collection 本身没有“计算属性”这个概念——Java 集合接口和标准实现类(如 ArrayList、HashSet)不支持像 Kotlin 的 val size: Int get() = ... 或 Swift 的 computed property 那样声明式定义的动态只读属性。
你真正需要的,是**在不破坏集合封装的前提下,按需计算派生结果的轻量包装设计**。这不是语法糖,而是明确分离「数据持有」与「逻辑计算」的实践选择。
为什么不能直接给 ArrayList 加个 getSum() 方法?
因为 ArrayList 是通用容器,它不关心你存的是 Integer 还是 User;强行扩展会污染类型语义,也违背开闭原则。常见错误是写一个工具类静态方法:CollectionUtils.sumInt(list),看似简单,但每次调用都重复遍历、无缓存、无法感知数据变更。
如何安全封装一个带 sum/avg/firstNonNull 等计算能力的 List?
用装饰器模式(Decorator)包装原始集合,把计算逻辑收进新类型里:
- 继承
AbstractList或组合List,暴露原集合所有读操作(get()、size()等) - 将计算逻辑封装为 final 方法,例如
sum()内部调用stream().mapToInt(...).sum(),或对数字型集合做一次遍历缓存 - 如果底层集合可变,且你需要响应式更新(比如 add 后自动刷新 sum),就重写
add()、remove()等方法并同步更新内部状态
示例关键片段:
public class SummableList extends AbstractList<Integer> {
private final List<Integer> delegate;
private int cachedSum = -1; // -1 表示未计算
public SummableList(List<Integer> delegate) {
this.delegate = delegate;
}
@Override
public Integer get(int index) { return delegate.get(index); }
@Override
public int size() { return delegate.size(); }
public int sum() {
if (cachedSum == -1) {
cachedSum = delegate.stream().mapToInt(i -> i).sum();
}
return cachedSum;
}
}
什么时候该用 Stream 而不是包装类?
多数场景下,别自己造轮子——直接用 list.stream().map(...).filter(...).reduce(...) 更清晰、更安全、更易测试。包装类只在以下情况值得投入:
- 同一组计算被高频重复调用(如 UI 每帧刷新 sum),且原始集合变化不频繁 → 缓存有意义
- 需要统一约束行为(比如所有计算都必须加非空校验、单位转换、精度截断)→ 封装可复用逻辑
- 对外暴露的 API 必须是「集合 + 计算结果」一体化对象(如 DTO、ViewModel)→ 包装类天然符合契约
容易踩的坑:缓存失效与并发修改
如果你在包装类里缓存了 sum、max 等值,又允许外部直接操作原始 delegate(比如传入一个 ArrayList,别人还在往里 add()),那缓存永远不一致。解决办法只有两个:
- 构造时深拷贝原始集合(适合小数据、低频写)
- 完全接管所有写操作入口(即包装类自己实现
add()、set(),并在其中更新缓存)
多线程环境下,还要给缓存字段加 volatile,或用 AtomicInteger,否则读到脏值连 debug 都难定位。
stream()。










