HashMap的知识点可以说在面试中经常被问到,是Java中比较常见的一种数据结构。所以这一篇就通过源码来深入理解下HashMap。

1 HashMap的底层是如何实现的?(基于JDK8)

1.1 HashMap的类结构和成员

  1. /**
  2. HashMap继承AbstractMap,而AbstractMap又实现了Map的接口
  3. */
  4. public class HashMap<K,V> extends AbstractMap<K,V>
  5. implements Map<K,V>, Cloneable, Serializable

从上面源码可以看出HashMap支持序列化和反序列化,而且实现了cloneable接口,能支持clone()方法复制一个对象。

1.1.1 HashMap源码中的几个成员属性

  1. //最小容量为16,且一定是2的幂次
  2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  3. //最大容量为2的30次方
  4. static final int MAXIMUM_CAPACITY = 1 << 30;
  5. // 默认加载因子
  6. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  7. //当某节点的链表长度大于8并且hash数组的容量达到64时,链表将会转换成红黑树
  8. static final int TREEIFY_THRESHOLD = 8;
  9. //当链表长度小于6时,红黑树将转换成链表
  10. static final int UNTREEIFY_THRESHOLD = 6;
  11. //链表变成红黑树的最小容量
  12. static final int MIN_TREEIFY_CAPACITY = 64;

从上面的源码可以看出,JDK1.8的HashMap实际上是由数组+链表+红黑树组成,在一定条件下链表会转换成红黑树。这里要谈一下默认加载因子为什么为0.75(3/4),加载因子也叫扩容因子,用来判断HashMap什么时候进行扩容。选择0.75的原因是为了平衡容量与查找性能:扩容因子越大,造成hash冲突的几率就越大,查找性能就会越低,反之扩容因子越小,所占容量就会越大。于此同时,负载因子为3/4的话,和capacity的乘积结果就可以是一个整数。

下面再看看hash数组中的元素

1.1.2 HashMap中的数组节点

​ hash数组一般称为哈希桶(bucket),结点在JDK1.7中叫Entry,在JDK1.8中叫Node。

  1. //1.8中Node实现entry的接口
  2. static class Node<K,V> implements Map.Entry<K,V> {
  3. //每个节点都会包含四个字段:hash、key、value、next
  4. final int hash;
  5. final K key;
  6. V value;
  7. Node<K,V> next;//指向下一个节点
  8. Node(int hash, K key, V value, Node<K,V> next) {
  9. this.hash = hash;
  10. this.key = key;
  11. this.value = value;
  12. this.next = next;
  13. }
  14. public final K getKey() { return key; }
  15. public final V getValue() { return value; }
  16. public final String toString() { return key + "=" + value; }
  17. //hash值是由key和value的hashcode异或得到
  18. public final int hashCode() {
  19. return Objects.hashCode(key) ^ Objects.hashCode(value);
  20. }
  21. public final V setValue(V newValue) {
  22. V oldValue = value;
  23. value = newValue;
  24. return oldValue;
  25. }
  26. public final boolean equals(Object o) {
  27. if (o == this)
  28. return true;
  29. //判断o对象是否为Map.Entry的实例
  30. if (o instanceof Map.Entry) {
  31. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  32. //再判断两者的key和value值是否相同
  33. if (Objects.equals(key, e.getKey()) &&
  34. Objects.equals(value, e.getValue()))
  35. return true;
  36. }
  37. return false;
  38. }
  39. }
  40. //这个是扰动函数,减少hash碰撞
  41. static final int hash(Object key) {
  42. int h;
  43. //将key的高16位与低16位异或(int是2个字节,32位)
  44. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  45. }

1.2 HashMap中的方法

1.2.1 查询方法

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. //将key值扰动后传入getNode函数查询节点
  4. return (e = getNode(hash(key), key)) == null ? null : e.value;
  5. }
  6. final Node<K,V> getNode(int hash, Object key) {
  7. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  8. //判断哈希表是否为空,第一个节点是否为空
  9. if ((tab = table) != null && (n = tab.length) > 0 &&
  10. (first = tab[(n - 1) & hash]) != null) {
  11. //从第一个节点开始查询,如果hash值和key值相等,则查询成功,返回该节点
  12. if (first.hash == hash && // always check first node
  13. ((k = first.key) == key || (key != null && key.equals(k))))
  14. return first;
  15. //查询下一个节点
  16. if ((e = first.next) != null) {
  17. //若该节点存在红黑树,则从红黑树中查找节点
  18. if (first instanceof TreeNode)
  19. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  20. //若该节点存在链表,循着链表查找节点
  21. do {
  22. if (e.hash == hash &&
  23. ((k = e.key) == key || (key != null && key.equals(k))))
  24. return e;
  25. } while ((e = e.next) != null);
  26. }
  27. }
  28. return null;
  29. }

1.2.2 新增方法

向哈希表中插入一个节点

  1. public V put(K key, V value) {
  2. //将扰动的hash值传入,调用putVal函数
  3. return putVal(hash(key), key, value, false, true);
  4. }
  5. //当参数onlyIfAbsent为true时,不会覆盖相同key的值value;当evict是false时,表示是在初始化时调用
  6. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  7. boolean evict) {
  8. Node<K,V>[] tab; Node<K,V> p; int n, i;
  9. //若哈希表为空,直接对哈希表进行扩容
  10. if ((tab = table) == null || (n = tab.length) == 0)
  11. n = (tab = resize()).length;
  12. //若当前节点为空,则直接在该处新建节点
  13. if ((p = tab[i = (n - 1) & hash]) == null)
  14. tab[i] = newNode(hash, key, value, null);
  15. else {//若当前节点非空,则说明发生哈希碰撞,再考虑是链表或者红黑树
  16. Node<K,V> e; K k;
  17. //如果与该节点的hash值和key值都相等,将节点引用赋给e
  18. if (p.hash == hash &&
  19. ((k = p.key) == key || (key != null && key.equals(k))))
  20. e = p;
  21. //如果p是树节点的实例,调用红黑树方法新增一个树节点e
  22. else if (p instanceof TreeNode)
  23. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  24. //若该节点后是链表
  25. else {
  26. for (int binCount = 0; ; ++binCount) {
  27. //遍历到链表末尾插入新节点
  28. if ((e = p.next) == null) {
  29. p.next = newNode(hash, key, value, null);
  30. //若插入节点后,链表节点数大于转变成红黑树的临界值(>=8)
  31. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  32. //将链表转换成红黑树
  33. treeifyBin(tab, hash);
  34. break;
  35. }
  36. //遍历过程中发现了key和hash值相同的节点,用e覆盖该节点
  37. if (e.hash == hash &&
  38. ((k = e.key) == key || (key != null && key.equals(k))))
  39. break;
  40. p = e;
  41. }
  42. }
  43. //对e节点进行处理
  44. if (e != null) {
  45. V oldValue = e.value;
  46. if (!onlyIfAbsent || oldValue == null)
  47. e.value = value;
  48. afterNodeAccess(e);
  49. return oldValue;
  50. }
  51. }
  52. //节点插入成功,修改modCount值
  53. ++modCount;
  54. //如果达到扩容条件,直接扩容
  55. if (++size > threshold)
  56. resize();
  57. afterNodeInsertion(evict);
  58. return null;
  59. }

1.2.3 扩容方法(非常重要)

  1. final Node<K,V>[] resize() {
  2. //当前的数组
  3. Node<K,V>[] oldTab = table;
  4. //当前的数组大小和阈值
  5. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6. int oldThr = threshold;
  7. //对新数组大小和阈值初始化
  8. int newCap, newThr = 0;
  9. //若当前数组非空
  10. if (oldCap > 0) {
  11. //若当前数组超过容量最大值,返回原数组不扩容
  12. if (oldCap >= MAXIMUM_CAPACITY) {
  13. threshold = Integer.MAX_VALUE;
  14. return oldTab;
  15. }
  16. //若当前数组低于阈值,直接在数组容量范围内扩大两倍
  17. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  18. oldCap >= DEFAULT_INITIAL_CAPACITY)
  19. newThr = oldThr << 1; // double threshold
  20. }
  21. //数组为空,且大于最小容量(数组初始化过)
  22. else if (oldThr > 0) // initial capacity was placed in threshold
  23. newCap = oldThr;
  24. //数组为空,且没有初始化
  25. else { // zero initial threshold signifies using defaults
  26. //初始化数组
  27. newCap = DEFAULT_INITIAL_CAPACITY;
  28. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  29. }
  30. //数组为空,且新的阈值为0
  31. if (newThr == 0) {
  32. //求出新的阈值(新数组容量*加载因子)
  33. float ft = (float)newCap * loadFactor;
  34. //判断新阈值是否越界,并做相应的赋值
  35. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  36. (int)ft : Integer.MAX_VALUE);
  37. }
  38. //阈值更新
  39. threshold = newThr;
  40. @SuppressWarnings({"rawtypes","unchecked"})
  41. //构建新的数组并赋值
  42. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  43. table = newTab;
  44. //若之前数组非空,将数据复制到新数组中
  45. if (oldTab != null) {
  46. //循环之前数组,将非空元素复制到新数组中
  47. for (int j = 0; j < oldCap; ++j) {
  48. Node<K,V> e;
  49. if ((e = oldTab[j]) != null) {
  50. oldTab[j] = null;
  51. //若循环到该节点是最后一个非空节点,直接赋值
  52. if (e.next == null)
  53. newTab[e.hash & (newCap - 1)] = e;
  54. //若发现该节点是树节点
  55. else if (e instanceof TreeNode)
  56. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  57. //若该节点后是链表
  58. else { // preserve order
  59. //定义现有数组的位置low,扩容后的位置high;high = low + oldCap
  60. Node<K,V> loHead = null, loTail = null;
  61. Node<K,V> hiHead = null, hiTail = null;
  62. Node<K,V> next;
  63. do {
  64. next = e.next;
  65. /*通过(e.hash & oldCap)来确定元素是否需要移动,
  66. e.hash & oldCap大于0,说明位置需要作相应的调整。
  67. 反之等于0时说明在该容量范围内,下标位置不变。
  68. */
  69. if ((e.hash & oldCap) == 0) {
  70. if (loTail == null)
  71. loHead = e;
  72. else
  73. loTail.next = e;
  74. loTail = e;
  75. }
  76. else {
  77. if (hiTail == null)
  78. hiHead = e;
  79. else
  80. hiTail.next = e;
  81. hiTail = e;
  82. }
  83. } while ((e = next) != null);
  84. //低位下标位置不变
  85. if (loTail != null) {
  86. loTail.next = null;
  87. newTab[j] = loHead;
  88. }
  89. //处于高位位置要改变为j + oldCap
  90. if (hiTail != null) {
  91. hiTail.next = null;
  92. newTab[j + oldCap] = hiHead;
  93. }
  94. }
  95. }
  96. }
  97. }
  98. return newTab;
  99. }

​ HashMap实际上是线程不安全的,在JDK1.7中,链表的插入方式为头插法,在多线程下插入可能会导致死循环。因此在JDK1.8中替换成尾插法(其实想要线程安全大可用ConcurrentHashMap、Hashtable)

  1. //JDK1.7源码
  2. void transfer(Entry[] newTable boolean rehash) {
  3. int newCapacity = newTable.length;
  4. for (Entry<K,V> e : table) {
  5. while(null != e) {
  6. //多线程在这里会导致指向成环
  7. Entry<K,V> next = e.next;
  8. if(rehash) {
  9. e.hash = null == e.key ? 0 : hash(e.key);
  10. }
  11. int i = indexFor(e.hash, new Capacity);
  12. e.next = newTable[i];
  13. newTable[i] = e;
  14. e = next;
  15. }
  16. }
  17. }

假如HashMap的容量为2,其中在数组中有一个元素a(此时已经到达扩容的临界点)。创建两个线程t1、t2分别插入b、c,因为没有锁,两个线程都进行到扩容这一步,那么其中有节点位子因为扩容必然会发生变化(以前的容量不够),这个时候假设t1线程成功运行,插入成功。但是由于t2线程的合并,加上节点位置的挪动,就会造成链表成环。最后读取失败

1.2.4 删除方法

  1. //通过key值删除该节点,并返回value
  2. public V remove(Object key) {
  3. Node<K,V> e;
  4. return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
  5. }
  6. //删除某个节点
  7. //若matchValue为true时,需要key和value都要相等才能删除;若movable为false时,删除节点时不移动其他节点
  8. final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
  9. Node<K,V>[] tab; Node<K,V> p; int n, index;
  10. //若数组非空
  11. if ((tab = table) != null && (n = tab.length) > 0 &&
  12. (p = tab[index = (n - 1) & hash]) != null) {
  13. //设node为删除点
  14. Node<K,V> node = null, e; K k; V v;
  15. //查到头节点为所要删除的点,直接赋于node
  16. if (p.hash == hash &&
  17. ((k = p.key) == key || (key != null && key.equals(k))))
  18. node = p;
  19. //否则遍历
  20. else if ((e = p.next) != null) {
  21. //当节点为树节点
  22. if (p instanceof TreeNode)
  23. node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
  24. //节点为链表时
  25. else {
  26. do {
  27. if (e.hash == hash &&
  28. ((k = e.key) == key ||
  29. (key != null && key.equals(k)))) {
  30. node = e;
  31. break;
  32. }
  33. p = e;
  34. } while ((e = e.next) != null);
  35. }
  36. }
  37. //对取回的node节点进行处理,当matchValue为false,或者value相等时
  38. if (node != null && (!matchValue || (v = node.value) == value ||
  39. (value != null && value.equals(v)))) {
  40. if (node instanceof TreeNode) //为树节点
  41. ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
  42. else if (node == p) //为链表头结点
  43. tab[index] = node.next;
  44. else //为链表中部节点
  45. p.next = node.next;
  46. //修改modCount和size
  47. ++modCount;
  48. --size;
  49. afterNodeRemoval(node);
  50. return node;
  51. }
  52. }
  53. return null;
  54. }

2.一些面试题

2.1 JDK1.8 HashMap扩容时做了哪些优化

  1. 新元素下标方面,1.8通过高位运算(e.hash & oldCap) == 0分类处理表中的元素:低位不变,高位原下标+原数组长度;而不是像1.7中计算每一个元素下标。

  2. 在resize()函数中,1.8将1.7中的头插逆序变成尾插顺序。但是仍然建议在多线程下不要用HashMap。

2.2 HashMap与Hashtable的区别

  1. 线程安全:Hashtable是线程安全的,不允许key,value为null。
  2. 继承父类:Hashtable是Dictionary类的子类(Dictionary类已经被废弃),两者都实现了Map接口。
  3. 扩容:Hashtable默认容量为11,扩容为原来的容量2倍+1,所以Hashtable获取下标直接用模运算符%。
  4. 存储方式:Hashtable中出现冲突后,只有用链表方式存储。

2.3 HashMap线程不安全,那么有哪些Map可以实现线程安全

  1. Hashtable: 直接在方法上加synchronized关键字,锁住整个哈希桶
  2. ConcurrentHashMap:使用分段锁,相比于Hashtable性能更高
  3. Collectons.synchronizedMap:是使用Collections集合工具的内部类,通过传入Map封装一个SynchronizedMap对象,内部定义一个对象锁,方法通过对象锁实现。

参考博文:

HashMap 底层实现原理是什么?JDK8 做了哪些优化?

一个HashMap跟面试官扯了半个小时

Java集合框架1-- HashMap的更多相关文章

  1. Java集合框架:HashMap

    转载: Java集合框架:HashMap Java集合框架概述   Java集合框架无论是在工作.学习.面试中都会经常涉及到,相信各位也并不陌生,其强大也不用多说,博主最近翻阅java集合框架的源码以 ...

  2. Java集合框架之HashMap浅析

    Java集合框架之HashMap浅析 一.HashMap综述: 1.1.HashMap概述 位于java.util包下的HashMap是Java集合框架的重要成员,它在jdk1.8中定义如下: pub ...

  3. (转)Java集合框架:HashMap

    来源:朱小厮 链接:http://blog.csdn.net/u013256816/article/details/50912762 Java集合框架概述 Java集合框架无论是在工作.学习.面试中都 ...

  4. Java 集合框架:HashMap

    原文出处:Java8 系列之重新认识 HashMap 摘要 HashMap 是 Java 程序员使用频率最高的用于映射 (键值对) 处理的数据类型.随着 JDK(Java Developmet Kit ...

  5. java集合框架之HashMap

    参考http://how2j.cn/k/collection/collection-hashmap/365.html#nowhere HashMap的键值对 HashMap储存数据的方式是-- 键值对 ...

  6. java集合框架之HashMap和Hashtable的区别

    参考http://how2j.cn/k/collection/collection-hashmap-vs-hashtable/692.html#nowhere HashMap和Hashtable的区别 ...

  7. Java集合框架(四)-HashMap

    1.HashMap特点 存放的元素都是键值对(key-value),key是唯一的,value是可以重复的 存放的元素也不保证添加的顺序,即是无序的 存放的元素的键可以为null,但是只能有一个key ...

  8. 深入理解java集合框架之---------HashMap集合

    深入理解HaspMap死循环问题 由于在公司项目中偶尔会遇到HashMap死循环造成CPU100%,重启后问题消失,隔一段时间又会反复出现.今天在这里来仔细剖析下多线程情况下HashMap所带来的问题 ...

  9. Java集合框架系列大纲

    ###Java集合框架之简述 Java集合框架之Collection Java集合框架之Iterator Java集合框架之HashSet Java集合框架之TreeSet Java集合框架之Link ...

  10. java集合框架之java HashMap代码解析

     java集合框架之java HashMap代码解析 文章Java集合框架综述后,具体集合类的代码,首先以既熟悉又陌生的HashMap开始. 源自http://www.codeceo.com/arti ...

随机推荐

  1. JavaWeb网上图书商城完整项目--21.用户模块各层相关类的创建

    1.现在要为user用户模块创建类 用户模块功能包括:注册.激活.登录.退出.修改密码. User类对照着t_user表来写即可.我们要保证User类的属性名称与t_user表的列名称完全相同. 我们 ...

  2. SMB扫描-Server Message Block 协议、nmap

    版本 操作系统 SMB1 Windows 200.xp.2003 SMB2 Windows Vista SP1.2008 SMB2.1 Windows 7/2008 R2 SMB3 Windows 8 ...

  3. JavaScript this 关键词

    this是什么呢? JavaScript this 关键词指的是它所属的对象. 它拥有不同的值,具体取决于它所使用的位置: 在方法中,this 指的是所有者对象. 单独的情况下,this 指的是全局对 ...

  4. LQR要点

    新的“A”变成着了这样:Ac = A - KB 基于对象:状态空间形式的系统 能量函数J:也称之为目标函数 Q:半正定矩阵,对角阵(允许对角元素出现0) R:正定矩阵,QR其实就是权重 下面这段话可能 ...

  5. webpack入门进阶(1)

    1.webpack应用实例 1.1.快速上手 初始化项目 mkdir webpack-demo cd webpack-demo npm init -y 安装webpack npm i webpack@ ...

  6. list 迭代器的用法

    string strTemp; list<string> strList; char *ch = new char[]; strcpy( ch , ""); strTe ...

  7. Linux下nginx反向代理服务器安装与配置实操

    1.我们只要实现访问nginx服务器能跳转到不同的服务器即可,我本地测试是这样的, 在nginx服务器里面搭建了2个tomcat,2个tomcat端口分别是8080和8081,当我输入我nginx服务 ...

  8. Sql sever 声明变量,赋值变量

    语句: --声明变量DECLARE @idcard nvarchar () , @rowid nvarchar () --给变量赋值SELECT @idcard = '{0}', @rowid = ' ...

  9. CentOS 7安装Oracle 12c图文详解

    环境: CentOS7@VMware12,分配资源:CPU:2颗,内存:4GB,硬盘空间:30GB Oracle 12C企业版64位 下载地址:http://www.oracle.com/techne ...

  10. JVM源码分析之堆内存的初始化

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十五篇. ​ 今天呢!灯塔君跟大家讲: JVM源码分析之堆内存的初始化   堆初始化 Java堆的初始化入口位于Univ ...