HashMap中的要点

1、HashMap碎碎念node

jdk1.8前插链表是头插法,以后是尾插法算法

key 能够为null,hash值为0数组

红黑树弱平衡:并发

红黑树是牺牲了严格的高度平衡的优越条件为代价红黑树可以以O(log2 n)的时间复杂度进行搜索、插入、删除操做

2、关键变量与默认值app

jdk1.8less

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  //初始容量为16
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
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; // 桶的阈值被超过期,若数组大小未超过这个,则resize(),超过了则转为红黑树;这个值要求比TREEIFY_THRESHOLD至少大四倍

 

3、扩容计算不小于当前容量的最小2的n次幂dom

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;
}

4、hash算法函数

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(1)首先获取对象的hashCode()值,而后将hashCode值右移16位,而后将右移后的值与原来的hashCode作异或运算,返回结果。(其中h>>>16,在JDK1.8中,优化了高位运算的算法,使用了零扩展,不管正数仍是负数,都在高位插入0)。优化

(2)在putVal源码中,咱们经过(n-1)&hash获取该对象的键在hashmap中的位置。(其中hash的值就是(1)中得到的值)其中n表示的是hash桶数组的长度,而且该长度为2的n次方,这样(n-1)&hash就等价于hash%n。由于&运算的效率高于%运算。this

     tab便是table,n是map集合的容量大小,hash是上面方法的返回值。由于一般声明map集合时不会指定大小,或者初始化的时候就建立一个容量很大的map对象,因此这个经过容量大小与key值进行hash的算法在开始的时候只会对低位进行计算,虽然容量的2进制高位一开始都是0,可是key的2进制高位一般是有值的,所以先在hash方法中将key的hashCode右移16位在与自身异或,使得高位也能够参与hash,更大程度上减小了碰撞率。

 

5、putVal 过程

①.判断键值对数组table[i]是否为空或为null,不然执行resize()进行扩容;

②.根据键值key计算hash值获得插入的数组索引i,若是table[i]==null,直接新建节点添加,转向⑥,若是table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key同样,若是相同直接覆盖value,不然转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是不是红黑树,若是是红黑树,则直接在树中插入键值对,不然转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操做,不然进行链表的插入操做;遍历过程当中若发现key已经存在直接覆盖value便可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,若是超过,进行扩容。

 

6、HashMap 扩容

①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;

②.每次扩展的时候,都是扩展2倍;

③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

7、为何是2倍,2次幂

好处1

在hashmap的源码中。put方法会调用indexFor(int h, int length)方法,这个方法主要是根据key的hash值找到这个entry在Hash表数组中的位置,源码以下:

/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

上述代码也至关于对length求模。 注意最后return的是h&(length-1)。若是length不为2的幂,好比15。那么length-1的2进制就会变成1110。在h为随机数的状况下,和1110作&操做。尾数永远为0。那么000一、100一、1101等尾数为1的位置就永远不可能被entry占用。这样会形成浪费,不随机等问题。 length-1 二进制中为1的位数越多,那么分布就平均。

好处2

如下图为例,其中图(a)表示扩容前的key1和key2两种key肯定索引位置的示例,图(b)表示扩容后key1和key2两种key肯定索引位置的示例,n表明length。

元素在从新计算hash以后,由于n变为2倍,那么n-1的mask范围在高位多1bit(红色),所以新的index就会发生这样的变化:

resize过程当中不须要像JDK1.7的实现那样从新计算hash,只须要看看原来的hash值新增的那个bit是1仍是0就行了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,能够看看下图为16扩充为32的resize示意图(一方面位运算更快,另外一方面抗碰撞的Hash函数其实挺耗时的):

这个设计确实很是的巧妙,既省去了从新计算hash值的时间,并且同时,因为新增的1bit是0仍是1能够认为是随机的,所以resize的过程,均匀的把以前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。

8、负载因子为何是0.75

Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006
* more: less than 1 in ten million
*
在理想状况下,使用随机哈希吗,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素的个数和几率的对照表。
从上表能够看出当桶中元素到达8个的时候,几率已经变得很是小,也就是说用0.75做为负载因子,每一个碰撞位置的链表长度超过8个是几乎不可能的。

负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,因此此时索引效率就会下降。反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间形成烂费,可是此时索引效率高。

9、1.7和1.8的区别

https://blog.csdn.net/qq_36520235/article/details/82417949

 

10、HashMap  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)
        //这里的resize  是初始化的时候调用 后面会讲
            n = (tab = resize()).length;    //从新计算一下大小
    //获取要插入元素在 哈希桶中的位置
    if ((p = tab[i = (n - 1) & hash]) == null) //若是这个位置没有Node
        //   return new Node<>(hash, key, value, next);
        tab[i] = newNode(hash, key, value, null);   //直接建立一个新的Node
        else {  //原来这个桶的位置上有Node
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) //若是你和桶上的第一个Node相等
                e = p;  //直接覆盖值
            else if (p instanceof TreeNode)//若是 你定位到的元素是一个TreeNode(Node的一个子类,也是HashMap的一个内部类)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//那么就插入一TreeNode节点
            else {//定位到这个hash桶了 可是这里面是链表(没有进行过树化)
                for (int binCount = 0; ; ++binCount) {//是链表
                    if ((e = p.next) == null) {
                    //若是p节点的next为空 直接在后面插入
                        p.next = newNode(hash, key, value, null);
    //这里的树化是putValue的时候 若是原本是链表 并且长度超过了8 那么就进行树化 
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                    //** 这个方法一会来分析
                            treeifyBin(tab, hash);
                        break;
                }
//若是下一个节点e 不为null 而且这个链表中的节点就是你要找的节点 终止循环               
                    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;    //将新的值赋值
                afterNodeAccess(e); //这个是空实现
                return oldValue;
            }
        }
            ++modCount;// 修改次数+1 和fastRemove()有关也和并发修改有关
        if (++size > threshold) //若是大于了阙值 须要扩容的大小
            resize();   //从新设置hash桶的大小,也有可能进行树化,见后面代码
        afterNodeInsertion(evict);//空方法
        return null;
    }

 

10、HashMap  resize方法源码解析

final Node<K,V>[] resize() {         Node<K,V>[] oldTab = table; // oldTable:当前的表         int oldCap = (oldTab == null) ? 0 : oldTab.length;  //若是你是新建立的话 表的大小就是0 不然就是原来的大小         //第一次是为0的    表明 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16         int oldThr = threshold;              int newCap, newThr = 0; //新的容量和新的扩容         //若是旧的容量大于0         if (oldCap > 0) {         //若是旧的容量大于最大的容量             if (oldCap >= MAXIMUM_CAPACITY) {                //那么扩容大小 = 最大范围                 threshold = Integer.MAX_VALUE;               //直接返回了                 return oldTab;               }         //不然 若是新的大小等于 oldCap * 2 < 最大的容量 , 而且旧的容量大于默认的初始化大小16             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                      oldCap >= DEFAULT_INITIAL_CAPACITY)      // double threshold  新的扩容 = 旧的扩容 * 2                  newThr = oldThr << 1;         }         else if (oldThr > 0) // initial capacity was placed in threshold              newCap = oldThr;    //若是旧的扩容原本就大于0,那么新的容量就是旧的扩容         else {               // zero initial threshold signifies using defaults 说明是 threshold为0的时候的状况             newCap = DEFAULT_INITIAL_CAPACITY;      //新的容量为默认容器的容量             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //新的阙值为 默认的容量 * 负载因子         }            if (newThr == 0) {  //若是新的扩容为0              float ft = (float)newCap * loadFactor;  //计算获得新的阙值         //新的阙值 =  若是新的容量小于 最大的容量 而且 新的阙只 < 最大的容量 那么新的阙值 = 计算的 不然 = 最大int             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) {     //遍历旧的hash桶         for (int j = 0; j < oldCap; ++j) {                   Node<K,V> e;                     if ((e = oldTab[j]) != null) {//若是旧的hash桶的元素不为null  e为旧的hash桶的元素                     oldTab[j] = null;   //旧的hash桶设置为null                     if (e.next == null)     //若是你就是一个元素                         newTab[e.hash & (newCap - 1)] = e;  //那么在新的hash桶给你安排一个位置  位置是你的hash值 & 新的桶的容量-1 这至关于 你的hash值 与 你的容量进行取模运算                     else if (e instanceof TreeNode) //若是你不仅一个元素而且是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;     }

相关文章
相关标签/搜索