前言
类似于上一篇讲的CopyOnWriteArrayList,CopyOnWriteArraySet可以认为是一个写时复制的HashSet。
但CopyOnWriteArraySet
的底层实现完全依赖了CopyOnWriteArrayList
,它持有了一个CopyOnWriteArrayList
类型的成员,很多方法的实现都是直接调用CopyOnWriteArrayList
的同名方法。
这意味着CopyOnWriteArraySet
的底层实现不再是散列表的实现,而只是一个普通数组,只不过现在CopyOnWriteArraySet
表现地像一个HashSet而已,不过这对于使用者来说已经足够了。
与CopyOnWriteArrayList不同之处
CopyOnWriteArraySet
的底层实现完全依赖了CopyOnWriteArrayList
是可以的,但问题是CopyOnWriteArrayList
允许有重复元素,但CopyOnWriteArraySet
作为一个HashSet却不能有重复元素。
为了解决这一问题,CopyOnWriteArrayList
专门提供了addIfAbsent
和addAllAbsent
,以防止添加元素时会添加重复元素到里面去。
addIfAbsent
//CopyOnWriteArraySet
public boolean add(E e) {
return al.addIfAbsent(e);
}
//CopyOnWriteArrayList
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
//indexOf返回的不是-1,说明元素是present而不是absent。即元素存在,所以直接返回false
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
//如果元素不存在,则返回addIfAbsent的返回值
addIfAbsent(e, snapshot);
}
addIfAbsent,简单的说,就是只有这个元素不存在于set中时(adsent,缺席),才会加入该元素e
,从而防止重复元素加入。
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
//如果数组成员已经改变,需要考虑当前的数组成员会不会又拥有了参数e
int common = Math.min(snapshot.length, len);
//检查[0, snapshot.length)内的元素
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
//检查[snapshot.length, len)内的元素
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
addIfAbsent(E e, Object[] snapshot)
方法需要考虑数组成员已经改变,当前的数组成员会不会又重新拥有了参数e
。而if (snapshot != current)
分支内的代码就是用来执行这额外的检查,而这个检查的手法和CopyOnWriteArrayList的remove(Object o)
方法几乎一样,在此不赘述。重点在于,if (snapshot != current)
分支的目的,是为了检查currrent
中是否拥有参数e
,如果有则需要退出。
但这个手法在这里有所简化,还是分为两种情况:
- snapshot.length >= len
- snapshot.length < len
如果snapshot.length >= len
,for循环会检查到current的所有元素,并且此时common
就是len
,执行indexOf(e, current, common, len)
时会直接返回-1。
如果snapshot.length < len
,for循环只能检查到[0, snapshot.length)
范围内的元素,剩下[snapshot.length, len)
范围交给indexOf(e, current, common, len)
检查。
如果if (snapshot != current)
分支执行完毕都没有返回,则说明current
中确实没有参数e
。
addAllAbsent
//CopyOnWriteArraySet
public boolean addAll(Collection<? extends E> c) {
return al.addAllAbsent(c) > 0;
}
//CopyOnWriteArrayList
public int addAllAbsent(Collection<? extends E> c) {
Object[] cs = c.toArray();
if (cs.length == 0)
return 0;
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
int added = 0;
// uniquify and compact elements in cs
for (int i = 0; i < cs.length; ++i) {
Object e = cs[i];
//先检查e是否在elements中
if (indexOf(e, elements, 0, len) < 0 &&
//再检查e是否在待添加元素set中
indexOf(e, cs, 0, added) < 0)
//cs被遍历过的范围都可以作为待添加set使用
cs[added++] = e;
}
if (added > 0) {//如果需要添加元素
Object[] newElements = Arrays.copyOf(elements, len + added);
System.arraycopy(cs, 0, newElements, len, added);
setArray(newElements);
}
return added;
} finally {
lock.unlock();
}
}
The returned array will be “safe” in that no references to it are maintained by this collection. (In other words, this method must allocate a new array even if this collection is backed by an array). The caller is thus free to modify the returned array.
首先要知道Collection#toArray
这个方法是保证了返回一个集合的数组副本(见上面注释),也就是说我们可以随意操作cs
这个数组,而不用担心对参数c
造成影响。
函数的主要逻辑如上图所示,indexOf(e, elements, 0, len)
用来检查遍历元素e
是否在elements
中,indexOf(e, cs, 0, added)
用来检查e
是否在待添加元素set中。这样检查是为了cs
中有重复的元素。
注意,实际上没有生成新的set作为待添加元素set,而是直接重复利用cs
数组,因为每遍历过了当前元素后,这个元素的位置就可以随意占用了。
最后待添加元素set的范围就是cs
的[0, added-1]
,因为added是添加的元素个数。该函数返回的也是added。
总结
虽然CopyOnWriteArraySet
的底层实现完全依赖了CopyOnWriteArrayList
,但它还是保证了元素的唯一性。