一、什么是hash表
数据结构的物理存储结构有两种:顺序存储结构和链式存储结构,在数组中根据下标查找某个元素,一次定位就可以找到,哈希表的主干就是数组。而这个下标的计算是通过hash函数来计算的,好的hash函数会使数据尽量的分散。
但是不管hash算法如何好,还是会有冲突发生,这就是hash冲突,解决方案有开放定址法,再散列函数法,链地址法,HashMap采用的就是链地址法,也就是数组加链表的方式。
二、HashMap实现原理
HashMap是Map的一个实现类,key不允许重复,可以为null,value无限制。jdk8之前,内部实现是由数组+链表来实现,jdk8的实现,在链表长度超过8的链表将转换为红黑树。
红黑树:https://www.cnblogs.com/skywang12345/p/3245399.html
//默认的容量,即默认的数组长度 16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//最大的容量,即数组可定义的最大长度static final int MAXIMUM_CAPACITY = 1 << 30;transient Node<K,V>[] table;//实际存储的键值对个数transient int size;//用于迭代防止结构性破坏的标量transient int modCount;
下面这三个属性是相关的,threshold 代表的下次扩容的size值,通常小于数组的实际长度。伴随着元素不断的被添加进数组,一旦数组中的元素数量达到这个阈值,那么表明数组应该被扩容而不应该继续任由元素加入。而这个阈值的具体值则由负载因子(loadFactor)和数组容量来决定,公式:threshold = capacity * loadFactor。
int threshold;final float loadFactor;//HashMap 中默认负载因子为 0.75static final float DEFAULT_LOAD_FACTOR = 0.75f;
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);}
这是一个最基本的构造函数,需要调用方传入两个参数,initialCapacity 和 loadFactor。程序的大部分代码在判断传入参数的合法性,initialCapacity 小于零将抛出异常,大于 MAXIMUM_CAPACITY 将被限定为 MAXIMUM_CAPACITY。loadFactor 如果小于等于零或者非数字类型也会抛出异常。
整个构造函数的核心在对 threshold 的初始化操作:
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;}
这是一个小巧但精妙的方法,这里通过异或的位运算将两个字节的 n 打造成比 cap 大但最接近 2 的 n 次幂的一个数值
1.1 put方法的实现
添加一个元素只需要传入一个键和一个值即可,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 还未被初始化,那么初始化它if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//根据键的 hash 值找到该键对应到数组中存储的索引//如果为 null,那么说明此索引位置并没有被占用//这里因为n始终是2的n次方,所以n-1 的二进制就全是1if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);//不为 null,说明此处已经被占用,只需要将构建一个节点插入到这个链表的尾部即可else {Node<K,V> e; K k;//当前结点和将要插入的结点的 hash 和 key 相同,说明这是一次修改操作if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//如果 p 这个头结点是红黑树结点的话,以红黑树的插入形式进行插入else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//遍历此条链表,将构建一个节点插入到该链表的尾部else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//如果插入后链表长度大于等于 8 ,将链表裂变成红黑树if (binCount >= TREEIFY_THRESHOLD - 1)treeifyBin(tab, hash);break;}//遍历的过程中,如果发现与某个结点的 hash和key,这依然是一次修改操作if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//e 不是 null,说明当前的 put 操作是一次修改操作并且e指向的就是需要被修改的结点if (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//如果添加后,数组容量达到阈值,进行扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
从整体上来看,该方法的逻辑已经清晰,然后看看一些细节。
首先,我们看 resize 这个方法是如何对 table 进行初始化的
//扩容方法final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;//拿到旧数组的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;//说明旧数组已经被初始化完成了,此处需要给旧数组扩容if (oldCap > 0) {//极限的限定,达到容量限定的极限将不再扩容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//未达到极限,将数组容量扩大两倍,阈值也扩大两倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1;}//构造函数根据传入的容量已经计算了下次扩容的一个合适的数组容量暂存在threshold中,//这里直接使用else if (oldThr > 0)newCap = oldThr;//数组未初始化并且阈值也为0,说明一切都以默认值进行构造else {newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//newCap = oldThr 之后并没有计算阈值,所以 newThr = 0if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;//根据新的容量初始化一个数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;//旧数组不为 null,这次的 resize 是一次扩容行为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;//如果 e 是红黑树结点,红黑树分裂,转移至新表else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//这部分是将链表中的各个节点原序地转移至新表中,我们后续会详细说明else {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;elseloTail.next = e;loTail = e;} else {if (hiTail == null)hiHead = e;elsehiTail.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;}}}}}//不论你是扩容还是初始化,都可以返回 newTabreturn newTab;
1.2 remove方法的实现
删除操作就是一个查找+删除的过程,相对于添加操作其实容易一些,但那是你基于上述添加方法理解的不错的前提下。
public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;}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;//如果跟头节点相等,则将待删除node指向头节点if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;//如果有下个节点else if ((e = p.next) != null) {//如果是红黑树,通过红黑树的操作方法获得nodeif (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {//否则就循环查找待删除nodedo {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}//如果node不为空,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通过控制容量是2的n次方,初始值是16,便于hash的计算(可以使用位操作),加载因子是空间利用率和查询修改效率直接的一个平衡,默认是0.75。
1.7之前的版本并发下添加数据可能会引起闭环,导致死循环,产生原因,当两个线程同时进行扩容的时候,某个位置上有链表 1->2->3,假如线程1执行完第一行后挂起,e指向1,e.next指向2,当线程2扩容后,此位置上到链表变成了2 ->1,然后线程1开始执行
执行第一行后 next -> null
第二行 e.next -> 2 ->1
第三行 newTable[i] -> e 已经形成闭环
第四行 e -> null 退出循环
1、Entry<K,V> next = e.next;2、e.next = newTable[i];3、newTable[i] = e;4、e = next;
