HashMap类的申明

HashMap的定义如下:

  1. public class HashMap<K,V> extends AbstractMap<K,V>
  2. implements Map<K,V>, Cloneable, Serializable {}

HashMap是一个散列表,用于存储key-value形式的键值对。

从源码的定义中可以看到HashMap继承了AbstractMap抽象类而且也实现了Map<K,V>接口,AbstractMap类本身也继承了Map<K,V>接口,Map接口定义了一些map数据结构的基本操作, AbstractMap提供了Map接口的一些默认实现。

HashMap实现了Cloneable接口和Serializable接口,这两个接口本身并没有定义方法,属于申明式接口,允许hashmap进行克隆和序列化。

另外,HashMap不是线程安全的,如果需要使用线程安全的HashMap,可以使用Collections类中的synchronizedMap方法来获得线程安全的HashMap:

Map map = Collections.synchronizedMap(new HashMap());

HashMap主要字段属性说明

  1. //hashmap的初始容量:16
  2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  3.  
  4. //hashmap的最大容量,hashmap的容量必须是2的指数值
  5. static final int MAXIMUM_CAPACITY = 1 << 30;
  6.  
  7. //默认填充因子
  8. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  9.  
  10. //链表转换为树的阀值:如果一个桶中的元素个数超过 TREEIFY_THRESHOLD=8 ,就使用红黑树来替换链表,从而提高速度
  11. static final int TREEIFY_THRESHOLD = 8;
  12.  
  13. //树还原为链表的阈值:扩容时桶中元素小于UNTREEIFY_THRESHOLD = 6,则把树形的桶元素还原为链表结构
  14. static final int UNTREEIFY_THRESHOLD = 6;
  15.  
  16. //哈希表的最小树形化容量:当哈希表中的容量大于这个值MIN_TREEIFY_CAPACITY = 64时,哈希表中的桶才能进行树形化,否则桶中元素过多时只会扩容,并不会进行树形化, 为了避免扩容和树形化选择的冲突,这个值不能小于4* TREEIFY_THRESHOLD = 32
  17. static final int MIN_TREEIFY_CAPACITY = 64;
  18.  
  19. //hashmap用于存储数据的Node数组,长度是2的指数值
  20. transient Node<K,V>[] table;
  21.  
  22. //保存entrySet返回的结果
  23. transient Set<Map.Entry<K,V>> entrySet;
  24.  
  25. //hashmap中键值对个数
  26. transient int size;
  27.  
  28. //hashmap对象修改计数器
  29. transient int modCount;
  30.  
  31. // threshold=容量*装载因子,代表目前占用数组长度的最大值,用于判断是否需要扩容
  32. int threshold;
  33.  
  34. //装载因子,用来衡量hashmap装载数据程度,默认值为EFAULT_LOAD_FACTOR = 0.75f,装载因子计算方法size/capacity
  35. final float loadFactor;

很多资料上都会提到JDK1.8之前hashmap的通过数组+链表的数据结构实现的,这样在hash值大量冲突时hashmap是通过一个长长的链表来存储的,JDK1.8开始,hashmap采用数组+链表+红黑树组合数据结构来实现,链表和红黑树将会按一定策略互相转换,JDK1.8开始,hashmap的存储结构大致如下:

回顾一下关于红黑树的定义:

1. 每个结点或是红色的,或是黑色的
      2. 根节点是黑色的
      3. 每个叶结点(NIL)是黑色的
      4. 如果一个节点是红色的,则它的两个儿子都是黑色的
      5. 对于每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑色结点

HashMap部分方法析

构造函数

  • 无参数构造函数:设置装载因子初始值0.75
  1. public HashMap() {
  2. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  3. }
  • HashMap(int initialCapacity)  :指定初始容量
  1. public HashMap(int initialCapacity) {
  2. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  3. }
  • HashMap(int initialCapacity)  :指定初始容量和装载因子
  1. public HashMap(int initialCapacity, float loadFactor) {
  2.   //初始容量校验
  3. if (initialCapacity < 0)
  4. throw new IllegalArgumentException("Illegal initial capacity: " +
  5. initialCapacity);
  6.   //校验初始容量不能超过hashmap最大容量:2的30次方
  7. if (initialCapacity > MAXIMUM_CAPACITY)
  8. initialCapacity = MAXIMUM_CAPACITY; //初始化为最大容量
  9.   //校验装载因子
  10. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  11. throw new IllegalArgumentException("Illegal load factor: " +
  12. loadFactor);
  13. this.loadFactor = loadFactor;
  14. this.threshold = tableSizeFor(initialCapacity);
  15. }
  16.   //根据根据初始化参数initialCapacity 返回大于等于该值得最小 2的指数值 作为初始容量
  17. static final int tableSizeFor(int cap) {
  18. int n = cap - 1;
  19. n |= n >>> 1;
  20. n |= n >>> 2;
  21. n |= n >>> 4;
  22. n |= n >>> 8;
  23. n |= n >>> 16;
  24. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  25. }

这个构造函数可以发现初始化hashmap的容量并不是随意指定多少就初始化多少,内部根据传入的容量值做了转换,严格的将hashmap的初始容量转换成的2的指数值,比如我们初始化一个new HashMap(25),实际初始化处来的容量是32,相当于new HashMap(32)

  • HashMap(Map<? extends K, ? extends V> m):初始化一个hashmap,使用默认加载因子0.75,并将hashmap参数值复制到新创建的hashmap对象中。
  1. public HashMap(Map<? extends K, ? extends V> m) {
  2. this.loadFactor = DEFAULT_LOAD_FACTOR;
  3. putMapEntries(m, false);
  4. }

put(K key, V value)

向hashmap中添加健值对

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4.  
  5. //hash:hashkey
  6. //key value :键值对
  7. //onlyIfAbsent:为true则不修改已存在的value
  8. //evict:返回被修改的value
  9. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  10. boolean evict) {
  11.  
  12. Node<K,V>[] tab;
  13. Node<K,V> p;
  14. int n, i;
  15.   //如果table为null或者空,则进行resize扩容
  16. if ((tab = table) == null || (n = tab.length) == 0)
  17. //执行resize扩容,内部将初始化table和threshold
  18. n = (tab = resize()).length;
  19.   //如果对应索引处没有Node,则新建Node并放到table里面
  20. if ((p = tab[i = (n - 1) & hash]) == null)
  21. tab[i] = newNode(hash, key, value, null); //tab[i]==null的情况,直接新创建节点并赋值给tab[i]
  22. else {
  23.   //else的情况表示tab[i]不为null
  24. Node<K,V> e;
  25.      K k;
  26.     //1:hash值与tab[i]的hash值相等且key也相等,那么覆盖该节点的value域
  27. if (p.hash == hash &&
  28. ((k = p.key) == key || (key != null && key.equals(k))))
  29. e = p;//暂存tab[i]的节点p到临时变量e
  30.    //2:判断tab[i]是否是红黑树
  31. else if (p instanceof TreeNode)
  32. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//添加到树形结构中
  33. else {
  34.     //3:不是红黑树 且不是第1中情况,即:hash值一致,但是key不一致,那么需要将新的key-value添加到链表末尾
  35. for (int binCount = 0; ; ++binCount) {
  36. if ((e = p.next) == null) {
  37.             //添加到链表末尾
  38. p.next = newNode(hash, key, value, null);
  39.             //如果该节点的链表长度大于8,则需要将链表转换为红黑树
  40. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  41. treeifyBin(tab, hash);
  42. break;
  43. }
  44. //如果key已经存在该链表中,直接break,执行后续更新逻辑
  45. if (e.hash == hash &&
  46. ((k = e.key) == key || (key != null && key.equals(k))))
  47. break;
  48. p = e;
  49. }
  50. }
  51. if (e != null) { // existing mapping for key
  52. V oldValue = e.value;
  53.       /hash值和key相等的情况下,更新value
  54. if (!onlyIfAbsent || oldValue == null)
  55. e.value = value;
  56. //
  57. afterNodeAccess(e);
  58.       //返回旧的value值
  59. return oldValue;
  60. }
  61. }
  62.   //修改次数自增
  63. ++modCount;
  64.     //判断是否需要再次扩容
  65. if (++size > threshold)
  66. resize();
  67. //
  68. afterNodeInsertion(evict);
  69. return null;
  70. }

上面的代码即便加了注释,看上去也不是很清晰,csdn有篇文章分析了hashmap的这个方法,并给出了一个流程图,逻辑很清晰:

图片来源:https://blog.csdn.net/lianhuazy167/article/details/66967698

上面put方法的实现中用到了一个很重要的方法resize(),这个方法的作用是当hashmap集合中的元素已经超过最大承载容量时,则对hashmap进行容量扩充。最大装载容量threshold=capacity*loadFactor,这个值一般小于数组的长度,下面看一下这个方法的实现过程:

  1. //初始化或者是扩展table的容量,table的容量时按照2的指数增长的,当扩大table容量时,元素的hash值以及位置可能发生改变
  2. final Node<K,V>[] resize() {
  3. Node<K,V>[] oldTab = table;
  4.   //计算当前哈希表 table数组长度
  5. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6.   //当前阈值(装载容量=数组长度*装载因子)
  7. int oldThr = threshold;
  8. int newCap, newThr = 0;
  9.   //如果table数组长度大于0
  10. if (oldCap > 0) {
  11.   //table数组长度大于等于hashmap默认的最大值: 2的30次方
  12. if (oldCap >= MAXIMUM_CAPACITY) {
  13.       //扩充为为int型最大值
  14. threshold = Integer.MAX_VALUE;
  15. return oldTab;
  16. }
  17.     //如果table数据长度>=初始化长度(16) 而且 扩展1倍也小于默认最大长度:2的30次方
  18. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  19. oldCap >= DEFAULT_INITIAL_CAPACITY)
  20.     // threshold 阈值扩大一倍
  21. newThr = oldThr << 1; // double threshold
  22. }
  23. //如果原先的装载容量>0,直接将新容量赋值为 原先的装载容量oldThr->oldThreshold
  24. else if (oldThr > 0) // initial capacity was placed in threshold
  25. newCap = oldThr;
  26. else { // zero initial threshold signifies using defaults
  27.     //原先的阈值oldThr< =0 而且table长度也=0,这说明hashmap还未初始化,执行初始化
  28. newCap = DEFAULT_INITIAL_CAPACITY;//数组长度赋值16
  29. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //
  30. }
  31.   //计算新的阈值上限
  32. if (newThr == 0) {
  33. float ft = (float)newCap * loadFactor;
  34. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  35. (int)ft : Integer.MAX_VALUE);
  36. }
  37.     //更新为新的阈值
  38. threshold = newThr;
  39.     //重新分配table容量
  40. @SuppressWarnings({"rawtypes","unchecked"})
  41. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  42. table = newTab;
  43.   //把原先table中的复制到新的table中
  44. if (oldTab != null) {
  45. for (int j = 0; j < oldCap; ++j) {
  46. Node<K,V> e;
  47. if ((e = oldTab[j]) != null) {
  48. oldTab[j] = null;
  49. if (e.next == null)
  50. newTab[e.hash & (newCap - 1)] = e;
  51. else if (e instanceof TreeNode)
  52. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  53. else { // preserve order
  54. Node<K,V> loHead = null, loTail = null;
  55. Node<K,V> hiHead = null, hiTail = null;
  56. Node<K,V> next;
  57. do {
  58. next = e.next;
  59. if ((e.hash & oldCap) == 0) {
  60. if (loTail == null)
  61. loHead = e;
  62. else
  63. loTail.next = e;
  64. loTail = e;
  65. }
  66. else {
  67. if (hiTail == null)
  68. hiHead = e;
  69. else
  70. hiTail.next = e;
  71. hiTail = e;
  72. }
  73. } while ((e = next) != null);
  74. if (loTail != null) {
  75. loTail.next = null;
  76. newTab[j] = loHead;
  77. }
  78. if (hiTail != null) {
  79. hiTail.next = null;
  80. newTab[j + oldCap] = hiHead;
  81. }
  82. }
  83. }
  84. }
  85. }
  86. return newTab;
  87. }

Hashmap的扩容机制主要实现步骤:

  1. 如果当前数组为空,则初始化当前数组(长度16,装载因子0.75)

  2. 如果当前数组不为空,则将当前数组容量扩大一倍,同时将阈值(threshold)也扩大一倍,然后将之前table素组中值全部复制到新的table中

get(Object key)

通过key获取对应的value,实现逻辑:根据key计算hash值,通过hash值和key从hashmap中检索出唯一的结果并返回。

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. return (e = getNode(hash(key), key)) == null ? null : e.value;
  4. }
  5. //hash:key对应的hash值
  6. //key:键值对的key
  7. final Node<K,V> getNode(int hash, Object key) {
  8. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  9.  
  10. if ((tab = table) != null && (n = tab.length) > 0 &&
  11. (first = tab[(n - 1) & hash]) != null) { // tab[(n - 1) & hash]
  12.     // 根据hash值计算出table中的位置,如果该位置第一个节点 key 和 hash值都和传递进来的参数相等,则返回该Node
  13. if (first.hash == hash && // always check first node
  14. ((k = first.key) == key || (key != null && key.equals(k))))
  15. return first;
  16.       //该键值对在hash表(n - 1) & hash索引处,但是不是第一个节点,多以遍历该链表(也可能是红黑树),不管是链表还是树,顺藤摸瓜就对了
  17. if ((e = first.next) != null) {
  18.         //如果是红黑树,则遍历树型结构
  19. if (first instanceof TreeNode)
  20. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  21.       //不是树,遍历链表
  22. do {
  23. if (e.hash == hash &&
  24. ((k = e.key) == key || (key != null && key.equals(k))))
  25. return e;
  26. } while ((e = e.next) != null);
  27. }
  28. }
  29. return null;
  30. }
  31.  
  32. //计算hash值
  33. static final int hash(Object key) {
  34. int h;
  35. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  36. }

get方法逻辑并没有什么难懂得地方,但是过程中有两个地方需要额外注意一下:

  1. tab[(n - 1) & hash]): 根据hash值计算元素位置,其中n为hashmap中table数组长度,这里使用(n-1)&hash的方式计算索引位置,简单解释一下这个含义,hashmap中数组的大小总是2的指数值,这种特殊的情况之下(n-1)&hash等同于hash%n取模运算结果,并且使用(n-1)&hash位运算的方式效率上也高于取模运算。

  2. hash(key):计算hash值,这个函数并不是直接通过hashCode()获取hash值,而是做了一步位运算(h = key.hashCode()) ^ (h >>> 16),即将hashcode的高16为与低16位异或运算,为什么这么做呢?因为hashcode()返回的是一个32位的int类型数值,将该数值的高16位与低16位做异或运算主要是想让高位数据参与运算,增加hash值得随机性。

JDK源码分析 – HashMap的更多相关文章

  1. 【JDK】JDK源码分析-HashMap(2)

    前文「JDK源码分析-HashMap(1)」分析了 HashMap 的内部结构和主要方法的实现原理.但是,面试中通常还会问到很多其他的问题,本文简要分析下常见的一些问题. 这里再贴一下 HashMap ...

  2. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...

  3. JDK源码分析-HashMap

    一.HashMap的内部属性 1.1 成员变量 1.1.1 size: HashMap包含的KV键值对的数量,也就是我们通常调用Map.size()方法的返回值 public int size() { ...

  4. 【JDK】JDK源码分析-LinkedHashMap

    概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...

  5. JDK 源码分析(4)—— HashMap/LinkedHashMap/Hashtable

    JDK 源码分析(4)-- HashMap/LinkedHashMap/Hashtable HashMap HashMap采用的是哈希算法+链表冲突解决,table的大小永远为2次幂,因为在初始化的时 ...

  6. JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

    JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...

  7. JDK源码分析(三)—— LinkedList

    参考文档 JDK源码分析(4)之 LinkedList 相关

  8. JDK源码分析(一)—— String

    dir 参考文档 JDK源码分析(1)之 String 相关

  9. JDK源码分析(2)LinkedList

    JDK版本 LinkedList简介 LinkedList 是一个继承于AbstractSequentialList的双向链表.它也可以被当作堆栈.队列或双端队列进行操作. LinkedList 实现 ...

随机推荐

  1. 大数据:spark集群搭建

    创建spark用户组,组ID1000 groupadd -g 1000 spark 在spark用户组下创建用户ID 2000的spark用户  获取视频中文档资料及完整视频的伙伴请加QQ群:9479 ...

  2. Leetcode---栈系列刷题(python3实现)----#20有效的括号

    给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效. 有效字符串需满足: 左括号必须用相同类型的右括号闭合. 左括号必须以正确的顺序闭合. 注意空字符串可被认 ...

  3. 虚拟机内安装Centos7步骤

    下面就来看看怎么安装centos7,首先就是要准备一个虚拟机了 简称VM,当然虚拟机的安装步骤,我也不再多说,我用的Workstation 15 Pro的版本,我们直接打开虚拟机,打开界面如下: 在安 ...

  4. c++拆分字符,不拆开中文

    // ConsoleApplication2.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <string.h&g ...

  5. python学习笔记(三):numpy基础

    Counter函数可以对列表中数据进行统计每一个有多少种 most_common(10)可以提取前十位 from collections import Counter a = ['q','q','w' ...

  6. 《Java核心技术36讲》阅读笔记:谈谈对Java平台的理解笔记

    1. 谈谈你对Java平台的理解. Java是一种面向对象的语言,最显著的特性有两个方面: 一个就是一次编译,到处运行(Write once, run anywhere),能够非常容易的获得跨平台能力 ...

  7. 20155321 2016-2017-2《Java程序设计》课堂实践项目

    20155321 2016-2017-2<Java程序设计>课堂实践项目 关于String类 split方法 charAt方法 项目题目: 模拟实现Linux下Sort -t : -k 2 ...

  8. 20155327 2016-2017-2 《Java程序设计》第一周学习总结

    20155327 2016-2017-2 <Java程序设计>第一周学习总结 教材学习内容总结 浏览教材,根据自己的理解每章提出一个问题 1.JAVA SE中JVM,JRE与JDK分别是什 ...

  9. uber司机 如何提高评分、接单率、成单率?

    接单率/成单率的解释 接单率计算方法为:成功接单的订单数 除以 系统派单的订单数. 成单率计算方法为:成功完成的订单数 除以 系统派单的订单数. 滴滴快车单单2.5倍,注册地址:http://www. ...

  10. RHCSA-EXAM 模拟题目

    参考答案:http://www.cnblogs.com/venicid/category/1088924.html 请首先按找以下要求配置考试系统: * Hostname: server0.examp ...