layout: post
title: HashMap源码阅读
date: 2020-02-02
author: xiepl1997
tags: 源码阅读java
下面是JDK11中HashMap的源码分析,对代码的分析将主要以注释的方式来体现。node
HashMap是基于Map接口实现的哈希表,实现了Map接口中的全部操做,并且HashMap容许键为空值,也容许值为空值,与之对应的是Hashtable,Hashtable不能将键和值设置为空。HashMap不能保证元素的顺序,特别是,它不能保证随着时间的推移保持顺序不变。算法
HashMap为基本操做(get和put)提供了恒定的时间性能,假设散列函数在木桶(buckets)中适当地分散了元素。集合(Collection)的迭代须要的时间与HashMap实例的“容量”和它的大小成比例。所以,若是迭代的性能很重要,要求很高,那么不将初始容量设置得过高(或负载因素太低)是很是重要的。数组
HashMap是线程不安全的集合,即当多线程访问时,同一时刻若是没法保证只有一个线程修改HashMap,则会毁坏HashMap,抛出ConcurrentModificationException。缓存
HashMap底层使用哈希表(数组 + 单链表),当链表过长会将链表转成红黑树以实现O(logn)时间复杂度内查找。
HashMap的定义为class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable
安全
1.Node
2.KeySet
3.Values
4.EntrySet
5.HashIterator
6.KeyIterator
7.ValueIterator
8.EntryIterator
9.HashMapSpliterator
10.KeySpliterator
11.ValueSpliterator
12.EntrySpliterator
13.TreeNode 表明红黑树节点,HashMap中对红黑树的操做的方法都在此类中数据结构
HashMap采用的扩容策略是,每次加倍的方式。这样,原来位置的Entry在新扩展的数组中要么依然在原来的位置,要么在原来的位置+原来的容量
的位置。多线程
HashMap经过hash()函数(也叫“扰动函数”)来计算hash值,方法为key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;
,计算出来的hash值存放在Node.hash中。函数
hash的值计算至关于将高16位与底16位进行异或,结果是高16位不变,底16位变成异或的新结果。为何这样作呢,缘由是HashMap扩容以前的数组大小才为16,散列值是不能直接拿来用的。在进行长度取模运算时采用的只是取二进制中的最右端的几位,并无用到高位二进制的信息,作带来的结果就是hash结果分布不太均匀。而将高16位和底16位异或后就可让低位附带高位的信息,加大低位的随机性。源码分析
在对散列值作完高低位的异或操做后,在对异或结果进行对长度的取模获得最终的结果。具体参考JDK源码中HashMap的hash方法原理是什么?-胖君的回答-知乎
在hash计算中,null的hash值为0,而后按照正常的putVal()
插入。
从源码中(下文构造函数)咱们能够看到,new HashMap()开销很是少,仅仅确认装载因子。真正的建立table的操做尽量的日后延迟,这使得HashMap有很多操做都须要检查table是否初始化。这种设计有一种好处,就是可以没必要担忧HashMap的开销,能够一次性大量的建立HashMap。
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable { //用于序列化 private static final long serialVersionUID = 362498820763181265L; //HashMap的默认容量是16 static final int DEFAULT_INITIAL_CAPACITY = 16; //最大容量为1073741824(2的30次方,即1<<30) static final int MAXIMUM_CAPACITY = 1073741824; //默认装载因子为0.75f static final float DEFAULT_LOAD_FACTOR = 0.75F; /* 将链表转化为红黑树的阈值为8,即当链表长度 >= 8时,链表转化为红黑树,也就是树形化。 为何要树形化呢?想一下咱们为何要用HashMap,是由于经过Hash算法在理想状况下时间复杂度O(1)就能找到元素,特别快,但仅限于理想状况下,若是遇到了hash碰撞,且碰撞比较频繁的话,那么当咱们get一个元素的时候,定位到了这个数组,还须要在这个数组中遍历一次链表最终才能找到要get的元素,是否是已经失去了hashmap的初心了?(由于须要遍历链表,因此时间复杂度就高上去了)。 因此使用红黑树这种数据结构来解决链表过长的问题,能够理解为红黑树遍历比链表遍历快,时间复杂度低。 */ static final int TREEIFY_THRESHOLD = 8; //将红黑树转化成链表的阈值为6(<6时),这个是在resize()的过程当中调用TreeNode.split()实现 static final int UNTREEIFY_THRESHOLD = 6; /* 最小树形化阈值。要树化并不只仅是要超过TREEIFY_THRESHOLD,同时容量要超过MIN_TREEIFY_CAPACITY,若是只是超过TREEIFY_THRESHOLD,则会进行扩容(调用resize())。为何这个时候是扩容而不是树形化呢? 缘由就在于,形成链表过长也多是数组(桶)过短了也就是容量过小了。举个例子,若是数组长度为1,那么全部的元素都挤在了数组的第0个位置上,这个时候就算树形化只是治标不治本,由于引发链表过长的根本缘由是数组太短。 因此在执行树形化以前(链表长度>=8),会检查数组长度,若是长度小于64,则对数组进行扩容,而不是树形化。 */ static final int MIN_TREEIFY_CAPACITY = 64; /* 哈希表的数组主体定义,初始化时,在构造函数中并不会初始化,因此在各类操做中老是要先检查table是否为null。 */ transient HashMap.Node<K, V>[] table; /* 做为一个entrySet缓存,使用entrySet首先检查其是否为null,不为null则使用这个缓存,不然生成一个entrySet并缓存至此。 */ transient Set<Entry<K, V>> entrySet; //HashMap中的Entry的数量 transient int size; /* 记录修改内部结构化修改次数,用于实现fail-fast,ConcurrentModificationException就是经过检测这个抛出。 */ transient int modCount; //其值=capacity*loadFactor,当size超过threshold的时候便进行一次扩容 int threshold; //装载因子 final float loadFactor; …… }
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) { throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); } else { if (initialCapacity > 1073741824) { initialCapacity = 1073741824; } if (loadFactor > 0.0F && !Float.isNaN(loadFactor)) { this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } else { throw new IllegalArgumentException("Illegal load factor: " + loadFactor); } } }
public HashMap(int initialCapacity) { this(initialCapacity, 0.75F); }
public HashMap() { this.loadFactor = 0.75F; }
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = 0.75F; this.putMapEntries(m, false); } final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //获取该map实际长度 int s = m.size(); if (s > 0) { //判断table是否初始化,若是没有初始化 if (this.table == null) { /**求出须要的容量,由于实际使用的长度=容量*0.75得来的,+1是由于小数相除,基本都不会是整数,容量大小 不能为小数的,后面转化为int,多余的小数就要被丢掉,因此+1,例如,map实际长度为29.3,则所须要的容量为30. */ float ft = (float)s / this.loadFactor + 1.0F; //判断该容量大小是否超出上限 int t = ft < 1.07374182E9F ? (int)ft : 1073741824; //对临界值进行初始化,tableSizeFor(t)这个方法会返回大于t值的,且离其最近的2次幂,例如t为29,则返回的值是32 if (t > this.threshold) { this.threshold = tableSizeFor(t); } } else if (s > this.threshold) { //若是table已经初始化,则进行扩容操做,resize就是扩容 this.resize(); } Iterator var8 = m.entrySet().iterator(); //遍历,把map中的数据转移到hashmap中 while(var8.hasNext()) { Entry<? extends K, ? extends V> e = (Entry)var8.next(); K key = e.getKey(); V value = e.getValue(); this.putVal(hash(key), key, value, false, evict); } } }
该构造函数,传入一个Map,而后把该Map转为hashMap,resize方法在下面添加元素的时候会详细讲解,在上面entrySet方法会返回一个Set<Map.Entry<K,V>>,泛型为Map的内部类Entry,它是一个存放key-value的实例,也就是Map中的每个key-value就是一个Entry实例,为何使用这个方式进行遍历,由于效率高,putVal方法把取出来的每一个key-value存入到hashMap中。
hash函数负责产生hashcode,计算方法为若为空则返回0,不然返回对key的高16位和底16位的异或的结果。
static final int hash(Object key) { int h; return key == null ? 0 : (h = key.hashCode()) ^ h >>> 16; }
这个方法是判断传入的Object对象x是否实现了Comparable接口,若是传入的是String对象,天然实现了Comparable接口,直接返回就行。可是对于其余的类,比方说咱们本身写了一个类对象,而后存在HashMap中,可是就HashMap来讲它并不知道咱们有没有实现Comparable接口,甚至都不知道咱们Comparable接口中有没有用泛型,泛型具体用的是哪一个类。
static Class<?> comparableClassFor(Object x) { if (x instanceof Comparable) { Class c; if ((c = x.getClass()) == String.class) { return c; } ype[] ts; if ((ts = c.getGenericInterfaces()) != null) { Type[] var5 = ts; int var6 = ts.length; for(int var7 = 0; var7 < var6; ++var7) { Type t = var5[var7]; Type[] as; ParameterizedType p; if (t instanceof ParameterizedType && (p = (ParameterizedType)t).getRawType() == Comparable.class && (as = p.getActualTypeArguments()) != null && as.length == 1 && as[0] == c) { return c; } } } } return null; }
若是x为空,返回0;若是x的类型为kc,则返回compareTo(x)。
static int compareComparables(Class<?> kc, Object k, Object x) { return x != null && x.getClass() == kc ? ((Comparable)k).compareTo(x) : 0; }
该函数用于计算大于等于cap的的最小的2的整数幂,用于作table的长度。numberOfLeadingZeros()方法的做用是返回无符号整形i的最高非零位前面的0的个数,包括符号位在内。
static final int tableSizeFor(int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); return n < 0 ? 1 : (n >= 1073741824 ? 1073741824 : n + 1); }
public V put(K key, V value) { //四个参数,第一个hash值,第四个参数表示若是该key存在值,若是为null的话,则插入新的value,最后一个参数,在hashMap中没有用,能够不用管。 return this.putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab哈希数组,p为该哈希桶的首节点,n为hashMap的长度,i为计算出的数组下标 HashMap.Node[] tab; int n; //获取长度并扩容,使用的是懒加载,table一开始是没有加载的,等puthou才开始加载 if ((tab = this.table) == null || (n = tab.length) == 0) { n = (tab = this.resize()).length; } Object p; int i; //若是计算出的该哈希桶的位置没有值,则把新插入的key-value放到此处,此处就算没有插入成功,也就是发生哈希冲突时也会把哈希桶的首节点赋予p if ((p = tab[i = n - 1 & hash]) == null) { tab[i] = this.newNode(hash, key, value, (HashMap.Node)null); } else { //发生哈希冲突的几种状况 //e临时节点的做用,k存放当前节点的key值 Object e; Object k; //第一种,插入的key-value的hash值,key都与当前节点相等,e=p,则表示为首节点 if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) { e = p; } //第二种,hash值不等于首节点,判断该p是否属于红黑树的节点 else if (p instanceof HashMap.TreeNode) { /* 为红黑树的节点,则在红黑树中进行添加,若是该节点已经存在,则返回该节点(不为null), 该值很重要,用来判断put操做是否成功,若是添加成功返回null */ e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value); } //第三种,hash值不等于首节点,不为红黑树节点,则为链表的节点 else { //遍历该链表 int binCount = 0; while(true) { //若是找到尾部,则代表添加的key-value没有重复,在尾部进行添加。 if ((e = ((HashMap.Node)p).next) == null) { ((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null); //判断是否要转化为红黑树结构 if (binCount >= 7) { this.treeifyBin(tab, hash); } break; } //若是链表有重复的key,e为当前重复的节点,结束循环。 if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) { break; } p = e; ++binCount; } } //e不为null,则说明有重复的key,则用待插入值进行覆盖,返回旧值。 if (e != null) { V oldValue = ((HashMap.Node)e).value; if (!onlyIfAbsent || oldValue == null) { ((HashMap.Node)e).value = value; } this.afterNodeAccess((HashMap.Node)e); return oldValue; } } /* 到了这步,说明待插入的key-value是没有key的重复,由于插入成功的e节点的值为null。 修改次数+1 */ ++this.modCount; //实际长度+1,并判断是否大于临界值,大于则扩容 if (++this.size > this.threshold) { this.resize(); } this.afterNodeInsertion(evict); //添加成功 return null; }
扩容方法resize()
final HashMap.Node<K, V>[] resize() { //把没有插入以前的哈希数组叫作oldTab HashMap.Node<K, V>[] oldTab = this.table; //oldTab的长度 int oldCap = oldTab == null ? 0 : oldTab.length; //oldTab的临界值 int oldThr = this.threshold; //初始化new的长度和临界值 int newThr = 0; int newCap; //oldCap>0也就说明不是首次加载,由于hashMap用的是懒加载 if (oldCap > 0) { //若是大于最大值 if (oldCap >= 1073741824) { //将临界值设置为整数的最大值 this.threshold = 2147483647; return oldTab; } //位置*。其余状况,扩容两倍,而且扩容后的长度要小于最大值,old的长度也要大于16 if ((newCap = oldCap << 1) < 1073741824 && oldCap >= 16) { //临界值也要扩容为old的2倍 newThr = oldThr << 1; } } /* 若是oldCap<0,可是已经初始化了,像把元素删除完以后的状况,那么它的临界值确定还存在, 若是是首次初始化,它的临界值则为0. */ else if (oldThr > 0) { newCap = oldThr; } //首次初始化,给默认值 else { newCap = 16; newThr = 12; //临界值等于容量*0.75 } //位置*的补充,也就是初始化时容量小于默认值16的,此时newThr没有赋值 if (newThr == 0) { //new的临界值 float ft = (float)newCap * this.loadFactor; //判断new容量是否大于最大值,临界值是否大于最大值 newThr = newCap < 1073741824 && ft < 1.07374182E9F ? (int)ft : 2147483647; } //把上面各类状况分析出的临界值,在此处进行真正的改变,也就是容量和临界值都改变了 this.threshold = newThr; //初始化 HashMap.Node<K, V>[] newTab = new HashMap.Node[newCap]; //赋予当前的table this.table = newTab; //此处天然是把old中的元素,遍历到new中 if (oldTab != null) { for(int j = 0; j < oldCap; ++j) { //临时变量 HashMap.Node e; //当前哈希桶的位置值不为null,也就是数组下标处有值,由于有值表示可能会发生冲突 if ((e = oldTab[j]) != null) { //把已经赋值以后的变量置位null,固然是为了好回收,释放内存 oldTab[j] = null; //若是下标处的节点没有下一个元素 if (e.next == null) { //把该变量的值存入newTab中,e.hash & new Cap-1并不等于j newTab[e.hash & newCap - 1] = e; } //若是该节点为红黑树结构,也就是存在hash冲突,该hash桶中有多个元素 else if (e instanceof HashMap.TreeNode) { //把此树转移到newTab中 ((HashMap.TreeNode)e).split(this, newTab, j, oldCap); } /* 此处表示为链表结构,一样把链表转移到newTab中,就是把链表遍历后,把值转过去,再置位null */ else { HashMap.Node<K, V> loHead = null; HashMap.Node<K, V> loTail = null; HashMap.Node<K, V> hiHead = null; HashMap.Node hiTail = null; HashMap.Node 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; } e = next; } while(next != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } //返回扩容后的hashmap return newTab; }
删除元素
public V remove(Object key) { //临时变量 HashMap.Node e; /* 调用removeNode,第三个value表示,把key的节点直接都删除了,不须要用到值, 若是设为值,则还须要去进行查找操做。 */ return (e = this.removeNode(hash(key), key, (Object)null, false, true)) == null ? null : e.value; } /* 第一参数为哈希值,第二个为key,第三个为value,第四个为true的话,则表示删除它 key对应的value,不删除key,第四个若是为false,则表示删除后,不移动节点。 */ final HashMap.Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { //tab哈希数组,p数组下标节点,n长度,index当前数组下标 HashMap.Node[] tab; HashMap.Node p; int n; int index; //哈希数组不为null,且长度大于0,而后得到要删除key的节点的数组下标位置 if ((tab = this.table) != null && (n = tab.length) > 0 && (p = tab[index = n - 1 & hash]) != null) { //node存储要删除的节点,e临时变量,k当前节点的key,v当前节点的value HashMap.Node<K, V> node = null; Object k; //若是数组下标的节点正好是要删除的节点,把值赋给临时变量 if (p.hash == hash && ((k = p.key) == key || key != null && key.equals(k))) { node = p; } //也就是要删除的节点,在链表或者红黑树上,先判断是否为红黑树的节点 else { HashMap.Node e; if ((e = p.next) != null) { if (p instanceof HashMap.TreeNode) { //遍历红黑树,找到该节点并返回 node = ((HashMap.TreeNode)p).getTreeNode(hash, key); } //若是是链表节点,遍历找到该节点 else { label88: { while(e.hash != hash || (k = e.key) != key && (key == null || !key.equals(k))) { //p为要删除节点的上一个节点 p = e; if ((e = e.next) == null) { break label88; } } //node为要删除的节点 node = e; } } } } Object v; /* 找到要删除的节点后,判断!matchValue,咱们正常的remove删除,!matchValue都为true */ if (node != null && (!matchValue || (v = ((HashMap.Node)node).value) == value || value != null && value.equals(v))) { //若是删除的节点是红黑树节点,则从红黑树中删除 if (node instanceof HashMap.TreeNode) { ((HashMap.TreeNode)node).removeTreeNode(this, tab, movable); } //若是是链表节点,且删除的节点为数组下标节点,也就是头节点,直接让下一个做为头。 else if (node == p) { tab[index] = ((HashMap.Node)node).next; } //为链表结构,删除的节点在链表中,要把删除的下一个节点设为上一个节点的下一个节点。 else { p.next = ((HashMap.Node)node).next; } //修改计数器 ++this.modCount; //长度减1 --this.size; this.afterNodeRemoval((HashMap.Node)node); //返回删除的节点 return (HashMap.Node)node; } } return null; }
删除还有clear方法,把全部的数组下标元素都置位null。
下面在看较为简单的获取元素。
public V get(Object key) { HashMap.Node e; //也是调用getNode方法来完成 return (e = this.getNode(hash(key), key)) == null ? null : e.value; } final HashMap.Node<K, V> getNode(int hash, Object key) { //tab哈希数组,first头节点,n长度,k为key HashMap.Node[] tab; HashMap.Node first; int n; //若是哈希数组不为null,且长度大于0,获取key值所在的链表头赋值给first if ((tab = this.table) != null && (n = tab.length) > 0 && (first = tab[n - 1 & hash]) != null) { Object k; //若是是头节点,则直接返回头节点。 if (first.hash == hash && ((k = first.key) == key || key != null && key.equals(k))) { return first; } HashMap.Node e; //结果不是头节点 if ((e = first.next) != null) { //判断是不是红黑树结构 if (first instanceof HashMap.TreeNode) { //去红黑树中找,而后返回 return ((HashMap.TreeNode)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); } } return null; }
hashMap源码暂时分析到这里,能力有限,若是内容出现错误,欢迎指出。