HashMap结构与图解

ε祈祈猫儿з 提交于 2020-02-26 01:23:56

主干结构

1.主干结构 Node<K,V>[] table
Node<K,V> implement Map.Entry<K,V>	//Node点
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;    //用来定位数组索引位置
    final K key;
    V value;
    Node<K,V> next;   //链表的下一个node

    Node(int hash, K key, V value, Node<K,V> next) { ... }
    public final K getKey(){ ... }
    public final V getValue() { ... }
    public final String toString() { ... }
    public final int hashCode() { ... }
    public final V setValue(V newValue) { ... }
    public final boolean equals(Object o) { ... }
}
2.HashMap哈希桶数组table的长度length大小必须为2的n次方(一定是合数),主要是为了在取模和扩容时做优化,同时减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
3.而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能

初始值创建 Map tableSizeFor()

tableSizeFor(int cap)				   //通过一个初始容量值 创建map 返回一个等于大于 cap 并且最接近cap 并且是2的次幂的数   
//cap 传入的容量参数  
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;
    	//经过上述 n|=n-1等五步,该算法让最高位的1后面的位全变为1。例如 01000 -> 01111.
    	// n = cap-1 避免 cap =2次幂造成的问题,例如 cap =8 的时候 返回的就是 16,我们想要的其实是8.
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

HashMap 计算 table[] 的索引位置

HashMap 计算 table[index]
// 方法一,jdk1.8 & jdk1.7都有:
static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 方法二,jdk1.7有,jdk1.8没有这个方法,但是实现原理一样的:
static int indexFor(int h, int length) {
     return h & (length-1);   //aka  h % length
}
三步原则
    (1) 取key的hashCode值,h = key.hashCode()
	(2) 高位参与运算,h ^ (h >>> 16)
	(3) 取模运算,h & (length-1)
当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

HashMap的put(K,V)方法

1.判断 table.length == 0,如果为0 进行 resize()扩容;
2.如果 table不为空数组,如果 key 进行 getIndex(key) 找到所在数组的索引位置,如果 table[index] == null,直接插入;如果key不存在,则判断 tbale[index]是否是treeNode 结构,如果是treeNode,则插入<K,V> 然后进行balanceTreeNode 平衡二叉树;如果不是treeNode,插入键值对 判断链表长度是否 >8 如果 >8,则treeify() 链表转为红黑树,如果插入的时候 key有一样的,则覆盖原来的value,并将原来的value return;最后modCount++,判断新增元素后的table[]的长度是否 > threshold [是否需要扩容]。

HashMap的扩容 resize()

resize()  //扩容
    1.如果 table[]为null的时候 默认设置长度为16的初始数组,threshold=16 * 0.75;如果table有Node,则设置长度为 16 << 1 == 16 * 2, threshold = 12 << 1 == 12 * 2。
    2.oldTable里面的数据复制到新的table中,其中有三种情况,node节点是一个单节点,没有形成链表,直接复制 newTab[e.hash & (newTabLength -1)]  = e; 
	3.node.next有节点,说明node已经形成一个链表结构或者是红黑树结构,当e instanceof TreeNode,是一个树形结构的话,会通过 (e.hash & oldTabLength) 判断 是在老的索引还是新的索引,如果(e.hash & oldTabLength) ==0 则e 在老的索引,如果(e.hash & oldTabLength) ==1 则 e在新的索引,新的索引=[e.hash & (oldLength - 1) + oldLength], TreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null; 分别存放 (e.hash & bit) == 0) 索引不改变的node节点,以及(e.hash & bit) == 1 新索引的node节点,然后再次通过 >UNTREEIFY_THRESHOLD(6)去判断是否需要继续构建成树形结构,如果节点数 >UNTREEIFY_THRESHOLD(6) 则通过treeify 构建成树形结构,如果小于 6, 则通过 untreeify 树形节点转为list链表。
    重点: 使用 (e.hash & oldTabLength == 0) 来判断是否需要在新的数组中改变索引位置 e.hash & oldTabLength == 1 需要改变索引位置,新的索引为: oldIndex+oldLength。
          都是内存指向的改变
          使用loHead loTail 高低位来进行复制Node

小记

DEFAULT_INITIAL_CAPACITY  =  1<< 4 		//aka 16  HashMap的默认数组大小
threshold							  //下一次HashMap扩容的临界值
tableSizeFor(int cap)				   //通过一个初始容量值 创建map 返回一个等于大于 cap 并且最接近cap 并且是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;
    	//经过上述 n|=n-1等五部,该算法让最高位的1后面的位全变为1。例如 01000 -> 01111.
    	// n = cap-1 避免 cap =2次幂造成的问题,例如 cap =8 的时候 返回的就是 16,我们想要的其实是8.
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
 位运算:
     8421 原则
     1111
     2的幂数 10000 原则
     n << 1 == n *2
     n >> 1 == n /2
     


treeify()	// 构建红黑树。
    
    如果 node元素是一个节点,node.next==null,没有形成链表或者tree,则newTab[e.hash & (newTabLength -1)]  = e;如果 node是一个tree
(1) 扩容是一个特别耗性能的操作,所以使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。

(2) HashMap是线程不安全的,在并发的环境中建议使用ConcurrentHashMap。 Collections.SynchronizedMap(Map)返回一个同步的map

(3) JDK1.8引入红黑树大程度优化了HashMap的性能,这主要体现在hash算法不均匀时,即产生的链表非常长,这时把链表转为红黑树可以将复杂度从O(n)降到O(logn)。

(4)HashMap是如何工作的?面试时可以这么回答: 
HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!