前言
類似於上一篇講的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
,但它還是保證了元素的唯一性。