Map初始化及put过程(源码解析)

来源:互联网 发布:centos7 库软件安装 编辑:程序博客网 时间:2024/06/05 23:02

这篇文章借鉴了知乎@清浅池塘的源码解析,并重新进行了整理。

HashMap通过默认的构造方法在堆内存中开辟一块地址。并指定默认负载因子。 

HashMap底层是一个数组+链表的结构。即一个线性数组结构,Map中有一个内部Entry接口,HashMap在自己的静态内部类Node中实现了它。有三个属性key/value/next。即键值和下一指向。 

当调用mapput方法时,调用hashmapgetNode方法,它返回一个Node节点。 

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

而且这一步在jdk1.7是做了4次扰动,jdk1.8简化为1次,一次就够用了,毕竟边际递减效应。 

h=hashCode() : h: h 16 : hash = h '4 (h 16) : (n WfihashCodeo i±AHash 0101 5

这部分的返回值即扰动后的散列值。 

static final int int h: return (key hash (0b ject key) = null) ? O: (h = key. hashCode (h 16)

外层putValue方法,当Node数组为空/长度为0时,调用resize函数,进行如下操作: 

当放入第一个元素时,出发resize函数的newCap =DEFAULT_INITIAL_CAPACITY。即当数组为空时,以默认容量16构造一个数组。 

final V hash, key, V value, boolean onlyIfAbsent, boolean evict) Node<K, V > C) tab: ID p: int n, i: if ( (tab = table) (n = tab. length) = null n = (tab = resize()). length; if ( (p = tabli = (n — 1) & hash)) = null) tab [i] = newNode (hash, key, value, next: null) ; else { Node<K, e; k;

然后继续执行下面的语句,有个判断,即上面所说的,将散列值和数组长度做与,算出数组下标。这个算出来的一定在0n-1之间。然后将其赋值,把Node放进该数组位置中。 

最后放进去是这个样子,所谓的线性数组。 

E15-5 :

但也可能出现数组下标冲突的情况。紧接着上面putVal的代码。 

else Node<K, e: if (p. hash hash = key Il (key null key. equals(k)))) key) else if (p instanceof TreeNode) e = ((TreeNode<K, . putTreeVa1 ( map: this, tab, hash, for (int binCount = 0: . ++bi nCount) if ( (e = p. next) = null) p. next newNode (hash, key, value, next: null) if (binCount TREEIFF_WRESHOLD- 1) treeifyBin (tab, hash) key, value) break: if (e. hash hash && = key ( (k = e. key) break (key ! = null && key. equals if (e — null) { existing napping for key V oldVa1ue e. value: if (!on1yIfAbsent oldVa1ue = value: e. value afterNodeAccess (e) : return oldValue = null)

For循环里有一行p.next = newNode(hash, key, value, null); 

也就是说new一个新的Node对象并把当前Nodenext引用指向该对象,也就是说原来该位置上只有一个元素对象,现在转成了单向链表。 

下面还有两行 

if (binCount >= TREEIFY_THRESHOLD - 1) //当binCount>=TREEIFY_THRESHOLD-1 

      treeifyBin(tab, hash); 

当链表长度到8时,将链表转化为红黑树来处理。果然追根溯源都到数据结构了。 

JDK1.7及以前的版本中,HashMap里是没有红黑树的实现的,在JDK1.8中加入了红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率。 

final V (int huh, K key V value. I»olean onlyIfAb•ent. boolean i tab; p; int n, ( (tab • table) — null (n • tab. length) — O) • nevNodeIha:h, key. value, null); else (P.hUh — , ( (k • — key Il (key null key.equalz(k)))) ror ant binCount • O; ++binCount) ( (e • p.next) null) p.next • nevNodeIha:h, key, value, null); (bin-count - 1) // -1 for treeityun(tab, hash) ; if (e.hash huh Ilk • e.key) — key Il (key null key.emaaiz(k)))) break 1 2 3 4 5 V oldVaIue • e -value; eoaIyIZAbsent e. valt* • value; atterEodeAcce:: (e) return 01flaIue: ( "size > threshold) resize 0 ; re turn null man:n; or oldVaIue null) value

当重复放入同一个key时时,hashmap会覆盖掉以前的key。上面putVal方法走125.至于2345则是在数组为空的时候才执行 

总结: 

计 幫 萦 弘 逻 罾 判 所 Start № 是 否 为 空 length=o 否 根 据 誕 值 key 计 算 hash 值 得 到 插 人 的 数 组 索 引 i table( 刂 = = nul resize . 扩 容 否 № [ 刂 是 否 为 treeNode 否 开 始 遍 历 链 表 准 备 插 入 表 长 度 是 否 大 于 8 链 表 插 人 , 若 key 存 在 直 接 覆 mvalue 是 是 key 是 否 存 在 直 接 覆 盖 value 红 蔗 红 黑 树 直 接 插 人 键 偵 对 转 换 红 黑 树 , 插 人 键 值 对 是 直 接 插 人 ++Size> threshold 否 End 是 resize- 扩 容

HashMap的最底层是数组来实现的,数组里的元素可能为null,也有可能是单个对象,还有可能是单向链表或是红黑树。 

文中的resize在底层数组为null的时候会初始化一个数组,不为null的情况下会去扩容底层数组,并会重排底层数组里的元素。 

知道hashmapput实现,也就能针对性的做一些性能优化,比如用map做一个本地缓存,如果没指定出事容量,默认16,乘以负载因子后该map的临界容量为12,想要往这个map里放600key,这个map就需要扩容六次,这个过程抛弃以前的线性数组,new一个新的线性数组,容量为其二倍,而且原有键值需要进行重hash,很浪费性能。反之,如果初始容量过大,散列程度过高,会减慢检索速度。所以要指定合适的初始容量.