美文网首页
HashSet & HashMap源码

HashSet & HashMap源码

作者: 打工崽 | 来源:发表于2021-04-01 21:20 被阅读0次

    HashSet

    HashSet()构造器

    public HashSet() {
            map = new HashMap<>();
        }
    

    底层new了一个HashMap,再来看Set的add()方法

    add()方法

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

    PRESENT是静态共享的,就是下面的value

    调用map的put()方法,进入

    put()方法

    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    

    这里执行hash(key)方法,得到key对应的hash值,这里hash值不等于hashcode值

    进入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;
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                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) {
                            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;
                    }
                }
                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);
            return null;
        }
    

    开头定义了辅助变量如Node等,table是HashMap的一个数组,类型是Node[]

    第一个if语句处表示如果当前table是null,或者大小是0,就第一次扩容到16个空间。

    第二个if语句处根据key得到hash值,去计算该key应该存到table表的哪个索引位置,并把这个位置的对象赋给p。再判断p是否为null。

    1. 如果p为null,表示还没有存放元素,就创建一个Node,放在该位置tab[i]。Node中存放了hash值方便未来进行比较
    2. 如果p不为null,则进入else语句块中,还是先创建一个Node,进入else块中的第一个if语句。
      2.1 else块中的第一个if语句里,如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样,并且满足下面两个条件之一,就不能加入
      a. 准备加入的key和p指向的Node结点的key是同一个对象
      b. p指向的Node结点的key的equals()方法和准备加入的key比较后相同
      2.2 假如上面的if返回false,表示可以加入,则进入下面的else if,判断p是不是一颗红黑树,如果是红黑树,就调用putTreeVal来进行添加
      2.3 假如上面的else if也返回false,即判断不是红黑树,则进入else语句块。如果table对应索引值已经是一个链表,则for循环遍历链表,依次和链表的每一个元素比较,都不相同,则说明元素不重复,则加入到该链表的最后,同时判断是否达到8个节点,达到8个节点就调用treeifyBin()方法进行红黑树化(内部还有判断逻辑)。当然如果有相同的,则直接break

    最后一个if语句处判断当前已存放的数据量是否大于threshold,即阈值,超过则调用resize()方法

    紧跟着的afterNodeInsertion()方法是一个空方法,我们就不看了,这个方法是留给HashMap的子类实现,可以进行自定义的对链表的排序等操作。下面我们还是来看看resize()方法

    resize()方法

    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    

    大家观察上面代码有一串DEFAULT_INITIAL_CAPACITY,翻译一下就是默认初始化容量,我们点进去看,如下

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    

    果然是移位操作,1<<4 = 16,这个很简单,大家都能理解,所以默认长度为16

    还有一串DEFAULT_LOAD_FACTOR,翻一下就是默认加载因子,我们点进去看,如下

    static final float DEFAULT_LOAD_FACTOR = 0.75f
    

    果然,默认加载因子是0.75,具体原因是加载因子为0.75时根据泊松分布,每个碰撞位置的链表长度超过8的概率已经是一百万分之一,已经不太可能发生碰撞。又因为加载因子过大会加大发生碰撞几率,过小虽然减少了碰撞几率,但是增加了扩容次数,扩容会影响性能,所以定为0.75最合适。

    什么时候发生扩容呢?我们发现上面还有一串代码是DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR,即上面这两个数相乘,16 * 0.75 = 12,即到12的时候会发生扩容,为了防止数据量过大的时候无法及时扩容,所以进行一次预扩容

    总结

    image.png

    HashMap

    HashMap与HashSet实现的是同一个方法,具体表现的话Set可以去重而Map可以替换是因为putVal()方法里的这一段

    putVal()

    if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
    

    大家也看到注释里existing mapping for key,说明此时key是有value的,是一个map结构而不是set结构,这段代码里主要就是替换了value,由此造成HashSet和HashMap的表现是不同的

    JDK1.7的resize()方法

    void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
     
            Entry[] newTable = new Entry[newCapacity];
            transfer(newTable, initHashSeedAsNeeded(newCapacity));
            table = newTable;
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
        }
    

    如果原有table长度已经达到上限,就不再扩容,如果还没有达到上限,就创建一个新的table,调用transfer()方法

    transfer()方法

    //Transfers all entries from current table to newTable.
    
        void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry<K,V> e : table) {
                while(null != e) {
                    Entry<K,V> next = e.next;             
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    int i = indexFor(e.hash, newCapacity); 
                    e.next = newTable[i];                  
                    newTable[i] = e;                     
                    e = next;                              
                }
            }
        }
    

    transfer()方法的作用是把原table的Node放到新的table中,使用头插法,也就是说新table中链表的顺序和旧链表中是相反的,在HashMap线程不安全的情况下,头插法是有可能导致环状节点和死循环的,如下


    image.png

    JDK1.8的resize()方法

    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    

    在resize()方法中,定义了oldCap参数,记录了原table的长度,定义了newCap参数,记录新table的长度,新容量newCap是旧容量oldCap长度的2倍,同时阈值newThr也变为原来的2倍

    进入到下面的for循环,遍历原table,把原table中的每个链表中的每个新元素放入新table

    for循环里的第2个if语句判断e.next是否为空,判断当前链表是否只有一个元素

    1. 只有一个元素就放入新table
    2. 否则else中的注释preserve order翻译一下就是“维护秩序”,计算节点在table中的下标,认识一下定义的四个新变量
      2.1 loHead,下标不变情况下的链表头
      2.2 loTail,下标不变情况下的链表尾
      2.3 hiHead,下标改变情况下的链表头
      2.4 hiTail,下标改变情况下的链表尾

    其实这里体现了JDK1.8计算节点在新table中的思路。即正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。


    image.png

    我们可以看到hash值的每个二进制位用abcde来表示,那么,hash和新旧table按位与的结果,最后4位显然是相同的,唯一可能出现的区别就在第5位,也就是hash值的b所在的那一位,如果b所在的那一位是0,那么新table按位与的结果和旧table的结果就相同,反之如果b所在的那一位是1,则新table按位与的结果就比旧table的结果多了10000(二进制),而这个二进制10000就是旧table的长度16。

    换言之,hash值的新散列下标是不是需要加上旧table长度,只需要看看hash值第5位是不是1就行了,位运算的方法就是hash值和10000(也就是旧table长度)来按位与,其结果只可能是10000或者00000。

    所以,do-while里的(e.hash & oldCap),就是用于计算位置b到底是0还是1用的,只要其结果是0,则新散列下标就等于原散列下标,否则新散列坐标要在原散列坐标的基础上加上原table长度。

    (e.hash & oldCap) == 0,就是代表散列下标不变的情况,这种情况下代码只使用了loHead和loTail两个参数,由他们组成了一个链表,否则将使用hiHead和hiTail参数。其实(e.hash & oldCap) == 0和(e.hash & oldCap) != 0后的逻辑完全相同,只是用的变量不一样。

    这里也看出JDK1.8采用的是尾插法,顺序和原链表顺序一致,而非倒序

    总结

    image.png

    JDK1.7的put()方法

    public V put(K key, V value) {
            //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(=16)
            if (table == EMPTY_TABLE) {
                inflateTable(threshold);//分配数组空间
            }
           //如果key为null,存储位置为table[0]或table[0]的冲突链上
            if (key == null)
                return putForNullKey(value);
            int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
            int i = indexFor(hash, table.length);//获取在table中的实际位置
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现
                    return oldValue;
                }
            }
            modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
            addEntry(hash, key, value, i);//新增一个entry
            return null;
        }
    
    

    第1个if处,做一个判空操作,为空则调用inflateTable创建一个哈希表

    第2个if处,如果key为null,调用putForNullKey方法

    紧接着用hash函数进行散列,并计算下标index,进入for循环,如果对应数据已经存在,则指向覆盖操作,用新的value替换旧的value,并return旧的value

    最后modCount用于计数防止并发,调用addEntry添加一个新的Entry结点


    JDK1.7的get()方法

    public V get(Object key) {
        // 判断key是否为null 
            if (key == null) {
                return getForNullKey(); // 直接去table[0]查找
            }
            // 否则 通过key获取获取对应下标的数组    
            Entry<K,V> entry = getEntry(key);
            // 返回value
            return null == entry ? null : entry.getValue();
        }
    

    开头也是先判断key是否为null,为null则调用getForNullKey,与上面put方法中为null时对应

    不为null则获取对应的下标数组,调用getEntry方法

    getEntry()方法

    final Entry<K,V> getEntry(Object key) {
            if (size == 0) {
                return null;
            }
         // 根据key获取hashcode值
            int hash = (key == null) ? 0 : hash(key);
         // 通过indexFor()计算下标, 获取对应数组下标的Entry对象, 并遍历 
            for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
                Object k;
            // 判断hash值是否相同  key是否一样
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            }
            return null;
        }
    

    如果Map的大小为0,返回null

    否则计算hash值,for循环中计算下标,最后判断hash和key的值是否相同,相同则返回对应的entry,否则返回null

    回到上面的get()方法,为null则返回null,否则返回entry的value值


    JDK1.8的put()方法

    上面已经分析过,不再赘述


    JDK1.8的get()方法

    public V get(Object key) {
        Node<K,V> e;
    
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    

    调用getNode()方法

    getNode()方法

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    
        // 1. 计算存放在数组table中的位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
    
            // 4. 通过该函数,依次在数组、红黑树、链表中查找(通过equals()判断)
            // a. 先在数组中找,若存在,则直接返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
    
            // b. 若数组中没有,则到红黑树中寻找
            if ((e = first.next) != null) {
                // 在树中get
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
    
                // c. 若红黑树中也没有,则通过遍历,到链表中寻找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

    第一个if处,如果table不为null,且长度大于0,且first变量等于(长度 - 1)& (hash值)不为null时,进入第2个if

    第2个if处,判断第一个结点first的hash值和key是否与参数相等,为真则返回第一个结点

    若第2个if为假则来到第3个if处判断节点数量

    若节点数量不为1,则进入第4个if处进行红黑树的查找

    若第3个if为真,第4个if为假,则进入do-while循环,在链表中寻找。和第2个if的判断条件一致,找到则返回e,否则进入循环,找链表的下一个结点


    总结与比较

    1.7put()方法

    1. 判断列表是否为空
      1.1 为空则初始化,初始化后判断参数key是否为null
      1.2 不为空则直接判断参数key是否为null

    2. 参数key若为null,则遍历数组索引为0处是否能找到key为null的元素
      2.1 找到则替换并返回
      2.2 找不到则直接返回null

    3. 参数key若不为null,对参数进行hash后计算下标,遍历数组判断是否有hash值相同且key相同,或者调用equals()方法后相同的位置
      3.1 如果有相同的,则替换旧元素并返回
      3.2 如果没有相同的,则先判断当前数组大小是否超过阈值

    4. 如果超过阈值,则数组扩容为原来的2倍,并重新按上述步骤计算插入的位置

    5. 如果没有超过阈值,则直接插入链表头部


      image.png

    先扩容,再插入

    1.8put()方法

    1. 先计算hash值

    2. 判断是否为空表
      2.1 若为空表,则进行resize操作
      2.2 若不为空表,则取hash与数组长度 - 1进行与操作计算存储位置

    3. 判断存储位置结点位置是否为null
      3.1 若为null则直接赋值,并判断赋值后存储元素数量是否大于阈值,大于则扩容,小于则返回null
      3.2 若不为null则查看第一个元素是否hash和key与参数相同

    4. 若相同则覆盖旧结点

    5. 若不相同则查看是否为树结点
      5.1 是则调用红黑树的put
      5.2 不是则遍历看是否能找到key与hash值与参数相同的元素

    6. 没找到则在链表末尾插入新结点,同样判断长度是否大于8,是否需要树化等一系列操作

    7. 找到则覆盖旧结点


      image.png

    先插入,再扩容


    1.7get()方法

    1. 先判断key是否为null
      1.1 是则调用getForNullKey()方法,放在索引为0处
      1.2 不是则调用getEntry()方法,并计算hash值,indexFor()方法计算下标

    2. 进而判断链表是否存在
      2.1 存在则查找链表中是否有key为null的结点,是则返回其value
      2.2 链表不存在或链表中不存在null结点则直接返回null


      image.png

    1.8get()方法

    1. 先计算hash值

    2. 调用getNode()方法,判断HashMap的table是否为null
      2.1 不为null则判断其长度是否大于0。大于0则与运算计算索引
      2.2 为null或table长度不大于0则返回null

    3. 判断哈希表对应索引上的元素是否为空
      3.1 不为空则先判断首结点是否与参数一致
      3.2 为空则返回null

    4. 若与参数一致则判断节点后个数是否大于1
      4.1 大于1则判断是否为树节点,是就遍历红黑树
      4.2 小于1则返回null。不是红黑树则遍历链表

    5. 查找红黑树或链表是否存在与参数相同的节点
      5.1 不存在则返回null
      5.2 存在则返回其value


      image.png

    hashcode和hash的区别与联系

    hashcode在HashMap中实现的是Object类型的hashcode方法,使用key的hashcode与上value的hashcode得出

    hash则是通过hashcode进行高十六位与低十六位异或运算得出,目的是为了让int类型的hashcode转换为32位比特位后,所有位都可以参与运算,增加散列程度

    而最终确定的index下标值则是hash与上数组长度 - 1得出的值


    部分参考:lkforce,b站【韩顺平讲java】


    欢迎指正。

    相关文章

      网友评论

          本文标题:HashSet & HashMap源码

          本文链接:https://www.haomeiwen.com/subject/yfztkltx.html