Java集合框架—HashMap—源码研读1

![5.jpg](https://upload-images.jianshu.io/upload_images/3154067-3b84eae377a07c4f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## **前言:** **本篇为HashMap源码研读系列第一篇,主要分析HashMap中put()方法的源码。**阅读前需要对HashMap有基础的了解譬如:hashCode、hashMap结构:数组列表、散列冲突、装填因子。 **第二篇主要分析resize和扩容原理的源码。源码基于JDK9.** * * * 最近,在研读HashSet源码时发现其put方法调用的是HashMap中的map.put(): ``` public boolean add(E e) { return map.put(e, PRESENT)==null; } ``` 然后想起来:原来**HashSet的底层是HashMap**,只是HashSet中put元素的时候,元素直接被当做key存进了hashMap中,这个PRESENT只是一个Object[]罢了,不论存放任何值都会有相同的PRESENT被当做‘值’存放进map中。 ``` private static final Object PRESENT = new Object(); ``` 但是今天,我们先不看set,而是主要研究下hashmap的源码,话不多说,让我们跳到HashMap源码中的put方法,看看有何乾坤? ``` public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } ``` 看上去很简单,put方法会return一个putVal方法,方法包含5个参数,其中前三个: **hash(key):**是根据传入的key键来计算出一个int型的数字 **key:**要放入hashMap中的【键】 **value:**和键相对应的【值】 **此处需要注意:hash(key)是根据传入的key键来计算出一个int型的数字,但是请注意,这个数字并不是通常所说的hashCode。我们点进去hash(key)中看一下:** ![image](https://upload-images.jianshu.io/upload_images/3154067-5051b5b30db29d9a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 看上去很简单,就一行。但其实,大有乾坤:*(P.S.此处^是异或运算符,>>>是无符号右移运算符。h>>>16表示将h的32位数值无符号右移16位。)* 此方法返回一个int型数字,如果key==null则返回0,否则返回(h = key.hashCode()) ^ (h >>> 16),这里首先会根据key计算出这个键的hashCode值,这里,key.hashCode()调用的是Object类的.hashCode()方法,此方法是native方法,具体实现不在java中,而是java通过JNI调用c++实现的。具体的native源码,可以在openJDK源码中找到对应实现。关于hashCode()此处就不展开了~我们需要知道的是:**如果一个对象保持不变,那么我调用hashCode()产生的32位int型的hash值也不会变。** **继续看代码:**h = key.hashCode()这里h是这个key通过hashCode()方法计算得到的hashCode值。得到h后:h ^ (h >>> 16) 即表示h会进一步和 (h >>> 16)进行异或运算并return最终的结果。**这里细心留意下会发现:h是个32位的int型,h >>> 16表示:原先位于右半部分的低16位全被清空,原先位于左半部分的高16位移到了现在的右半部分,左边空位用0补齐。** **然后得到的数再和h进行异或,那么最后的结果相当于保留了原先h的高16位部分,而低16位部分则相当于用原高16位和低16位异或。** 看到这,我想大部分人会有点懵,为什么算出来一个值还要右移16位?移位后还要进行异或运算?这么做的目的何在?***此处,留个悬念,我们继续往下看源码:*** hash(key)计算出键的hash值后,put方法reurn一个putVal()方法 ``` return putVal(hash(key), key, value, false, true); ``` 现在,我们进入putVal方法中一探究竟: ![image](https://upload-images.jianshu.io/upload_images/3154067-2a7bcfd255e17f5f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) putVal方法初始化了几个变量: ``` Node [] tab; Node p; int n, i; ``` 看到Node,我们点进去看一下是如何定义的: ``` static class Node implements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } ``` **可以看到,和LinkedList中的Node类似,HashMap中也定义了内部类:Node **,不过有点不同的是Node节点类有4个成员变量: **final int hash;** **final K key;** **V value;** **Node next;** 每个Node节点都可以存一个int类型的hash值,key,value,和指向另一个Node节点的引用next,现在让我们回到putVal中看一下: ``` Node [] tab; Node p; int n, i; ``` 这个tab是个Node节点数组,里面肯定有联系,带着疑问我们接着往下看: ``` if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; ``` 第一句就有点晕啊,table是什么? ``` transient Node [] table; ``` 原来table是个Node节点数组,到这里大概就能猜出来了:**HashMap的底层是链表数组,这个Node [] table就是HashMap所谓的【链表数组】** ``` if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; ``` 将table赋给tab,若tab为空或tab.length == 0,则执行n = (tab = resize()).length; 如果初始的HashMap为空则tab == null则进入初始化过程,初始化主要是通过resize()执行。 resize()后的数组列表赋给tab完成初始化。resize方法,看名字就知道是用于扩容的方法,在此先不做分析,留到下一篇讲解,现在我们知道resize()方法是用来给HashMap扩容的就行了。经过resize()方法初始化赋值后tab成为了一个长度为16,阈值为12的数组列表(初始容量默认为16,装填因子默认为0.75,阈值 = 容量 * 装填因子) ``` if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); ``` 如果tab[i]对象为空,则通过newNode()方法构造一个新节点,然后插入tab[i]中。注意,此处 **索引i = (n - 1) & hash,这是hashMap中的核心知识点。**对于任意一个新的等待添加的元素,是如何计算其插入位置索引的呢?就是通过(n - 1) & hash.hash是之前通过hash(key)计算出来的int型的hash值,n为table的容量 = table.length。 还记得上面int型的hash值是如何得到的么?**h = key.hashCode()) ^ (h >>> 16),也就是说这里的hash值h是和**hashCode相关的,但是不等价,而且这个h的计算方式比较独特,为什么不直接通过key.hashCode()得到hash值而非要用这种方式? 让我们看一张图和两段话: ![image](https://upload-images.jianshu.io/upload_images/3154067-d3c101893a007955.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) **在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。** ***这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率*** *——摘自美团技术团队分享:*[Java 8系列之重新认识HashMap](https://zhuanlan.zhihu.com/p/21673805) * * * 解决了hashMap中根据key求其在table[]中索引i的问题,接下来我们面临这第二个问题: **哈希碰撞。**table[i]处原先没有对象我才可以通过tab[i] = newNode(hash, key, value, null);插入新对象。要是tab[i]处不为空该怎么办呢?此处就是**hash碰撞:即不同元素的key通过hash(key)散列出的索引i相等,导致这些元素都会插入至table[i],也就是产生了哈希碰撞** 这里分几种情况: ``` //如果当前待插入对象的hash值和table[i]对象的hash值相同,且key值相同,则用新value覆盖掉原value if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) e = p; //若key不相同,且table[i]为红黑树,则将节点放入树中 else if (p instanceof TreeNode) e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); //若key不相同,且table[i]为链表,则进入for循环 else {...} ``` 1.若当前对象的hash值和tab[i]对象的hash值相同,且key键也相等,则直接用当前对象的新value值覆盖原value。 2.若hash值相同但key不同,则将当前对象插入到table[i]中的对象中去,此对象可能是链表也可能是红黑树。若table[i]对象为红黑树,则将当前对象插入树中。 3.若table[i]对象为链表,则插入之前需要依次遍历每个链表节点,寻找插入位置。 让我们来看看第3种情况: ![image](https://upload-images.jianshu.io/upload_images/3154067-feb95d5733c1663b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 简单来说,如果table[i]为链表,那么会通过一个无限for循环遍历此链表来寻找插入位。正常情况下会循环到链表尾,停止循环,将待插入的key和value通过newNode()方法放入新构造的Node节点中,将此Node节点置于链表尾。或者在循环过程中发生key值冲突,则会提前break出for循环。这时,最后如果e != null,则表示发生了key值冲突,则会用新value值覆盖掉原节点的value值,并return出去。 * * * **本篇文章主要讲解的是put方法,hashmap还有很多精彩的内容其实并未涉及,譬如resize()扩容方法,链表转红黑树的实现.....这些问题放在下一篇文章中。最后,墙裂推荐几篇非常好的文章~** [HashMap实现原理及源码分析 - dreamcatcher-cx - 博客园​www.cnblogs.com![图标](https://upload-images.jianshu.io/upload_images/3154067-ee1ea3b8964be325.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/chengxiao/p/6059914.html) [美团技术团队:Java 8系列之重新认识HashMap​zhuanlan.zhihu.com![图标](https://upload-images.jianshu.io/upload_images/3154067-6ce7362d88eb6e5a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)](https://zhuanlan.zhihu.com/p/21673805) [极乐君:Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结​zhuanlan.zhihu.com![图标](https://upload-images.jianshu.io/upload_images/3154067-57ca5715a96180a9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)](https://zhuanlan.zhihu.com/p/24338517) [清浅池塘:HashMap底层实现原理(上)​zhuanlan.zhihu.com![图标](https://upload-images.jianshu.io/upload_images/3154067-3fe892eeb9fa2942.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)](https://zhuanlan.zhihu.com/p/28501879)

你可能感兴趣的:(Java集合框架—HashMap—源码研读1)