一、jdk1.8中,对“HashMap的容量一定要是2的n次幂”做了严格控制
  1.默认初始容量:
[Java] 纯文本查看 复制代码
1
2
3
4
/**
 * The default initial capacity - MUST be a power of two.(默认初始容量——必须是2的n次幂。)
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16(16 = 2^4)
  2.使用HashMap的有参构造函数来自定义容量的大小(保证容量是2的n次幂):
  HashMap总共有4个构造函数,其中有2个构造函数可以自定义容量的大小:
  ①HashMap(int initialCapacity):底层调用的是②HashMap(int initialCapacity, float loadFactor)构造函数
[Java] 纯文本查看 复制代码
1
2
3
public HashMap(int initialCapacity) {
       this(initialCapacity, DEFAULT_LOAD_FACTOR);
   }
  ②HashMap(int initialCapacity, float loadFactor)
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
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的n次幂呢?
  答案就在标记出来的tableSizeFor(initialCapacity)方法中:
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
/**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }
  上面这段代码的作用:
    假如你传的cap是5,那么最终的初始容量为8;假如你传的cap是24,那么最终的初始容量为32。
    这是因为5并非是2的n次幂,而大于等于5且距离5最近的2的n次幂是8(8 = 2^3);同样的,24也并非2的n次幂,大于等于24且距离24最近的2的n次幂是32(32 = 2^5)。
    假如你传的cap是64,那么最终的初始容量就是64,因为64是2^6,它就是等于cap的最小2的n次幂。
    总结起来就一句话:通过位移运算,找到大于或等于 cap 的 最小2的n次幂。
   jdk1.7的初始容量处理机制和上面jdk1.8具有相同的作用,但1.7的代码好懂很多:
[Java] 纯文本查看 复制代码
1
2
3
4
5
6
7
8
public HashMap(int initialCapacity, float loadFactor) { 
   ……
    int capacity = 1
    while (capacity < initialCapacity)  {
        capacity <<= 1;
    }
    ……
}
  3.扩容:同样需要保证扩容后的容量是2的n次幂( jdk1.8 HashMap.resize()扩容方法的源码解析)
  resize()扩容方法主要做了三件事(这里这里重点讲前两件事,第三件事在下文的“三、2.”中讲):
    ①计算新容量(新桶) newCap 和新阈值 newThr;
    ②根据计算出的 newCap 创建新的桶数组table,并对table做初始化;
    ③将键值对节点重新放到新的桶数组里;
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
   final Node<K,V>[] resize() { //扩容
     
//---------------- -------------------------- 1.计算新容量(新桶) newCap 和新阈值 newThr。 ---------------------------------
  
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//看容量是否已初始化
        int oldThr = threshold;//下次扩容要达到的阈值。threshold(阈值) = capacity * loadFactor。
        int newCap, newThr = 0;
        if (oldCap > 0) {//容量已初始化过了:检查容量和阈值是否达到上限《==========
            if (oldCap >= MAXIMUM_CAPACITY) {//oldCap >= 2^30,已达到扩容上限,停止扩容
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
       // newCap < 2^30 && oldCap > 16,还能再扩容:2倍扩容
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 扩容:阈值*2。(注意:阈值是有可能越界的)
        }
        //容量未初始化 && 阈值 > 0。
        //【啥时会满足层判断:使用HashMap(int initialCapacity, float loadFactor)或 HashMap(int initialCapacity)构造函数实例化HashMap时,threshold才会有值。】
        else if (oldThr > 0)
            newCap = oldThr;//初始容量设为阈值
        else { //容量未初始化 && 阈值 <= 0 :
            //【啥时会满足这层判断:①使用无参构造函数实例化HashMap时;②在“if (oldCap > 0)”判断层newThr溢出了。】
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {//什么情况下才会进入这个判断框:前面执行了else if (oldThr > 0),并没有为newThr赋值,就会进入这个判断框。
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
         
//------------------------------------------------------2.扩容:------------------------------------------------------------------
         
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//扩容
        table = newTab;
         
//--------------------------------------------- 3.将键值对节点重新放到新的桶数组里。------------------------------------------------
         
        ……//此处源码见下文“二、2.”
 
        return newTab;
    }
  通过resize()扩容方法的源码可以知道:每次扩容,都是将容量扩大一倍,所以新容量依旧是2的n次幂。如oldCap是16的话,那么newCap则为32。
  通过上面三点可以确定,不论是默认初始容量,还是自定义容量大小,又或者是扩容后的容量,都必须保证一定是2的n次幂。
二、为什么HashMap的容量一定要是2的n次幂?或者说,保证“HashMap的容量一定是2的n次幂”有什么好处?
  原因有两个:
  1.关系到元素在桶中的位置计算问题:
  简单来讲,一个元素放到哪个桶中,是通过 “hash % capacity” 取模运算得到的余数来确定的(注:“元素的key的哈希值”在本文统一简称为“hash”)。
  hashMap用另一种方式来替代取模运算——位运算:(capacity - 1) & hash。这种运算方式为什么可以得到跟取模一样的结果呢? 答案是capacity是2的N次幂。(计算机做位运算的效率远高于做取模运算的效率,测试见:https://www.cnblogs.com/laipimei/p/11316812.html
  证明取模和位运算结果的一致性:
  

 

  2.关系到扩容后元素在newCap中的放置问题:
  扩容后,如何实现将oldCap中的元素重新放到newCap中?
  我们不难想到的实现方式是:遍历所有Node,然后重新put到新的table中, 中间会涉及计算新桶位置、处理hash碰撞等处理。这里有个不容忽视的问题——哈希碰撞。在元素put进桶中时,就已经处理过了哈希碰撞问题:哈希值一样但通过equals()比较确定内容不同的元素,会在同一个桶中形成链表,链表长度 >=8 时将链表转为红黑树;扩容时,需要重新处理这些元素的哈希碰撞问题,如果数据量一大.......要完……
  jdk1.8用了优雅又高效的方式来处理扩容后元素的放置问题,下面我们一起来看看jdk1.8到底是怎么做的。
  2.1 先看jdk1.8源码实现:
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
final Node<K,V>[] resize() { //扩容方法
     
//---------------- -------------------------- 1.计算新容量(新桶) newCap 和新阈值 newThr: -------------------------------------------
  
        …… //此处源码见前文“一、3.”
         
//---------------------------------------------------------2.扩容:------------------------------------------------------------------
         
        …… //此处源码见前文“一、3.”
         
//--------------------------------------------- 3.将键值对节点重新放到新的桶数组里:------------------------------------------------
         
        if (oldTab != null) {//容量已经初始化过了:
            for (int j = 0; j < oldCap; ++j) {//一个桶一个桶去遍历,j 用于记录oldCap中当前桶的位置
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {//当前桶上有节点,就赋值给e节点
                    oldTab[j] = null;//把该节点置为null(现在这个桶上什么都没有了)
                    if (e.next == null)//e节点后没有节点了:在新容器上重新计算e节点的放置位置《===== ①桶上只有一个节点
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)//e节点后面是红黑树:先将红黑树拆成2个子链表,再将子链表的头节点放到新容器中《===== ②桶上是红黑树
                        ((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) {//“定位值等于0”的为一组:
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {//“定位值不等于0”的为一组:
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
 
              //将分好的子链表放到newCap中:
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;//原链表在oldCap的什么位置,“定位值等于0”的子链表的头节点就放到newCap的什么位置
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead; //“定位值不等于0”的子节点的头节点在newCap的位置 = 原链表在oldCap中的位置 + oldCap
                        }
                    }
                }
            }
        }
        return newTab;
    }
  2.2 深入分析(含图解)
  ① 如果桶上只有一个节点(后面即没链表也没树):元素直接做 “hash & (newCap - 1)” 运算,根据结果将元素节点放到newCap的相应位置;
  ②如果桶上是链表:
  将链表上的所有节点做 “hash & oldCap” 运算(注意,这里oldCap没有-1),会得到一个定位值(“定位值”这个名字是我自己取的,为了更好理解该值的意义)。定位值要么是“0”,要么是“小于capacity的正整数”!这是个规律,之所以能得此规律和capacity取值一定是2的n次幂有直接关系,如果容量不是2的n次幂,那么定位值就不再要么是“0”,要么是“小于capacity的正整数”,它还有可能是其他的数;
  根据定位值的不同,会将链表一分为二得到两个子链表,这两个子链表根据各自的定位值直接放到newCap中:
    子链表的定位值 == 0: 则链表在oldCap中是什么位置,就将子链表的头节点直接放到newCap的什么位置;
    子链表的定位值 == 小于capacity的正整数:则将子链表的头节点放到newCap的“oldCap + 定位值”的位置;
  这么做的好处:链表在被拆分成两个子链表前就已经处理过了元素的哈希碰撞问题,子链表不用重新处理哈希碰撞问题,可以直接将头节点直接放到newCap的合适的位置上,完成 “扩容后将元素放到newCap”这一工作。正因为如此,大大提高了jdk1.8的HashMap的扩容效率。
  下面将通过画图的形式,进一步理解HashMap到底是怎么将元素放到newCap中的。
  前面我们说了jdk1.8的HashMap元素放到哪个桶中哪个位置,是通过计算 “(capacity - 1) & hash” 得到的余数来确定的。现在有四个元素,哈希值分别为35、27、19、43,当“容量 = 8”时,计算所得余数都等于3,所以这4个元素会被放到 table[3] 的位置,如下图所示:
  

 

进行一次扩容后,现在容量 = 16,再次计算“(capacity - 1) & hash”后,这四个元素在newCap中的位置会有所变化:要么在原位置,要么在“oldCap + 原位置”;也就是说这四个元素被分成了两组。如下图所示:
  

 

下面我们不用 “(capacity - 1) & hash” 的方式来放置元素,而是根据jdk1.8中HashMap.resize()扩容方法来放置元素:先通过 “hash & oldCap” 得到定位值,再根据定位值同样能将链表一分为二(见证奇迹的时候到了):
    “定位值 = 0”的为一组,这组元素就是前面将容量从8扩到16后,通过“(newCap - 1) & hash” 计算确定 “放回原位置” 的那些元素;
    “定位值 != 0”的为一组,这组元素就是扩容后,确定 “oldCap + 原位置”的那些元素。 如下图所示:
    

 

再将这两组元素节点分别连接成子链表:loHead是 “定位值 == 0” 的子链表的头节点;hiHead是 “定位值 != 0” 的子链表的头节点。如下图所示:
  

 

最后,将子链表的头节点loHead放到newCap中,位置和在oldCap中的原位置一致;将另一个子链表的头节点hiHead放到newCap的“oldCap + 原位置”上。到这里HashMap就完成了扩容后将元素重新放到newCap中的工作了。如下图所示:
  

 

到这里其实我们已经把 “容量一定是2的n次幂是 提高扩容后将元素重新放到newCap中的效率 的前提”解释完了,现在还有一个小小的问题——通过定位值将链表一分为二,会分得均匀吗?如果分得很不均匀会怎么样?
  众所周知,要想HashMap的查询速度快,那就得尽量做到让元素均匀地散落到每个桶里。将链表平均分成两个子链表,就意味着让元素更均匀地放到桶中了,增加了元素散列性,从而提高了元素的查找效率。那jdk1.8又是如何将链表分得更平均的呢?这关系到两点:①元素的哈希值更随机、散列;②通过“hash & oldCap”中的oldCap再次增加元素放置位置的随机性。第①点和哈希算法的实现直接相关,这里不细说;第②点的意思如下:
  以 “capacity = 8” 为例,下面这些都是当 “容量 = 8” 时放在table[3]位置上的元素的hash值。扩容时做“hash & oldCap” 运算,通过下图我们可以发现,oldCap所在的位上(即倒数第4位),元素的hash值在这个位是0还是1具有随机性。
  

 

  也就是说,jdk1.8在元素通过哈希算法使hash值已经具有随机性的前提下,再做了一个增加元素放置位置随机性的运算。
  ③如果桶上是红黑树:
  将红黑树重新放到newCap中的逻辑和将链表重新放到newCap的的逻辑差不多。不同之处在于,重新放后,会将红黑树拆分成两条由 TreeNode 组成的子链表:
    此时,如果子链表长度 <= UNTREEIFY_THRESHOLD(即 <= 6 ),则将由 TreeNode组成的子链表 转换成 由Node组成的普通子链表,然后再根据定位值将子链表的头节点放到newCap中;
    否则,根据条件重新将“由 TreeNode 组成的子链表”重新树化,然后再根据定位值将树的头节点放到newCap中。
  本文不对“HashMap扩容时红黑树在newCap中的重新放置”做详细解释,后面我会再写一篇有关《红黑树转回链表的具体时机》的博文,在这篇博文中会做详细的源码解析。
一言蔽之:jdk1.8 HashMap的容量一定要是2的n次幂,是为了提高“计算元素放哪个桶”的效率,也是为了提高扩容效率(避免了扩容后再重复处理哈希碰撞问题)。 
更多技术资讯可关注:itheimaGZ获取

为什么jdk1.8 HashMap的容量一定要是2的n次幂的更多相关文章

  1. jdk1.8 HashMap底层数据结构:深入解析为什么jdk1.8 HashMap的容量一定要是2的n次幂

    前言 1.本文根据jdk1.8源码来分析HashMap的容量取值问题: 2.本文有做 jdk1.8 HashMap.resize()扩容方法的源码解析:见下文“一.3.扩容:同样需要保证扩容后的容量是 ...

  2. HashMap的容量大小增长原理(JDK1.6/1.7/1.8)

    . 前言 HashMap的容量大小会根据其存储数据的数量多少而自动扩充,即当HashMap存储数据的数量到达一个阈值(threshold)时,再往里面增加数据,便可能会扩充HashMap的容量. 可能 ...

  3. JDK1.8 HashMap源码分析

      一.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时 ...

  4. 关于HashMap初始化容量问题

    使用阿里云代码规范插件扫描后出现以下提示: hashmap should set a size when initalizing,即hashmap应该在初始化时设置一个大小 在网上搜到一篇讲解(htt ...

  5. JDK1.8 HashMap$TreeNode.balanceInsertion 红黑树平衡插入

    红黑树介绍 1.节点是红色或黑色. 2.根节点是黑色. 3.每个叶子节点都是黑色的空节点(NIL节点). 4 每个红色节点的两个子节点都是黑色.(从每个叶子到根的所有路径上不能有两个连续的红色节点) ...

  6. JDK1.8 HashMap$TreeNode.rotateLeft 红黑树左旋

    红黑树介绍 1.节点是红色或黑色. 2.根节点是黑色. 3.每个叶子节点都是黑色的空节点(NIL节点). 4 每个红色节点的两个子节点都是黑色.(从每个叶子到根的所有路径上不能有两个连续的红色节点) ...

  7. java基础 - 什么是hashmap的负载因子,hashmap的容量(即桶个数)为什么是2的幂次

    HashMap的负载因子是指,比如容量为16,负载因子为0.75,则当HashMap的元素个数达到16*0.75=12时,触发扩容.(16和0.75是初始默认的容量和负载因子). HashMap的容量 ...

  8. 为什么要指定HashMap的容量?HashMap指定容量初始化后,底层Hash数组已经被分配内存了吗?

    为什么要指定HashMap的容量? 首先创建HashMap时,指定容量比如1024后,并不是HashMap的size不是1024,而是0,插入多少元素,size就是多少: 然后如果不指定HashMap ...

  9. HashMap初始化容量过程

    集合是Java开发日常开发中经常会使用到的,而作为一种典型的K-V结构的数据结构,HashMap对于Java开发者一定不陌生.在日常开发中,我们经常会像如下方式以下创建一个HashMap: Map&l ...

随机推荐

  1. JavaScript—飞机大战

    今天来写个游戏,飞机大战 1,布局 2,思路 1,动态创建自己的飞机 让它在规定的区域,跟着鼠标运动. 2,在自己飞机的上方,间隔1s生成子弹.子弹往上移动 当top:0 子弹消失 3,每隔1s 产生 ...

  2. vim 马哥

    VIM编辑器 编辑模式   默认 输入模式   i 末行模式   : vim +# file   #打开文件后直接跳到第#行 vim + file   直接跳到尾行 vim  +/关键字    跳转到 ...

  3. C#-类型转换和引用转换

    对象的引用可以被: 隐式地向上转换 显示的向下转换 向上转换 向上转换是指一个从一个基类指向一个子类: House house = new House(); Asset asset = house; ...

  4. CodeForces 382B 数学推导

    这个题目题意简单,但是TLE得哭哭的... 输入 a b w x c五个数,最终要使得c<=a, 每一秒可以进行一个操作,如果b>=x,则 b=b-x,同时 c--;如果b<x,则a ...

  5. LeetCode——787. K 站中转内最便宜的航班

    有 n 个城市通过 m 个航班连接.每个航班都从城市 u 开始,以价格 w 抵达 v. 现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到从 src 到 dst 最多经过 ...

  6. 复杂分布式架构下的计算治理之路:计算中间件 Linkis

    前言 在当前的复杂分布式架构环境下,服务治理已经大行其道.但目光往下一层,从上层 APP.Service,到底层计算引擎这一层面,却还是各个引擎各自为政,Client-Server 模式紧耦合满天飞的 ...

  7. python加速

    之前一直用 conda版python, 发现可以直接装intel的numpy了. https://software.intel.com/en-us/articles/installing-the-in ...

  8. 01 语言基础+高级:1-3 常用API第一部分_day08【String类、static、Arrays类、Math类】

    day08[String类.static.Arrays类.Math类] String类static关键字Arrays类Math类 教学目标能够使用String类的构造方法创建字符串对象能够明确Stri ...

  9. POJ 3585 Accumulation Degree【换根DP】

    传送门:http://poj.org/problem?id=3585 题意:给定一张无根图,给定每条边的容量,随便取一点使得从这个点出发作为源点,发出的流量最大,并且输出这个最大的流量. 思路:最近开 ...

  10. Python笔记_第二篇_面向过程_第二部分_4.常用模块的简单使用_import语句的解释

    1. import语句.from...import语句.from...import*语句 解释:注意一定要在体同一级目录下 1.1 引入模块 格式:import module[,module2,... ...