HashMap是实现map接口的一个重要实现类,在咱们不管是平常仍是面试,以及工做中都是一个常常用到角色。它的结构以下:html
它的底层是用咱们的哈希表和红黑树组成的。因此咱们在学习HashMap底层原理的时候,须要有这两种数据结构的知识作铺垫,才能有更好的理解!java
散列表是由咱们的数组和链表组成的,集成了两种数据结构的优势,咱们先简单介绍一下这两种数据结构。node
数组:数组存储区间是连续的,占用内存严重,故空间复杂度很大,但数组的二分查找时间复杂度很小,为 o(1),数组的特色:查找速度快、插入和删除效率低面试
链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,为 o(n),链表的特色:查找速度慢、插入和删除效率高数组
哈希表:哈希表为每一个对象计算出一个整数,称为哈希码。根据这些计算出来的整数(哈希码)保存在对应的位置上!若是遇到了哈希冲突,也就是同一个坑遇到了被占用的状况下,那么咱们就会以链表的形式添加在后面。安全
关于红黑树的知识点比较多,若是过多介绍红黑树的话,那么HashMap就很差介绍了。这里给上一个链接,一篇关于红黑树很是好的文章。点击这里数据结构
好了,开始解析咱们的源码,经过解析源码更好的了解HashMap后,对那么常见的面试题也能够更加的吃透!多线程
首先就是介绍咱们的HashMap的基本属性,对基本属性介绍完以后,对后面方法里使用时才不会迷惑并发
一、咱们的默认的初始化的hashmap的容量,若是没有指定的话,就是咱们的默认,1<<4就是16。app
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
二、咱们的hashmap最大容量,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
三、默认的装载因子,0.75。有什么用呢?好比咱们的容量如今是16,16*0.75=12,也就是说,当咱们的实际容量到了12的时候,那么就会触发扩容机制,进行扩容!
static final float DEFAULT_LOAD_FACTOR = 0.75f;
四、咱们知道哈希表是由数组和链表组成的,每个位置均可以说是一个哈希桶。咱们的哈希桶默认是链表,可是在JDK1.8以后咱们的哈希桶中当有TREEIFY_THRESHOLD个节点的时候,也就是下面默认的8,咱们桶中的链表会被转换为红黑树的结构。
static final int TREEIFY_THRESHOLD = 8;
五、与上面相同,不过不一样的是,会将红黑树转换成链表。
static final int UNTREEIFY_THRESHOLD = 6;
六、当哈希桶的结构转换成树以前,还会有一次判断,只有键值对大于64才会转换!也就是咱们下面定义的最小容量,这是为了不哈希表创建初期多个键值对恰巧都在一个哈希桶上面,而致使了不必的转换。
static final int MIN_TREEIFY_CAPACITY = 64;
七、内部结构静态内部类
八、其余成员变量
这里同时引起了咱们一些思考?为何要将转换成树形结构的阈值设置为8呢?为何不将转换成链表结构的阈值也设置为8呢?这里咱们在最后面试题分析的时候统一进行回答!
hashmap的构造方法有四个,不过咱们重点介绍其中的一个,由于这一个理解了,其余的也不成问题。
//initialCapacity:初始大小 //loadFactor:装载因子 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 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; this.threshold = tableSizeFor(initialCapacity); }
总结了构造方法进行的操做:
tableSizeFor
来返回一个大于等于initialCapacity的2次幂。static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
关于为何作了位运算后能够返回大于等于它的二次幂,能够看一下这篇博文!点击跳转
这里的threshold也就是咱们的阈值,当达到了这个阈值的时候咱们会进行扩容!可是这里可能也会以为疑惑,阈值不是容量*装载因子吗?不该该写成下面这样子吗?
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
注意,在构造方法中,并无对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会用到resize()方法,而后对threshold从新计算。后面咱们对方法分析时会谈到。
关于hashmap和核心方法和考点,其实都集中在put方法和resize()方法,这也会是咱们下面重点要介绍到的。
咱们首先来看put方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
调用了咱们的putval方法,参入了一个以key计算的哈希值,key,value,还有两个其余参数。在看putVal方法以前先来看一下hash方法,看看它是如何计算哈希值。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
这是一个三目运算符,若是key不为null的话,返回咱们的key的哈希值(低十六位)同时与高16位的异或运算。这一步的操做意义何为呢?咱们先临时跳到putVal方法里面能够看到有这么一步操做
它将咱们计算出来的哈希值,与咱们的哈希表长度-1(为了得到)进行&
运算,这是为了获取个人table下标。至于为何-1呢?由于咱们的长度都是2的整数次幂,转换成2进制也就是1000000....这种的形式,为了更好的随机,全部咱们进行了-1操做,也就是变成11111111这种。由于&
操做是都为1的时候才会为1,因此个人的1多的时候随机性才会更大,毕竟一个1能干过那么多的1吗?这是减小哈希冲突的第一步操做。举个例子说明一下:
好比咱们的长度转换为2进制为 1000 0000 ,进行-1操做后就是 0111 1111 而这个时候咱们原来的二进制数 1000 0000 & 0101 1011 = 0000 0000 与任何最高位不为1的数进行&运算,都会变成0,也就让咱们的哈希冲突变大了! 而咱们-1操做后 0111 1111 & 0101 1011 = 0101 1011 能够看出来,这样比原来的减小了不少的哈希冲突。 同时这也是为何咱们要让哈希的容量大小必定要为2的整数次幂
好了,咱们要回答一下再上面那个问题了,为何要返回低16位与高16位的异或做为key的最终hash值呢?一样举个例子演示一下这个流程:
假设length为8,HashMap的默认初始容量为16;
length = 8 ,(length-1) = 7 , 转换二进制为111;
假设一个key的 hashcode = 78897121 ,转换二进制:100101100111101111111100001,与(length-1)& 运算以下
0000 0100 1011 0011 1101 1111 1110 0001 &运算 0000 0000 0000 0000 0000 0000 0000 0111 = 0000 0000 0000 0000 0000 0000 0000 0001 (就是十进制1,因此下标为1)
上述运算实质是:001 与 111 & 运算。也就是哈希值的低三位与length与运算。若是让哈希值的低三位更加随机,那么&结果就更加随机,就更能减小咱们的哈希冲突了。如何让哈希值的低三位更加随机,那么就是让其与高位异或,因此咱们才在返回的时候与高位异或了再返回。低位与高位异或的过程举个例子以下:
而后总结一下在与咱们与哈希值进行运算的时候有这么一个规律:
当length=8时 下标运算结果取决于哈希值的低三位
当length=16时 下标运算结果取决于哈希值的低四位
当length=32时 下标运算结果取决于哈希值的低五位
当length=2的N次方, 下标运算结果取决于哈希值的低N位。
好了,咱们继续回到咱们的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; //当咱们的table为空的时候调用resize()进行扩容初始化 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)))) //hashcode和key相等,记录下原先的值 e = p; //若是这个时候咱们的哈希桶已是红黑树结构,那么调用树的插入函数 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //链表结构,同时咱们的hashcode不相等 //找到与key相等的节点,更新value,退出循环 //若是没有找到与key相等的节点,在链表尾部插入,若是插入后节点数量大于 //咱们变成红黑树的阈值,那么进行转换成红黑树 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; //空实现,为LinkedHashMap预留 afterNodeAccess(e); return oldValue; } } ++modCount; //键值对达到阈值,进行扩容 if (++size > threshold) resize(); //空实现,为LinkedHashMap预留 afterNodeInsertion(evict); return null; }
咱们在上面无论是源码分析仍是在哪分析,都说到了咱们的resize()方法,下面咱们将正式开始讲到
final Node<K,V>[] resize() { //原table数组赋值 Node<K,V>[] oldTab = table; //若是原数组为null,那么原数组长度为0 int oldCap = (oldTab == null) ? 0 : oldTab.length; //赋值阈值 int oldThr = threshold; //newCap 新数组长度 //newThr 下次扩容的阈值 int newCap, newThr = 0; // 1. 若是原数组长度大于0 if (oldCap > 0) { //若是大于最大长度1 << 30 = 1073741824,那么阈值赋值为Integer.MAX_VALUE后直接返回 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 2. 若是原数组长度的2倍小于最大长度,而且原数组长度大于默认长度16,那么新阈值为原阈值的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 3. 若是原数组长度等于0,但原阈值大于0,那么新的数组长度赋值为原阈值大小 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults // 4. 若是原数组长度为0,阈值为0,那么新数组长度,新阈值都初始化为默认值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 5.若是新的阈值等于0 if (newThr == 0) { //计算临时阈值 float ft = (float)newCap * loadFactor; //新数组长度小于最大长度,临时阈值也小于最大长度,新阈值为临时阈值,不然是Integer.MAX_VALUE newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //计算出来的新阈值赋值给对象的阈值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //用新计算的数组长度新建一个Node数组,并赋值给对象的table Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //后面是copy数组和链表数据逻辑 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; }
这个时候咱们以最初的三种构造方法来模拟一下流程。上面每个扩容状况都标注了记号
//① Map<String, String> map = new HashMap<>(); map.put("1", "1"); //② Map<String, String> map1 = new HashMap<>(2); map1.put("2", "2"); //③ Map<String, String> map2 = new HashMap<>(2, 0.5f); map2.put("3", "3");
代码4
逻辑,等到数组长度超过阈值12后,触发第二次扩容,此时table数组,和threshold都不为0,即oldTab、oldCap、oldThr都不为0,先走代码1
,若是oldCap长度的2倍没有超过最大容量,而且oldCap 长度大于等于 默认容量16,那么下次扩容的阈值 变为oldThr大小的两倍即 12 2 = 24,newThr = 24,newCap=32代码3
,肯定此次扩容的新数组大小为2,此时尚未肯定newThr 下次扩容的大小,因而进入代码5
肯定newThr为 2 0.75 = 1.5 取整 1 ,及下次扩容阈值为1。当数组已有元素大于阈值及1时,触发第二次扩容,此时oldCap为1,oldThr为1,走代码1
newCap = oldCap << 1 结果为 4 小于最大容量, 但oldCap 小于hashMap默认大小16,结果为false,跳出判断,此时因为newThr等于0,进入代码5
,肯定newThr为 4 0.75 = 3,下次扩容阈值为3代码1
,同实例②,newCap = oldCap << 1 结果为 4 小于最大容量, 但oldCap 小于hashMap默认大小16,结果为false,跳出判断,进入代码5
,肯定newThr为 4 * 0.5 = 2,下次扩容阈值为2public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
获取了咱们的key的hashcode而后做为参数传入getNode方法中!
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //若是计算出来的哈希值是在哈希表上 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //若是恰好在哈希桶的第一个节点上,返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //若是不在第一个节点,遍历节点 if ((e = first.next) != null) { //若是这个时候咱们的哈希桶已是树形结构了,调用树形查找 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //对咱们的链表进行遍历查找元素 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //若是没有找到的话,返回null return null; }
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
首先是计算出咱们的hash,而后调用removeNode方法来移除
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; //咱们的哈希桶不为空,同时要映射的哈希值也在 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; //恰好咱们的哈希桶首位就是要删除的,记录下来 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; //若是不是,进行遍历查找 else if ((e = p.next) != null) { //若是是红黑树结构的话,调用树的查找方法 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { //对链表进行查找key do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //找到了以后就去删除,分成黑树,桶的首位,链表中, if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //红黑树,调用树删除节点的方法 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); //在桶首位,直接将下一个节点赋值给首位 else if (node == p) tab[index] = node.next; //在链表中,将下一个结点赋值给前一个节点的下一个节点,删除自我 else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
扩容是一个特别耗性能的操做,因此当咱们在使用 HashMap,正确估算 map 的大小,初始化的时候给一个大体的数值,避免 map 进行频繁的扩容。
负载因子 loadFactor 是能够修改的,也能够大于1,可是建议不要轻易修改,除非状况特殊。
HashMap 是非线程安全的,不要在并发的状况下使用 HashMap,建议使用 ConcurrentHashMap!
关于HashMap的源码就分析这些,由于这些足够咱们去了解它的一些基本特性和常见面试足够用了。下面我收集了一些面试题和咱们上面的留下的思考题进行分析!
一、为何要将转换成树形结构的阈值设置为8呢?为何不将转换成链表结构的阈值也设置为8呢?
当初始阈值为8时,链表的长度达到8的几率变的很小,若是再大几率减少的并不明显
树结构查找的时间复杂度是O(log(n)),而链表的时间复杂度是O(n),当阈值为8时,long8 = 3,相比链表更快,但树结构比链表占用的空间更多,因此这是一种时间和空间的平衡
至于为何不将转换链表的阈值也设置为8,是由于若是两个值太接近的话,就会形成频繁的转换,致使咱们的时间复杂度变高。而在6是通过计算后最合适的数值
二、HashMap 为何不用平衡树,而用红黑树?
这一题应该归类与数据结构了,不过这里一样给出分析
红黑树也是一种平衡树,但不是严格平衡,平衡树是左右子树高度差不超过1,红黑树能够是2倍
红黑树在插入、删除的时候旋转的几率比平衡树低不少,效率比平衡树高
查找时间复杂度都维持在O(logN),具体的还望查看红黑树的特性,上面最开始也给了一篇关于红黑树的介绍。
三、HashMap在并发下会产生什么问题?有什么替代方案?
HashMap并发下产生问题:因为在发生hash冲突,插入链表的时候,多线程会形成环链,再get的时候变成死循环,Map.size()不许确,数据丢失。
关于为何会形成环链的话,能够看这里!
替代方案:
四、HashMap中的key能够是任何对象或数据类型吗?
能够是null,但不能是可变对象,若是是可变对象,对象中的属性改变,则对象的HashCode也相应改变,致使下次没法查找到已存在Map中的数据
若是要可变对象当着键,必须保证其HashCode在成员属性改变的时候保持不变
五、为何不直接将key做为哈希值而是与高16位作异或运算?
这个咱们在上面说过了,还用图和样例解释,是为了更好的随机性,解决哈希碰撞。
六、关于更多的面试题
这里提供了一篇关于面试题挺多的博文,经过阅读源码,里面大部分的面试题均可以解答了!
由于HashTable和HashMap非常相似,就跟咱们的Vector与ArrayList的关系同样。提供了线程安全的解决方案,全部咱们在这里经过区别,就至关与对HashTable进行了源码分析!
从存储结构和实现来说基本上都是相同的。
它和HashMap的最大的不一样是它是线程安全的,另外它不容许key和value为null。
Hashtable是个过期的集合类,不建议在新代码中使用,不须要线程安全的场合能够用HashMap替换,须要线程安全的场合能够用ConcurrentHashMap替换或者Collections的synchronizedMap方法使HashMap具备线程安全的能力。
不一样点 | HashMap | HashTable |
---|---|---|
数据结构 | 数组+链表+红黑树 | 数组+链表 |
继承的类不一样 | 继承AbstractMap | 继承Dictionary |
是否线程安全 | 否 | 是 |
性能高低 | 高 | 低 |
默认初始化容量 | 16 | 11 |
扩容方式不一样 | 原始容量*2 | 原始容量*2+1 |
底层数组的容量为2的整数次幂 | 要求为2的整数次幂 | 不要求 |
确认key在数组中的索引的方法不一样 | i = (n - 1) & hash; | index = (hash & 0x7FFFFFFF) % tab.length; |
遍历方式 | Iterator(迭代器) | Iterator(迭代器)和Enumeration(枚举器) |
Iterator遍历数组顺序 | 索引从小到大 | 索引从大到小 |
公众号《Java3y》文章
知乎专栏《Java那些事儿》