前言

之前周会技术分享,一位同事讲解了HashMap的源码,涉及到一些常量设计的目的,本文将谈谈这些常量为何这样设计,希望大家有所收获。

HashMap默认初始化大小为什么是1 << 4(16)

/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

HashMap默认初始化大小为什么是16,这里分两个维度分析,为什么是2的幂,为什么是16而不是8或者32。

默认初始化大小为什么定义为2的幂?

 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)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);

我们知道HashMap的底层数据结构是数组+链表/数组+红黑树,由以上方法,可以发现数组下标索引的定位公式是:i = (n - 1) & hash,当初始化大小n是2的倍数时, (n - 1) & hash等价于n%hash。定位下标一般用取余法,为什么这里不用取余呢?

  • 因为,与运算(&)比取余(%)运算效率高
  • 求余运算: a % b就相当与a-(a / b)*b 的运算。
  • 与运算: 一个指令就搞定

因此,默认初始化大定义为2的幂,就是为了使用更高效的与运算

默认初始化大小为什么是16而不是8或者32?

如果太小,4或者8,扩容比较频繁;如果太大,32或者64甚至太大,又占用内存空间

打个比喻,假设你开了个情侣咖啡厅,平时一般都是7,8对情侣来喝咖啡,高峰也就10对。那么,你是不是设置8个桌子就好啦,如果人来得多再考虑加桌子。如果设置4桌,那么就经常座位不够要加桌子,如果设置10桌或者更多,那么肯定占地方嘛。

默认加载因子为什么是0.75

    /**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

加载因子表示哈希表的填满程度,跟扩容息息相关。为什么不是0.5或者1呢?

如果是0.5,就是说哈希表填到一半就开始扩容了,这样会导致扩容频繁,并且空间利用率比较低。

如果是1,就是说哈希表完全填满才开始扩容,这样虽然空间利用提高了,但是哈希冲突机会却大了。可以看一下源码文档的解释:

 * <p>As a general rule, the default load factor (.75) offers a good
* tradeoff between time and space costs. Higher values decrease the
* space overhead but increase the lookup cost (reflected in most of
* the operations of the <tt>HashMap</tt> class, including
* <tt>get</tt> and <tt>put</tt>). The expected number of entries in
* the map and its load factor should be taken into account when
* setting its initial capacity, so as to minimize the number of
* rehash operations. If the initial capacity is greater than the
* maximum number of entries divided by the load factor, no rehash
* operations will ever occur.

翻译大概意思是:

作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了良好的权衡。负载因子数值越大,空间开销越低,但是会提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。

简言之, 负载因子0.75是冲突的机会空间利用率权衡的最后体现,也是一个程序员实验的经验值。

StackOverFlow有个回答这个问题的:

What is the significance of load factor in HashMap?



这个回答解释:一个bucket空和非空的概率为0.5,通过牛顿二项式等数学计算,得到这个loadfactor的值为log(2),约等于0.693

最后选择选择0.75,可能0.75是接近0.693的四舍五入数中,比较好理解的一个,并且默认容量大小16*0.75=12,为一个整数。

链表转换红黑树的阀值为什么是8

    /**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;

JDK8及以后的版本中,HashMap底层数据结构引入了红黑树。当添加元素的时候,如果桶中链表元素超过8,会自动转为红黑树。那么阀值为什么是8呢?请看HashMap的源码这段注释:

* 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

理想状态中,在随机哈希码情况下,对于默认0.75的加载因子,桶中节点的分布频率服从参数为0.5的泊松分布,即使粒度调整会产生较大方差。

由对照表,可以看到链表中元素个数为8时的概率非常非常小了,所以链表转换红黑树的阀值选择了8。

一个树的链表还原阈值为什么是6

/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;

上一小节分析,可以知道,链表树化阀值是8,那么树还原为链表为什么是6而不是7呢?这是为了防止链表和树之间频繁的转换。如果是7的话,假设一个HashMap不停的插入、删除元素,链表个数一直在8左右徘徊,就会频繁树转链表、链表转树,效率非常低下。

最大容量为什么是1 << 30

/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

HashMap为什么要满足2的n次方?

由第一小节(HashMap默认初始化大小为什么是1 << 4)分析可知,HashMap容量需要满足2的幂,与运算比取余运算效率高。只有容量是2的n次方时,与运算才等于取余运算。

tab[i = (n - 1) & hash]

为什么不是2的31次方呢?

我们知道,int占四个字节一个字节占8位,所以是32位整型,也就是说最多32位。那按理说,最大数可以向左移动31位即2的31次幂,在这里为什么不是2的31次方呢

实际上,二进制数的最左边那一位是符号位,用来表示正负的,我们来看一下demo代码:

        System.out.println(1<<30);
System.out.println(1<<31);
System.out.println(1<<32);
System.out.println(1<<33);
System.out.println(1<<34);

输出:

1073741824
-2147483648
1
2
4

所以,HashMap最大容量是1 << 30。

哈希表的最小树形化容量为什么是64

    /**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;

这是因为容量低于64时,哈希碰撞的机率比较大,而这个时候出现长链表的可能性会稍微大一些,这种原因下产生的长链表,我们应该优先选择扩容而避免不必要的树化。

参考与感谢

个人公众号

  • 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。
  • 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。

面试加分项-HashMap源码中这些常量的设计目的的更多相关文章

  1. 面试必备:HashMap源码解析(JDK8)

    1 概述 本文将从几个常用方法下手,来阅读HashMap的源码. 按照从构造方法->常用API(增.删.改.查)的顺序来阅读源码,并会讲解阅读方法中涉及的一些变量的意义.了解HashMap的特点 ...

  2. 面试加分项---HashMap底层实现原理

    想必大家都知道HashSet和HashMap之间的关系,HashSet是依赖于HashMap的,HashSet集合就是HashMap的key所组成的集合,我们都知道HashMap的value是可以重复 ...

  3. HashMap 源码详细分析(JDK1.8)

    一.概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值, ...

  4. 逐行解读HashMap源码

    [本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 一.写在前面 相 ...

  5. HashMap源码实现分析

    HashMap源码实现分析 一.前言 HashMap 顾名思义,就是用hash表的原理实现的Map接口容器对象,那什么又是hash表呢. 我们对数组都很熟悉,数组是一个占用连续内存的数据结构,学过C的 ...

  6. Java中的HashMap源码记录以及并发环境的几个问题

    HashMap源码简单分析: 1 一切需要从HashMap属性字段说起: /** The default initial capacity - MUST be a power of two. 初始容量 ...

  7. Java BAT大型公司面试必考技能视频-1.HashMap源码分析与实现

    视频通过以下四个方面介绍了HASHMAP的内容 一. 什么是HashMap Hash散列将一个任意的长度通过某种算法(Hash函数算法)转换成一个固定的值. MAP:地图 x,y 存储 总结:通过HA ...

  8. Java中HashMap源码分析

    一.HashMap概述 HashMap基于哈希表的Map接口的实现.此实现提供所有可选的映射操作,并允许使用null值和null键.(除了不同步和允许使用null之外,HashMap类与Hashtab ...

  9. java集合中的HashMap源码分析

    1.hashMap中的成员分析 transient Node<K,V>[] table; //为hash桶的数量 /** * The number of key-value mapping ...

随机推荐

  1. 【POJ - 3273】Monthly Expense (二分)

    Monthly Expense 直接上中文 Descriptions 给你一个长度为N的序列,现在要让你把他们切割成M份(所以每一份都是连续的),然后每一份都有一个和sum[i],其中最大的一个是ma ...

  2. Day01:JAVA开发环境

    下载JDK 首先我们需要下载java开发工具包JDK,下载地址:http://www.oracle.com/technetwork/java/javase/downloads/index.html,点 ...

  3. Placement_pools on Rados-GW

    The purpose of this test is to map a RadosGw Bucket to a specific Ceph pool. For exemple, if using a ...

  4. java 动手动脑7

    ---恢复内容开始--- 一.动手动脑:多层的异常捕获-1 阅读以下代码(CatchWho.java),写出程序运行结果: ArrayIndexOutOfBoundsException/内层try-c ...

  5. SpringMVC项目案例之---数据的获取与显示

    数据的获取与显示 (一)功能 1.对用户输入的数据进行获取 2.将获取的数据显示到页面 3.使用了SpringMVC技术的注解方式 4.使用了过滤器,处理中文乱码问题 5.在web.xml中设置了访问 ...

  6. 使用 PowerShell 远程管理

    要求 PowerShell 版本要求至少是2.0版本以上,目前PowerShell 2.0 支持最低的操作系统版本为Windows XP.本次操作使用的是 PowerShell 5.1 请使用管理员身 ...

  7. Spark 系列(五)—— Spark 运行模式与作业提交

    一.作业提交 1.1 spark-submit Spark 所有模式均使用 spark-submit 命令提交作业,其格式如下: ./bin/spark-submit \ --class <ma ...

  8. 在MAC终端下打开Finder:

    在Terminal中打开Finder: open . 在Finder中打开Terminal: 系统偏好设置 -> 键盘 -> 快捷键 -> 服务,勾选「新建位于文件夹位置的终端窗口」

  9. 用 bat 文件实现 excel 周报复制

     又要写周报???? 写周报就算了每次都要改这一大堆的日期,什么鬼嘛,最骚的我还总是有的忘记改....        作为一个正儿八经的程序员,固定每周某天干重复的一件事,哦~~  这是机器人 程序应 ...

  10. abp(net core)+easyui+efcore实现仓储管理系统——使用 WEBAPI实现CURD (十三)

    abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统——ABP总体介绍(一) abp(net core)+ ...