HashMap源码分析(学HashMap源码看这一篇就够了)
写在前面:这篇博客主要是针对HashMap的源码分析,读源码最忌心浮气躁,希望大家能静下心来一句一句阅读代码和注释,相信你一定有收获
HashMap简介
HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。在jdk1.8以前HashMap主要基于数组加链表来实现,而jdk1.8以后HashMap通过数组加链表加红黑树来实现
HashMap的底层结构和核心理论
HashMap的底层结构(图片来源于网络)
HashMap通过hash函数将对应的key转化成固定长度的值,来充当下标映射到数组对应的位置
Hash的特点:
- 通过hash值不能反向推导出原始的数据的值
- 相同的元素会得到相同的hash值
路由寻址算法:hash&size-1(hash为hash码,size为数组长度)
路由寻址算法找到的即为key在数组中对应的下标值,即得到对应的存放位置
哈希冲突:hash的原理是将输入空间的值映射到hash空间内,由于hash的空间远小于输入的空间,所以可能导致不同的输入映射到相同的hash空间处,即哈希冲突。这也是为什么HashMap()要使用数组加链表加红黑树的结构就是为了解决发生hash冲突的情况。
加载因子(loadFactor):用来计算扩容阈值,公式为:thresold=loadFactor*size(size为数组长度)
loadFactor默认为0.75f
扩容阈值(thresold):容量达到扩容阈值进行扩容
HashMap源码分析(此次只选取部分重要代码分析)
变量:
//序列号 private static final long serialVersionUID = 362498820763181265L; // 默认初始容量为16,1 << 4表示1左移4位 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // HashMap数组最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认构造因子 /* DEFAULT_LOAD_FACTOR用来计算HashMap的扩容阈值,即 * 即什么时候对HashMap进行扩容 计算公式为:thresold=loadFactor*size(size为HashMap数组大小) * 当数组容量大于16时扩容时新的阈值为旧阈值的两倍,而不计算 * * */ static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当桶上的结点数大于这个值时链表会转化成红黑树 static final int TREEIFY_THRESHOLD = 8; // 当桶上的结点树小于这个值时红黑树会转化成链表 static final int UNTREEIFY_THRESHOLD = 6; // 链表转化成红黑树所要求的数组长度的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; int threshold;//扩容阈值,容量到达阈值时进行扩容`
构造方法
HashMap提供了四个构造方法 ,其中3个有参构造,1个无参构造
//构造方法1,用来构造指定的初始容量和加载因子的HashMap public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0)//容量要大于0 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) //如果容量大于默认最大容量则设为默认最大容量 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //通过给定的initialCapacity来计算初始容量,HashMap要 //求数组容量为2的n次方的格式,所以需要对你输入的数进行格式化 //tableSizeFor的作用就是返回大于或等于且最接近initialCapacity //的2的n次幂的格式的数 this.threshold = tableSizeFor(initialCapacity); } // tableSizeFor源码 static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1;//n=n|n>>>1,n等于n与n右移一位进行位或(有1为1) n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; //位或执行的结果是让输入的参数最高位的1后面全变成1 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//最后再加一即得到一个大于等于输入数 //(最接近输入数)的2的n次幂的数 }
构造方法2 public HashMap(int initialCapacity) {//用来构造指定的初始容量的HashMap this(initialCapacity, DEFAULT_LOAD_FACTOR); }
构造方法3 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
构造方法4 public HashMap(Map<? extends K, ? extends V> m) { //用来构造HashMap,将Map的数据赋值给它 this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
put方法
// 调用putVal方法进行存放数据操作 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } //hash方法(扰动函数) //作用:让key的高16位也参与路由寻址运算,减少哈希碰撞 //路由寻址运算为hash&size-1(size为数组长度) //一开始数组长度不大,故在进行&运算时并不能让hash码的所有位都 //参与运算(只有低位的能参与运算),所以让hash码的高位与低位进行异或 //运算可以让hash的高位也可以参与和size-1的&运算 static final int hash(Object key) { int h; //key.hashCode()调用本地方法获取hash码 //(h = key.hashCode()) ^ (h >>> 16) hashCode的低16位与高16位进行异或运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //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)//将table赋值给tab并判断是否为空(判断数组是否已初始化) //HashMap第一次初始化不是在创建对象时,而是在调用put方法时 n = (tab = resize()).length;//数组未初始化,调用扩容函数resize进行初始化 // 数组已初始化 if ((p = tab[i = (n - 1) & hash]) == null)//(n - 1) & hash路由寻址,找到该key在数组对应的下标并将对应位置的数据赋值给p // p为null表示在数组的该位置还没有存放数据,故直接存放进去就好了 tab[i] = newNode(hash, key, value, null); // 数组已初始化且通过路由寻址找到的数组对应位置已有数据存放(发生hash冲突) else { Node<K,V> e; //存放结点 K k; // 判断在数组该位置上的元素是不是与存放的元素key值是否相等 // 注:hash值相等,两个数据不一定相等,但两个数据相等,hash值一定相等 // *** if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;//相等把p赋值给e //数组该位置上的元素与存放的元素key值不等 else if (p instanceof TreeNode)//判断是不是红黑树 // 红黑树等专门写一篇来记录 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//说明数组首元素与要存入元素key值不等,且后面结构不是红黑树(即为链表) for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {//到达链表尾部直接存放在链表尾 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // 判断结点是否到达阈值,到达了要转化为红黑树 treeifyBin(tab, hash); break; } // *** 判断链表中的结点与要存入元素key值是否相等,e此时存放对与要存入元素key相等的结点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e;// p = e与e = p.next组合用于遍历链表 } } // ***处发生时执行的代码 if (e != null) { // 该结点key值与要存入元素相等 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null)//onlyIfAbsent:putValue函数参数,为true则替换key相同元素 e.value = value; afterNodeAccess(e);//访问后回调 return oldValue; } } ++modCount;//表示散列表结构被修改的次数 替换不算 if (++size > threshold)//实际大小大于阈值则进行扩容 resize(); afterNodeInsertion(evict);//插入后回调 return null; }
接下来是最核心的resize方法
首先解释一下为什么要进行扩容,HashMap进行扩容最重要的原因是数据变多时,hash冲突频率也会变高,此时会影响HashMap函数的查找效率(时间复杂度会从O(1)变成O(n)),所以需要扩容
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table;//存放数组 int oldCap = (oldTab == null) ? 0 : oldTab.length;//当前数组容量,oldTab为null说明是第一次存放数据(初始化) int oldThr = threshold;//扩容阈值 int newCap, newThr = 0; if (oldCap > 0) {//数组已经初始化了 if (oldCap >= MAXIMUM_CAPACITY) {//数组长度达到最大值了,此时不再进行扩容 threshold = Integer.MAX_VALUE;//将扩容阈值设置为int的最大值(很难达到) return oldTab; } // 扩容oldCap << 1,数组容量翻一倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)// newThr = oldThr << 1; // 当容量大于16时不再用加载因子计算阈值,而是直接将当前阈值翻倍 } // oldCap==0的情况 else if (oldThr > 0) // 数组还未初始化了 调用构造方法时指定了初始容量大小 newCap = oldThr; else { // 数组还未初始化了 调用构造方法时没有初始容量大小 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) {//数组容量小于16或调用构造方法时指定容量,使用公式计算阈值 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;//当前node结点 if ((e = oldTab[j]) != null) {//说明当前桶位有数据,给e赋值 oldTab[j] = null;//把原数组对应位置置空,方便JVM进行GC时回收 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 { // 链表 /* * 扩容后数组长度翻一倍,说明在用路由寻址函数时size-1比原来的数组多了一位 * 此时hash不变,则寻址有两种结果 * 1、hash与size多一位对应处数为0,则数据存放在新数组处的位置与原数组相等(低位j结点) * 2、hash与size多一位对应处数为1,则数据存放在新数组处为原数组位置加上旧数组长度(高位结点) * 如原来数组长度为16(size-1=1111),扩容后为32(size-1=11111) * 此时01111与11111在旧数组经过路由寻址后得到的下标值相同 * 而在新数组中的寻址结果却不同 * 所以在复制时需要对hash进行计算来寻找到对应的位置 * */ 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) {//判断hash与新数组最高位&的那一位是0还是1 if (loTail == null)//e为第一个元素,插入新数组 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; }
结尾
关于HashMap的常用方法源码分析到这里就结束了,如果有什么错误希望各位大佬能帮忙指正,如果有什么问题也可以评论大家一起讨论。如果觉得这篇文章对你有帮助的话希望能帮我点个赞
- 一篇文章搞定HashMap面试——HashMap源码分析
- HashMap源码分析 —— 一篇文章搞定HashMap面试
- 源码分析四(HashMap与HashTable的区别 )
- HashMap 源码分析 -- entrySet()
- java HashMap源码分析
- Java 容器源码分析之ConcurrentHashMap
- Java 8 中HashMap源码分析
- 随笔:深入理解HashMap——put和get方法的源码分析
- HashMap、HashTable源码分析
- BAT面试必问HashMap源码分析
- HashMap源码分析
- jdk1.7 HashMap源码分析
- HashMap源码分析(史上最详细的源码分析)
- HashMap源码分析
- HashMap.put(K key, V value)源码分析
- 源码分析系列1:HashMap源码分析(基于JDK1.8)
- 源码分析系列1:HashMap源码分析(基于JDK1.8)
- HashMap源码分析
- JDK8中HashMap源码分析
- JDK1.8 HashMap源码分析