前言java
HashSet 的特色數組
類圖安全
源碼分析多線程
HashSet 如何保證元素的不重複併發
總結app
在工做中,常常有這樣的需求,須要判斷某個ID是否在某個組的管理之下等,就須要查詢該組下的ID放到一個集合中,且集合中元素不能有重複,以後判斷該集合是否包含咱們的目標ID;這時,咱們可使用 HashSet 來存放咱們的ID,HashSet能夠自動的幫助咱們去重,好比HashSet<String> set = new HashSet<>(list) 等。接下來看下 HashSet 的內部是怎麼實現的。源碼分析
從 HashSet 的 Javadoc 的說明中,能夠獲得如下信息:this
1. HashSet 底層是使用 HashMap 來保存元素的spa
2.它不保證集合中存放元素的順序,便是無序的,且這種順序可能會隨着時間的推移還會改變.net
3.容許 null 值,且只有一個
4.HashSet 不是線程安全的,底層的 HashMap 不是線程安全的,它天然就不是啦,可使用 Collections.synchronizedSet(new HashSet()) 來建立線程安全的 HashSet;synchronizedSet 方法底層會根據 Set 來建立 SynchronizedSet 對象,而 SynchronizedSet 類繼承了 SynchronizedCollection,在 SynchronizedCollection 中的全部方法都加上了 synchronized 關鍵字,因此它是線程安全的,能夠被多線程併發訪問。
5.集合中的元素不會重複
先來看看 HashSet 的一個類圖
從類圖中,能夠看到, HashSet 繼承了 AbstractSet 抽象類, 而 AbstractSet 又繼承了 AbstractCollection 抽象類,此外,HashSet 還實現了 Set 接口等。
AbstractSet 抽象類主要實現了兩個方法 equals 和 hashcode 方法,由於 HashSet 中沒有重複元素,就是根據這兩個方法來進行判斷的:
public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Set)) return false; Collection<?> c = (Collection<?>) o; if (c.size() != size()) return false; try { return containsAll(c); } catch (ClassCastException unused) { return false; } catch (NullPointerException unused) { return false; } } public int hashCode() { int h = 0; Iterator<E> i = iterator(); while (i.hasNext()) { E obj = i.next(); if (obj != null) h += obj.hashCode(); } return h; }
Set 接口,它是一個頂層接口,主要定義了一些公共的方法,如 add, isEmpty, size, remove, contains 等一些方法;HashSet, SortedSet,TreeSet 都實現了該接口。
接下來看下它的內部實現,它內部使用 HashMap 來存放元素,它的全部方法基本上都是調用 HashMap 的方法來實現的,相等於對HashMap包裝了一層,關於 HashMap 的實現,能夠參考 HashMap源碼分析-jdk1.6和jdk1.8的區別。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { // 用來存放元素的 HashMap private transient HashMap<E,Object> map; // 由於 HashMap 是存放鍵值對的,而 HashSet 只會存放其中的key,即當作 HashMap 的key, // 而value 就是這個 Object 對象了,HashMap 中全部元素的 value 都是它 private static final Object PRESENT = new Object(); // 無參構造,建立 HashMap 對象,初始大小爲 16 public HashSet() { map = new HashMap<>(); } public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } // 根據初始大小和加載因子建立 HashSet public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } // 根據初始大小建立 HashSet public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } // 迭代器 public Iterator<E> iterator() { return map.keySet().iterator(); } ........................ }
從上面聲明可看到,HashSet 底層是使用 HashMap 來存放元素的,且 HashMap 中全部元素的 value 都是同一個 Object 對象,且它被 final 修飾。
接下來看下它的方法實現:
// 返回集合中元素的個數 public int size() { return map.size(); } // 判斷集合是否爲空 public boolean isEmpty() { return map.isEmpty(); } // 集合中是否包含某個值,即判斷 HashMap 中是否包含該key public boolean contains(Object o) { return map.containsKey(o); } // 添加元素,在這裏能夠看到,添加元素的時候,會向 HashMap 中添加,且 HashMap中的value都是同一個 Object對象 public boolean add(E e) { return map.put(e, PRESENT)==null; } // 刪除元素 public boolean remove(Object o) { return map.remove(o)==PRESENT; } // 清楚集合 public void clear() { map.clear(); }
以上就是 HashSet 源碼的所有實現了,看着很簡單,可是要知道 HashMap 的實現過程纔會清楚。
接下來,看下 HashSet 的 add 方法,看下它是如何保證添加的元素不重複的
public boolean add(E e) { return map.put(e, PRESENT)==null; }
以後來看下 HashMap 的 put 方法:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
put 方法會調用 putVal 方法進行添加元素,來看下 putVal 方法的實現:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 若是 key 的 hashcode 在 Node 數組中不存在,即 集合中沒有改元素,則建立 Node 節點,加入到 Node 數組中,添加元素成功 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // 若是在 Node 數組中該 key ,則在 Node 數組該索引上的鏈表進行查找 Node<K,V> e; K k; // 鏈表上已經存在該元素 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 在該鏈表上找不到該key,則建立,插入到鏈表上 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 若是 key 已存在,則返回舊的值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); // 若是元素不存在,則返回 null return null; }
因此,在向 HashSet 添加元素的時候,若是要添加元素的 hashcode 已存在,且 equals 相等,則會替換掉舊的值。
關於 HashMap 的其餘方法實現,能夠參考 HashMap源碼分析-jdk1.6和jdk1.8的區別
以上就是 HashSet 的實現。看起來很簡單,可是前提是得知道 HashMap 的實現。
1. HashSet 底層是使用 HashMap 來保存元素的
2.它不保證集合中存放元素的順序,便是無序的,且這種順序可能會隨着時間的推移還會改變
3.容許 null 值,且只有一個
4.HashSet 不是線程安全的,底層的 HashMap 不是線程安全的,它天然就不是啦,可使用 Collections.synchronizedSet(new HashSet()) 來建立線程安全的 HashSet
5.集合中的元素不會重複