
为什么不该自己写不可变集合类
Java 9+ 已经提供了 Set.of()、List.copyOf()、Map.ofEntries() 等原生不可变集合工厂方法,它们底层是高度优化的私有实现,内存紧凑、线程安全、无反射开销。自己封装一个“不可变”集合,大概率会漏掉关键防护点,比如:未冻结内部数组、未防御性拷贝构造参数、未重写 stream() 返回可变流、未拦截 toArray() 后的修改。真要包装,优先用 Collections.unmodifiableList() 这类标准工具——它虽不完美(如底层数组仍可能被改),但至少覆盖了常见接口契约。
如果非得手写包装类:必须拦截的 4 个操作
以包装 ArrayList 为例,仅靠重写 add()、remove() 不够,这些方法在继承链中可能被绕过。真正要堵死的入口是:
-
set(int index, E element):直接篡改元素,比add()更隐蔽 -
clear():清空整个结构,常被忽略 -
retainAll(Collection)和removeAll(Collection):批量修改,容易漏覆写 -
listIterator()返回的ListIterator实例:它自带set()和add()方法,必须返回一个自定义的只读迭代器
示例:重写 set() 时不能只抛 UnsupportedOperationException,还要检查索引是否越界——否则异常类型和原生集合不一致,破坏契约。
防御性拷贝的坑:构造时 vs. getter 时
用户传入一个 ArrayList 构造你的不可变类,你必须在构造函数里做深拷贝(或至少 new ArrayList(input)),而不是存引用。但反过来,当用户调用你的 getItems() 方法时,绝不能返回内部列表引用——哪怕你用了 Collections.unmodifiableList() 包裹,因为该包装只是视图,底层数组一旦被原始持有者修改,你的“不可变”就失效了。
立即学习“Java免费学习笔记(深入)”;
正确做法:
- 构造时:用
new ArrayList(source)或List.copyOf(source)(Java 10+)复制数据 - getter 时:每次调用都返回新包装视图,例如
Collections.unmodifiableList(new ArrayList(internalList));或者更轻量地返回List.copyOf(internalList)
注意:List.copyOf() 要求输入非 null 且不含 null 元素,否则抛 NullPointerException,这点和 unmodifiableList() 行为不同。
序列化与 equals/hashCode 的一致性陷阱
如果你的类实现了 Serializable,默认序列化会把内部可变集合一起存进去,反序列化后得到的是可变对象——你的“不可变”在 IO 边界就崩了。必须显式定义 writeObject() 和 readObject(),确保只序列化副本,并在 readObject() 中重新套上不可变包装。
另一个常见断层是 equals() 和 hashCode():如果直接复用内部集合的实现,而内部集合本身是可变的,那对象在放入 HashSet 后一旦被外部修改,哈希码就变了,导致查找失败。必须基于副本内容计算,且确保计算过程不依赖可变状态。
最省事的解法:把内部字段设为 private final List<e> items</e>,并在构造时完成拷贝和不可变包装,之后所有方法只读取 items,不暴露任何可变路径——这才是包装模式真正起效的临界点。










