Java集合之HashSet、LinkedHashSet、TreeSet
討論集合關注的問題:
- 底層資料結構
- 增刪改查方式
- 初始容量,擴容方式,擴容時機
- 執行緒安全與否
- 是否允許空,是否允許重複,是否有序
1. 概述
前篇,我寫了關於Map系列的集合(點選跳轉);本篇重新回顧Collection三大類Set、List、Queue中的Set。
Set可以視作是數學中集合的概念,也即集合中不能有重複的元素。Set集合中的各種實現集合,其內部都與Map有關,先對Map有了解更好。常見的Set集合有HashSet、LinkedHashSet和TreeSet,下面通過原始碼試著分析其內部構造。
2. HashSet
HashSet繼承自AbstractSet,實現了Set介面,同時也是可克隆物件和進行序列化的。其內部的資料儲存區通過一個transient修飾的HashMap
維護,也就是說HashSet中的資料是存放在HashMap中(回憶:HashMap中是通過一個transient的陣列來儲存不同的Hash值的key,相同的Key鏈成一個連結串列)。進行序列化時,不會序列化空的值。它維持它自己的內部排序,所以隨機訪問沒有任何意義。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L; private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); /** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>(); }
HashSet進行構造時,除了可以使用Collection進行構造外,基本都呼叫了HashMap的建構函式完成。主要的引數是基礎容量為16個單位,載入因子是0.75。
HashSet基於HashMap,所以其對資料的訪問基本都是用HashMap的方法,包括獲取size、載入迭代器等。我們知道存入Set中的資料本身是無序的,維護訪問順序沒有意義。
public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean contains(Object o) { return map.containsKey(o); } /** * Returns an iterator over the elements in this set. The elements * are returned in no particular order. * * @return an Iterator over the elements in this set * @see ConcurrentModificationException */ public Iterator<E> iterator() { return map.keySet().iterator(); }
新增元素時,HashSet會呼叫HashMap的add方法,但在操作時做了一點細微的處理。HashSet類中維護了一個final的空物件Object,每次加入集合中的資料,其實是儲存在Map中的key
中,而map中的Value
都是這個物件。所以相同元素插入時,此時會發生value的替換,因為所有entry的value一樣,所以和沒有插入時一樣的。HashSet在新增元素或移除時,同樣會run一遍HashMap中那些操作(查詢、成鏈等),只是內容變化不大。
方式:hash(key的hash碼處理得到)———(相同與否)———key(相同與否)————替換value或鏈成表
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
3. LinkedHashSet
LinkedHashSet是HashSet的一個“擴充套件版本”,HashSet並不管什麼順序,不同的是LinkedHashSet會維護“插入順序”。HashSet內部使用HashMap物件來儲存它的元素,而LinkedHashSet內部使用LinkedHashMap物件來儲存和處理它的元素。
LinkedHashSet直接繼承自HashSet,能夠維護基礎的有序性。
LinkedHashSet使用LinkedHashMap物件來儲存它的元素,插入到LinkedHashSet中的元素實際上是被當作LinkedHashMap的鍵儲存起來的。LinkedHashMap的每一個鍵值對都是通過內部的靜態類Entry。
其內部只有幾個簡單的建構函式,使用了父類的一個比較特殊的建構函式:
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
-----------------------------------------------------------------------
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
4. TreeSet
TreeSet與TreeMap實現類似。
TreeMap是一個有序的二叉樹,TreeSet同樣也是一個有序的,它的作用是提供有序的Set集合。TreeSet繼承自AbstractSet,實現了Set介面、Cloneable和Serializable、NavigableSet介面。其內部主要是通過一個NavigableMap的map維護資料儲存。
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a set backed by the specified navigable map.
*/
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet() {
this(new TreeMap<E,Object>());
}
//構造一個包含指定 collection 元素的新 TreeSet,它按照其元素的自然順序進行排序。
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
//構造一個新的空 TreeSet,它根據指定比較器進行排序。
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
//構造一個與指定有序 set 具有相同對映關係和相同排序的新 TreeSet。
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
NavigableSet是 SortedSet的子類,具有了為給定搜尋目標報告最接近匹配項的導航方法,這就意味著它支援一系列的導航方法——比如查詢與指定目標最匹配項。
TreeSet內部通過維護了一顆樹,來保證資料的有序性。
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
//空樹時,判斷節點是否為空
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//非空樹,根據傳入比較器進行節點的插入位置查詢
if (cpr != null) {
do {
parent = t;
//節點比根節點小,則找左子樹,否則找右子樹
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
//如果key的比較返回值相等,直接更新值(一般compareto相等時equals方法也相等)
else
return t.setValue(value);
} while (t != null);
}
else {
//如果沒有傳入比較器,則按照自然排序
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//查詢的節點為空,直接插入,預設為紅節點
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//插入後進行紅黑樹調整
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
5. 小結
HashSet/TreeSet和LinkedHashSet,其內部都是基於Map來實現的。TreeSet和LinkedHashSet分別使用了TreeMap和LinkedHashMap來控制訪問資料的有序性。
三者都屬於Set的範疇,都是沒有重複元素的集合。基於HashMap和TreeMap,所以都是非執行緒安全的。