CopyOnWriteArraySet.add()通过遍历底层数组并用equals()逐个比较判断重复,不依赖hashCode;若未重写equals(),则按引用比较,导致逻辑重复元素无法去重。

CopyOnWriteArraySet.add() 怎么判断重复元素
它不靠哈希,也不维护红黑树,而是每次 add() 时遍历底层数组,用 equals() 逐个比较。只要已有元素和待添加元素 equals() 返回 true,就跳过插入。
这意味着:去重行为完全取决于你元素类型的 equals()(和 hashCode() 无关,因为没哈希表);如果没重写 equals(),默认用 Object.equals(),也就是引用比较——两个新 new String("a") 会被视为不同元素。
- 常见错误现象:
CopyOnWriteArraySet里出现“看起来一样”的对象,比如多个new User("Alice") - 必须确保元素类正确重写了
equals()(且最好连带hashCode()一起重写,虽然这里不用,但避免后续误用) - 性能影响明显:集合越大,每次
add()的时间越接近 O(n),不适合高频写 + 大数据量场景
底层数组怎么做到“写时复制”又保持线程安全
每次修改(add()、remove())都会新建一个数组副本,在副本上操作完再原子替换掉旧数组引用。读操作(contains()、迭代器)始终面对某个快照,不加锁也不阻塞。
关键点在于:写操作的“复制-修改-替换”三步是原子的,但中间没有锁;读操作看到的永远是某次写完成后的完整数组,不会读到半截状态。
立即学习“Java免费学习笔记(深入)”;
- 迭代器是弱一致性的:它基于构造时的数组快照,所以遍历时看不到之后的新增,也看不到自己正在遍历的元素被其他线程删掉(不会抛
ConcurrentModificationException) - 不能用于需要强实时一致性的场景,比如“立刻看到最新成员列表”的管理后台
- 内存开销比普通
HashSet高:每次写都多一份数组内存,尤其元素大或写频繁时要注意 GC 压力
为什么不用 equals() 就会失效?看一个典型误用
假设你往 CopyOnWriteArraySet 里加了几个未重写 equals() 的 POJO:
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
// ...
Set<Point> set = new CopyOnWriteArraySet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2)); // 这个会被插入!因为 Object.equals() 比较的是引用
结果集合大小是 2,而不是预期的 1。
- 修复方式只有:在
Point中重写equals()和hashCode() - 注意 IDE 自动生成的
equals()要包含所有业务相关字段,别漏掉 - 如果元素类型是第三方类(如
LocalDateTime),确认它已正确定义equals()——好在 JDK 类基本都做了
和 CopyOnWriteArrayList 的去重逻辑有关系吗
没有。两者底层都是数组 + 写时复制,但 CopyOnWriteArrayList 根本不去重,add() 总是追加;而 CopyOnWriteArraySet 是在 add() 前先调用 contains() 判断是否已存在——而 contains() 正是那个遍历数组 + equals() 的逻辑。
换句话说:CopyOnWriteArraySet 是“套壳”实现,内部持有一个 CopyOnWriteArrayList,所有去重逻辑都在自己的 add() 方法里手动遍历检查。
- 所以别指望它支持
TreeSet那样的排序去重,也没法传Comparator - 也不能通过反射去改底层 list 来绕过去重——它的
list字段是私有的,且所有 public 方法都走自己的逻辑 - 如果真需要排序 + 线程安全 + 去重,得考虑
Collections.synchronizedSortedSet(new TreeSet(...)),但注意读写都需同步,性能模型完全不同
equals() 没生效;或者在大数据量下反复写入,发现 CPU 和内存悄悄涨上去了。










