欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

HashMap源码解析(初始化及put方法)

程序员文章站 2022-05-25 11:41:17
...

Map初始化及put过程:

首先通过默认的构造方法在堆内存中开辟一块地址。并指定默认负载因子。
HashMap底层是一个数组+链表的结构。即一个线性数组结构,Map中有一个内部Entry接口,HashMap在自己的静态内部类Node中实现了它。有三个属性key/value/next。即键值和下一指向。
当调用map的put方法时,调用hashmap的getNode方法,它返回一个Node节点。

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

再往下看,如果key不为空,通过key算出散列值,并赋给h,h再与右移16位的h异或。这种操作是为了加大hashcode低位的随机性。散列值是一个int类型的16进制数,共32位。在底层需要通过该散列值算出所在数组下标,以确定存储位置。但Hashmap默认容量16,32位散列值太大,不能直接拿来计算,因此要先对数组长度取模,得到余数,再用于计算下标。取模还是通过一个indexFor函数实现的,它将散列值和数组长度做与。高位清空,保留低位,如果数组长度还是取16的话,那取模之后只保留4位了。但如果只取最后几位,哈希碰撞可能很严重,且如果散列本身做的不好,分布上成等差数列,会产生规律性。这是就通过下面的扰动函数解决问题。先右移16位,在与自身异或。混合原始哈希码的高位和低位,以此来加大低位的随机性。
而且这一步在jdk1.7是做了4次扰动,jdk1.8简化为1次,一次就够用了,毕竟边际递减效应。
HashMap源码解析(初始化及put方法)
这部分的返回值即扰动后的散列值。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

外层putValue方法,当Node数组为空/长度为0时,调用resize函数,进行如下操作:
当放入第一个元素时,出发resize函数的newCap =DEFAULT_INITIAL_CAPACITY。即当数组为空时,以默认容量16构造一个数组。

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);

然后继续执行下面的语句,有个判断,即上面所说的,将散列值和数组长度做与,算出数组下标。这个算出来的一定在0到n-1之间。然后将其赋值,把Node放进该数组位置中。
最后放进去是这个样子,所谓的线性数组。
HashMap源码解析(初始化及put方法)
但也可能出现数组下标冲突的情况。紧接着上面putVal的代码。

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;
    }

For循环里有一行p.next = newNode(hash, key, value, null);
也就是说new一个新的Node对象并把当前Node的next引用指向该对象,也就是说原来该位置上只有一个元素对象,现在转成了单向链表。
下面还有两行
if (binCount >= TREEIFY_THRESHOLD - 1) //当binCount>=TREEIFY_THRESHOLD-1
treeifyBin(tab, hash);
当链表长度到8时,将链表转化为红黑树来处理。果然追根溯源都到数据结构了。
在JDK1.7及以前的版本中,HashMap里是没有红黑树的实现的,在JDK1.8中加入了红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率。
HashMap源码解析(初始化及put方法)
1.从底层数组取值,算出数组位置,如果该位置为空,直接把node插进去
2.如果该位置不为空,判断key是否重复了,如果重复,就替换掉老的node
3.如果该位置既不是空,又没重复,判断一下是否是红黑树
4.如果该位置既不是空,又没重复,又不是红黑树,那必然是链表。把node放下一链上。再看一下链表长度是否大于8
,如果大于8,转成红黑树。4下面的和2一样。在链表上判断是否重复了,重复的话就把该位置的链表值替换掉
5.如果e不为空,执行替换操作
解决哈希冲突有两种方法:
链地址法
开放地址法
HashMap采用了链地址法,即在冲突的数组位置上加链表
总结:
HashMap源码解析(初始化及put方法)
HashMap的最底层是数组来实现的,数组里的元素可能为null,也有可能是单个对象,还有可能是单向链表或是红黑树。
文中的resize在底层数组为null的时候会初始化一个数组,不为null的情况下会去扩容底层数组,并会重排底层数组里的元素。
知道hashmap的put实现,也就能针对性的做一些性能优化,比如用map做一个本地缓存,如果没指定出事容量,默认16,乘以负载因子后该map的临界容量为12,想要往这个map里放600个key,这个map就需要扩容六次,这个过程抛弃以前的线性数组,new一个新的线性数组,容量为其二倍,而且原有键值需要进行重hash,很浪费性能。反之,如果初始容量过大,散列程度过高,会减慢检索速度。所以要指定合适的初始容量,

相关标签: hashmap 源码