1. 程式人生 > >HashSet add()方法原始碼詳解

HashSet add()方法原始碼詳解

HashSet實現了set介面,也是日常開發中比較常用的類,今天通過對HashSet add()方法原始碼的分析進一步加深對HashSet的理解。

首先先看下HashSet的建構函式,程式碼如下:

   /**
     * 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<>();
    }
   /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * the specified initial capacity and the specified load factor.
     *
     * @param      initialCapacity   the initial capacity of the hash map
     * @param      loadFactor        the load factor of the hash map
     * @throws     IllegalArgumentException if the initial capacity is less
     *             than zero, or if the load factor is nonpositive
     */
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
   /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * the specified initial capacity and default load factor (0.75).
     *
     * @param      initialCapacity   the initial capacity of the hash table
     * @throws     IllegalArgumentException if the initial capacity is less
     *             than zero
     */
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

這三個HashSet的建構函式說明HashSet本質上都是構建了一個HashMap,第一個建構函式僅僅是構建了一個簡單的HashMap,用了HashMap預設的初始容量(16)以及預設LOAD_FACTOR(0.75f);第二個建構函式構建HashMap時會指定HashMap的初始容量以及LOAD_FACTOR;第三個建構函式構建HashMap時會指定HashMap的初始容量。

注意:這裡初始容量應儘量設定為2的冪,這樣能夠保證鍵值對更加均勻地分佈在HashMap上。

接下來我們來看下本文的核心HashSet的add()方法,原始碼如下:

   /**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element <tt>e</tt> to this set if
     * this set contains no element <tt>e2</tt> such that
     * <tt>(e==null ? e2==null : e.equals(e2))</tt>.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns <tt>false</tt>.
     *
     * @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;
    }

這裡方法註釋的意思是新增指定的element如果該set集合不包含該element,如果該set集合已包含將要新增的element,則不對set進行任何操作並返回false;若該set集合不包含將要新增的element,則將該element新增至該set集合中並返回true。add()方法中的e即為新增的element而PRESENT則為一個虛擬的假值,不用在意。PRESENT的定義程式碼如下:

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

接下來來看下add()方法的具體實現程式碼,map.put()方法最終呼叫的是HashMap的putVal()方法其中HashSet新增的element 其實就是HashMap的key。原始碼及註釋如下:

   /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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) //HashMap底層儲存為 Node<K,V>[]結構若當前 Node<K,V>[]的length為0則resize
            n = (tab = resize()).length;//獲取resize後的連結串列length
        if ((p = tab[i = (n - 1) & hash]) == null)//這裡通過length及key的hash值計算element儲存的具體index若該節點無元素則直接存入
            tab[i] = newNode(hash, key, value, null);//新建新node並存入 Node<K,V>[]
        else {
            Node<K,V> e; K k;
            if (p.hash == hash && //若tab[i = (n - 1) & hash]上有元素且該元素的hash值與新增的key.hash相同則e=p
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)//若tab[i = (n - 1) & hash]上的元素呈樹形(紅黑樹)排布則將新增element儲存入該紅黑樹中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//儲存後返回新增的樹形節點
            else {//若tab[i = (n - 1) & hash]呈連結串列形式排布則找到末尾節點將新增元素存在末尾節點之後
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//將新增元素儲存在末尾
                        p.next = newNode(hash, key, value, null);//新增節點
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//若元素數量超過閾值則自動將連結串列轉為樹形結構(紅黑樹)
                            treeifyBin(tab, hash);//轉為樹形結構(紅黑樹)
                        break;
                    }
                    if (e.hash == hash &&//若連結串列中的某一節點hash和key與新增元素的key.hash以及key相同break
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value; //獲取element舊的value
                if (!onlyIfAbsent || oldValue == null) //這裡onlyIfAbsent為false
                    e.value = value;//更新value
                afterNodeAccess(e);//預留函式不用管
                return oldValue;//返回舊的value
            }
        }
        ++modCount;//HashMap修改次數+1
        if (++size > threshold)//size+1 若size大於閾值(LOAD_FACTOR * INITIAL_CAPACITY)則resize
            resize();//resize
        afterNodeInsertion(evict);//預留函式不用理會
        return null;//若為新節點則返回null
    }

通過分析HashMap的putVal()方法,我們可以知道事實上HashMap定位元素的方式都是通過key以及key對應的hash。在putVal()方法中,如果新增的key在HashMap中不存在,則會直接通過key的hash值以及HashMap的length進行&運算然後得到儲存的index並直接存入新的鍵值對同時返回null;若key已在HashMap中存在,則根據實際情況檢索並得到key對應的鍵值對,然後更新value並返回舊的value。

瞭解的HashMap的putVal()原理後,再回過頭來看HashSet的add()原始碼:

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

我們可以知道:當新增的element e (即新增HashMap中的key)不存在於HashMap中,那麼HashMap的putVal()方法會返回null,從而使add()方法返回true即新增成功;若新增的element e (即新增HashMap中的key)已存在於HashMap中,那麼HashMap的putVal()方法會返回舊的value即dummy value (new Object()),從而使add()方法返回false即新增失敗。

以上就是HashSet add()方法的解析,若有不正確的地方請大家指正,謝謝!