最近博主参加面试,发现自己对于Java的HashMap的扩容过程理解不足,故最近在此进行总结。

首先说明博主德Java为1.8版本

HashMap中的变量

首先要了解HashMap的扩容过程,我们就得了解一些HashMap中的变量:

  • Node<K,V>:链表节点,包含了key、value、hash、next指针四个元素
  • table:Node<K,V>类型的数组,里面的元素是链表,用于存放HashMap元素的实体
  • size:记录了放入HashMap的元素个数
  • loadFactor:负载因子
  • threshold:阈值,决定了HashMap何时扩容,以及扩容后的大小,一般等于table大小乘以loadFactor
 

HashMap的构造函数

HashMap的构造函数主要有四个,代码如下:
  1. public HashMap(int initialCapacity, float loadFactor) {
  2. ...
  3. this.loadFactor = loadFactor;
  4. this.threshold = tableSizeFor(initialCapacity);
  5. }
  6. public HashMap(int initialCapacity) {
  7. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  8. }
  9. public HashMap() {
  10. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  11. }
  12. public HashMap(Map<? extends K, ? extends V> m) {
  13. this.loadFactor = DEFAULT_LOAD_FACTOR;
  14. putMapEntries(m, false);
  15. }

其中主要有两种形式:

  • 直接拷贝别的HashMap的形式,在此不作讨论
  • 定义初始容量大小(table数组的大小,缺省值为16),定义负载因子(缺省值为0.75)的形式
值得注意的是,当我们自定义HashMap初始容量大小时,构造函数并非直接把我们定义的数值当做HashMap容量大小,而是把该数值当做参数调用方法tableSizeFor,然后把返回值作为HashMap的初始容量大小:
  1. /**
  2. * Returns a power of two size for the given target capacity.
  3. */
  4. static final int tableSizeFor(int cap) {
  5. int n = cap - 1;
  6. n |= n >>> 1;
  7. n |= n >>> 2;
  8. n |= n >>> 4;
  9. n |= n >>> 8;
  10. n |= n >>> 16;
  11. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  12. }

该方法会返回一个大于等于当前参数的2的倍数,因此HashMap中的table数组的容量大小总是2的倍数。

何时进行扩容?

HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put 方法插入元素之前,HashMap并不会去初始化或者扩容table:
  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  5. boolean evict) {
  6. Node<K,V>[] tab; Node<K,V> p; int n, i;
  7. if ((tab = table) == null || (n = tab.length) == 0)
  8. n = (tab = resize()).length;
  9. if ((p = tab[i = (n - 1) & hash]) == null)
  10. tab[i] = newNode(hash, key, value, null);
  11. else {
  12. ...
  13. }
  14. ++modCount;
  15. if (++size > threshold)
  16. resize();
  17. afterNodeInsertion(evict);
  18. return null;
  19. }

在putVal方法第8、9行我们可以看到,当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化

在putVal方法第16、17行我们可以看到,当添加完元素后,如果HashMap发现size(元素总数)大于threshold(阈值),则会调用resize方法进行扩容
 
在这里值得注意的是,在putVal方法第10行我们可以看到,插入元素的hash值是一个32位的int值,而实际当前元素插入table的索引的值为 :
  1. (table.size - 1)& hash

又由于table的大小一直是2的倍数,2的N次方,因此当前元素插入table的索引的值为其hash值的后N位组成的值

 

resize扩容

  1. final Node<K,V>[] resize() {
  2. Node<K,V>[] oldTab = table;
  3. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  4. int oldThr = threshold;
  5. int newCap, newThr = 0;
  6. if (oldCap > 0) {
  7. if (oldCap >= MAXIMUM_CAPACITY) {
  8. threshold = Integer.MAX_VALUE;
  9. return oldTab;
  10. }
  11. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  12. oldCap >= DEFAULT_INITIAL_CAPACITY)
  13. newThr = oldThr << 1; // double threshold
  14. }
  15. else if (oldThr > 0) // initial capacity was placed in threshold
  16. newCap = oldThr;
  17. else {               // zero initial threshold signifies using defaults
  18. newCap = DEFAULT_INITIAL_CAPACITY;
  19. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  20. }
  21. if (newThr == 0) {
  22. float ft = (float)newCap * loadFactor;
  23. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  24. (int)ft : Integer.MAX_VALUE);
  25. }
  26. threshold = newThr;
  27. @SuppressWarnings({"rawtypes","unchecked"})
  28. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  29. table = newTab;
  30. if (oldTab != null) {
  31. for (int j = 0; j < oldCap; ++j) {
  32. Node<K,V> e;
  33. if ((e = oldTab[j]) != null) {
  34. oldTab[j] = null;
  35. if (e.next == null)
  36. newTab[e.hash & (newCap - 1)] = e;
  37. else if (e instanceof TreeNode)
  38. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  39. else { // preserve order
  40. Node<K,V> loHead = null, loTail = null;
  41. Node<K,V> hiHead = null, hiTail = null;
  42. Node<K,V> next;
  43. do {
  44. next = e.next;
  45. if ((e.hash & oldCap) == 0) {
  46. if (loTail == null)
  47. loHead = e;
  48. else
  49. loTail.next = e;
  50. loTail = e;
  51. }
  52. else {
  53. if (hiTail == null)
  54. hiHead = e;
  55. else
  56. hiTail.next = e;
  57. hiTail = e;
  58. }
  59. } while ((e = next) != null);
  60. if (loTail != null) {
  61. loTail.next = null;
  62. newTab[j] = loHead;
  63. }
  64. if (hiTail != null) {
  65. hiTail.next = null;
  66. newTab[j + oldCap] = hiHead;
  67. }
  68. }
  69. }
  70. }
  71. }
  72. return newTab;
  73. }

从第15 ~ 20行可以看到,若threshold(阈值)不为空,table的首次初始化大小为阈值,否则初始化为缺省值大小16

 
当table需要扩容时,从第11 ~ 13行可以看到,扩容后的table大小变为原来的两倍,接下来就是进行扩容后table的调整:
假设扩容前的table大小为2的N次方,有上述put方法解析可知,元素的table索引为其hash值的后N位确定
那么扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位
因此,table中的元素只有两种情况:
  1. 元素hash值第N+1位为0:不需要进行位置调整
  2. 元素hash值第N+1位为1:调整至原索引的两倍位置
在resize方法中,第45行的判断即用于确定元素hashi值第N+1位是否为0:
  • 若为0,则使用loHead与loTail,将元素移至新table的原索引处
  • 若不为0,则使用hiHead与hiHead,将元素移至新table的两倍索引处
扩容或初始化完成后,resize方法返回新的table

Java HashMap的扩容的更多相关文章

  1. Java学习笔记(二二)——Java HashMap

    [前面的话] 早上起来好瞌睡哈,最近要注意一样作息状态.       HashMap好好学习一下. [定义] Hashmap:是一个散列表,它存储的内容是键值对(key——value)映射.允许nul ...

  2. 转:Java HashMap实现详解

    Java HashMap实现详解 转:http://beyond99.blog.51cto.com/1469451/429789 1.    HashMap概述:    HashMap是基于哈希表的M ...

  3. java HashMap的原理

    HashMap的数据结构: 在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外.HashMap实际 ...

  4. 自学Java HashMap源码

    自学Java HashMap源码 参考:http://zhangshixi.iteye.com/blog/672697 HashMap概述 HashMap是基于哈希表的Map接口的非同步实现.此实现提 ...

  5. 深入理解HashMap的扩容机制

    什么时候扩容: 网上总结的会有很多,但大多都总结的不够完整或者不够准确.大多数可能值说了满足我下面条件一的情况. 扩容必须满足两个条件: 1. 存放新值的时候当前已有元素的个数必须大于等于阈值 2. ...

  6. Java HashMap工作原理及实现

    Java HashMap工作原理及实现 2016/03/20 | 分类: 基础技术 | 0 条评论 | 标签: HASHMAP 分享到:3 原文出处: Yikun 1. 概述 从本文你可以学习到: 什 ...

  7. Java - HashMap 多线程安全解析

    HashMap多线程并发问题分析 多线程put后可能导致get死循环 从前我们的Java代码因为一些原因使用了HashMap这个东西,但是当时的程序是单线程的,一切都没有问题.后来,我们的程序性能有问 ...

  8. 十个问题带你了解和掌握java HashMap

    十个问题带你了解和掌握java HashMap 一.前言 本篇内容是源于 " 由阿里巴巴Java开发规约HashMap条目引发的故事",并在此基础上加了自己的对HashMap更多的 ...

  9. Java HashMap 源代码分析

    Java HashMap jdk 1.8 Java8相对于java7来说HashMap变化比较大,在hash冲突严重的时候java7会退化为链表,Java8会退化为TreeMap 我们先来看一下类图: ...

随机推荐

  1. React Native学习(九)—— 使用Flexbox布局

    本文基于React Native 0.52 Demo上传到Git了,有需要可以看看,写了新内容会上传的.Git地址 https://github.com/gingerJY/React-Native-D ...

  2. SpringCloud的EurekaClient : 客户端应用访问注册的微服务(有断路器场景)

    演示客户端应用如何访问注册在EurekaServer里的微服务 一.概念和定义 采用Ribbon或Feign方式访问注册到EurekaServer中的微服务.1.Ribbon实现了客户端负载均衡,2. ...

  3. greenplum集群某台机器磁盘占用100%处理方式

    一.问题描述 使用gpfdist往集群中导入大量数据, 一段时间后连接退出,集群无法连接 二.问题定位 使用如下命令查看: gpstate -s mdw-:gpadmin-[INFO]:- Segme ...

  4. 赛码网算法: 军训队列( python实现 )

    军训队列 题目描述某大学开学进行军训队列训练,将学生从一开始按顺序依次编号,并排成一行横队,训练的规则如下:从头开始一至二报数,凡报到二的出列剩下的依次向前靠拢,再从头开始进行一至三报数,凡报到三的出 ...

  5. Python之编码

    一.Python2与Python3的区别 1.从宏观上考虑,Python2重复代码太多,错误率高,不够规范.Python崇尚的是语言简洁.优美.清晰.Python3更加规范,重复代码少: 2.Pyth ...

  6. 彻底理解了call()方法,apply()方法和bind()方法

    javascript中的每一个作用域中都有一个this对象,它代表的是调用函数的对象.在全局作用域中,this代表的是全局对象(在web浏览器中指的是window).如果包含this的函数是一个对象的 ...

  7. python 进程 线程

    进程 线程 操作系统 为什么要有操作系统? 操作系统:操作系统是一个用来协调,管理和控制计算机硬件和软件资源的系统程序.位于底层硬件与应用软件之间 工作方式:向下管理硬件 向上提供接口 切换 1.出现 ...

  8. urllib.parse

    1 url分解 import urllib.parse result = urllib.parse.urlparse('http://www.baidu.com') print(result) 结果为 ...

  9. Paper藐小之处明察秋毫故时有物外之趣

    暂且针对第一篇叶脉提取的paper 插入图像的"图 N": 英 Times New Roman, 中 宋体, 10磅. 文末的引文: 两端对齐. 流程图框格内文字换行时, 忌: 将 ...

  10. 再谈angularJS数据绑定机制及背后原理—angularJS常见问题总结

    这篇是对angularJS的一些疑点回顾,是对目前angularJS开发的各种常见问题的整理汇总.如果对文中的题目全部了然于胸,觉得对整个angular框架应该掌握的七七八八了.希望志同道合的通知补充 ...