HashMap是我们在日常写代码时最常用到的一个数据结构,它为我们提供key-value形式的数据存储。同时,它的查询,插入效率都非常高。

在之前的排序算法总结里面里,我大致学习了HashMap的实现原理,并制作了一个简化版本的HashMap。 今天,趁着项目的间歇期,我又仔细阅读了Java中的HashMap的实现。

HashMap的初始化:

Java代码 
  1. public HashMap(int initialCapacity, float loadFactor)
  2. public HashMap(int initialCapacity)
  3. public HashMap()
  4. public HashMap(Map<? extends K, ? extends V> m)

最近看到几篇精彩的文章:

存取之美 —— HashMap原理、源码、实践

Hash碰撞与拒绝服务攻击

这些文章让我收获良多, 但是有些地方说的不够详细, 在此写下本人对上述文章的总结和理解, 希望可以给需要的朋友带来一些帮助.

1. 概述

HashMap在底层采用数组+链表的形式存储键值对.

在HashMap中定义了一个内部类Entry<K, V>, 该内部类是对key-value的抽象. Entry类包含4个成员: key, value, hash, next. key和value的意义很清晰, hash表示key的hash值, next是指向下一个Entry对象的引用.

HashMap内部维护了一个Entry<K, V>[] table, 数组table中的Entry元素是一个Entry链表的头结点(理解这一点很重要).

2. put/get方法

向HashMap中添加键值对时, 程序会根据key的hashCode值计算出hash值, 然后对hash值取模, 模数是table.length. 假如取模的结果为index, 则取出table[index]. table[index]可能为null, 也可能是一个Entry对象. 如果为null, 则直接存储. 否则计算key.equals(table[index].key), 如果为false, 就取出table[index].next, 继续调用key的equals方法, 直到equals方法返回true, 或者比较完链表中所有Entry对象.

Java代码 
  1. public V put(K key, V value) {
  2. if (key == null)
  3. return putForNullKey(value);
  4. // 对hashCode值进行二次hash得到最终的hash值
  5. int hash = hash(key.hashCode());
  6. // 根据hash值定位数组中的索引位置
  7. int i = indexFor(hash, table.length);
  8. // 遍历table[i]位置处的链表
  9. for (Entry<K, V> e = table[i]; e != null; e = e.next) {
  10. Object k;
  11. // 如果hash值相同且equals返回true, 则替换原来的value值
  12. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  13. V oldValue = e.value;
  14. e.value = value;
  15. e.recordAccess(this);
  16. return oldValue;
  17. }
  18. }
  19. modCount++;
  20. // 如果之前函数没有return, 将该键值对插入table[i]链表中
  21. addEntry(hash, key, value, i);
  22. return null;
  23. }

理解了put方法, 那么get方法就会很容易理解:

Java代码 
  1. public V get(Object key) {
  2. if (key == null)
  3. return getForNullKey();
  4. int hash = hash(key.hashCode());
  5. // 首先根据hash值计算index, 然后取出index处的链表的头结点. 遍历链表.
  6. for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
  7. Object k;
  8. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
  9. return e.value;
  10. }
  11. return null;
  12. }

3. HashMap的容量和索引位置确定

前面没有叙述HashMap的容量问题, 是因为容量是与索引位置计算紧密相关的.

理解HashMap的容量就需要关注成员变量size, loadFactor, threshold.

size表示HashMap中实际包含的键值对个数.

loadFactor表示负载因子, loadFactor的值越大, 则对table数组的利用率越大, 相当于节省内存空间. 但是loadFactor的值增大, 同时也会导致hash冲突的概率增加, 从而使得程序效率降低. loadFactor的取值应该兼顾内存空间和效率, 默认值为0.75.

threshold表示极限容量, 计算公式为threshold = (int)(capacity * loadFactor);  当size达到threshold时, 就需要对table数组扩容.

HashMap的容量大小就是table.length. 由于java中取模是一个效率低下的操作, 所以出于性能的考虑, HashMap的容量被设计为2的N次方. 如此hash%table.length就可以转换为hash&(table.length-1). 与运算的效率比取模运算高效很多.

Java代码 
  1. public HashMap(int initialCapacity, float loadFactor) {
  2. if (initialCapacity < 0)
  3. throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
  4. if (initialCapacity > MAXIMUM_CAPACITY)
  5. initialCapacity = MAXIMUM_CAPACITY;
  6. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  7. throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
  8. // 计算大于initialCapacity的最小的2的N次方数
  9. int capacity = 1;
  10. while (capacity < initialCapacity)
  11. capacity <<= 1;
  12. this.loadFactor = loadFactor;
  13. // 求出极限容量
  14. threshold = (int) (capacity * loadFactor);
  15. // table的容量被设计为2的N次方
  16. table = new Entry[capacity];
  17. init();
  18. }

如果使用无参的构造函数创建HashMap, 则容量默认为16, 负载因子默认为0.75.

indexFor函数用于确定索引位置:

Java代码 
  1. static int indexFor(int h, int length) {
  2. // 当length为2的N次方时相当于h%table.length, 但效率要高效很多
  3. return h & (length - 1);
  4. }

4. rehash

前面提到过, 当size达到threshold时, 就需要对table数组扩容. 调用put函数向HashMap中插入一个键值对时会调用到addEntry(hash, key, value, i)方法:

Java代码 
  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. // 取出索引处的Entry对象
  3. Entry<K, V> e = table[bucketIndex];
  4. // 更新索引处链表的头结点, 并使新的头结点的next属性指向原来的头结点
  5. table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
  6. // 当size大于threshold时扩容数组, 容量增加至原来的2倍. 保证table的容量始终是2的N次方
  7. if (size++ >= threshold)
  8. resize(2 * table.length);
  9. }

resize用于扩容数组. 数组的length增加大了, 那么HashMap中已有的键值对就必须重新进行hash, 这就是rehash. 如果不进行rehash, 就会使得put和get时table数组的length不同, 从而导致get方法无法取出原先put存入的键值对.

Java代码 
  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. if (oldCapacity == MAXIMUM_CAPACITY) {
  5. threshold = Integer.MAX_VALUE;
  6. return;
  7. }
  8. Entry[] newTable = new Entry[newCapacity];
  9. transfer(newTable);
  10. table = newTable;
  11. threshold = (int) (newCapacity * loadFactor);
  12. }
  13. void transfer(Entry[] newTable) {
  14. Entry[] src = table;
  15. int newCapacity = newTable.length;
  16. // 对已有的键值对进行rehash
  17. for (int j = 0; j < src.length; j++) {
  18. // 得到j处的链表的头结点
  19. Entry<K, V> e = src[j];
  20. // 遍历链表
  21. if (e != null) {
  22. src[j] = null;
  23. do {
  24. // 进行rehash
  25. Entry<K, V> next = e.next;
  26. int i = indexFor(e.hash, newCapacity);
  27. e.next = newTable[i];
  28. newTable[i] = e;
  29. e = next;
  30. } while (e != null);
  31. }
  32. }
  33. }

从源码可以看出, rehash对性能的影响是非常大的, 因此我们应该尽量避免rehash的发生. 这就要求我们预估需要存入HashMap的键值对的数量, 根据数量在创建HashMap对象时指定合适的容量和负载因子.

5. hash碰撞和HashMap的退化

hash碰撞在HashMap中的表现为: 不同的key, 计算出相同的index. 如果对所有的key调用indexFor方法的返回值都是相同的, 那么HashMap就退化为链表, 这对性能的影响也是非常大的. 几个月前的闹得沸沸扬扬的hash碰撞攻击就是基于这样的原理.

常用的web框架都会将请求中的参数保存在HashMap(或HashTable)中, 如果客户端根据Web应用框架采用的Hash函数来通过某种Hash攻击的方式获得大量的碰撞, 那么HashMap就会退化为链表, 服务器有可能处理一次请求要花上十几分钟甚至几个小时的时间...

6. 线程安全

HashMap是线程不安全的, 如果遍历HashMap的过程中修改了HashMap, 那么就会抛出java.util.ConcurrentModificationException异常:

Java代码 
  1. final Entry<K, V> nextEntry() {
  2. if (modCount != expectedModCount)
  3. throw new ConcurrentModificationException();
  4. Entry<K, V> e = next;
  5. if (e == null)
  6. throw new NoSuchElementException();
  7. if ((next = e.next) == null) {
  8. Entry[] t = table;
  9. while (index < t.length && (next = t[index++]) == null)
  10. ;
  11. }
  12. current = e;
  13. return e;
  14. }

modCount是HashMap的成员变量, 用于表示HashMap的状态. expectedModCount是遍历初始时modCount的值. 如果在遍历过程中改变了modCount的值就会导致modCount和expectedModCount不相等, 从而抛出异常. put, clear, remove等方法都会导致modCount的值改变.

java jdk 中HashMap的源码解读的更多相关文章

  1. Java中HashMap的源码分析

    先来回顾一下Map类中常用实现类的区别: HashMap:底层实现是哈希表+链表,在JDK8中,当链表长度大于8时转换为红黑树,线程不安全,效率高,允许key或value为null HashTable ...

  2. 浅析JDK中ServiceLoader的源码

    前提 紧接着上一篇<通过源码浅析JDK中的资源加载>,ServiceLoader是SPI(Service Provider Interface)中的服务类加载的核心类,也就是,这篇文章先介 ...

  3. java.io.BufferedWriter API 以及源码解读

    下面是java se 7 API 对于java.io.BufferedWriter 继承关系的描述. BufferedWriter可以将文本写入字符流.它会将字符缓存,目的是提高写入字符的效率. bu ...

  4. java.io.writer API 以及 源码解读

    声明 我看的是java7的API文档. 如下图所示,java.io.writer 继承了java.lang.Object,实现的接口有Closeable, Flushable, Appendable, ...

  5. go中sync.Cond源码解读

    sync.Cond 前言 什么是sync.Cond 看下源码 Wait Signal Broadcast 总结 sync.Cond 前言 本次的代码是基于go version go1.13.15 da ...

  6. go中sync.Mutex源码解读

    互斥锁 前言 什么是sync.Mutex 分析下源码 Lock 位运算 Unlock 总结 参考 互斥锁 前言 本次的代码是基于go version go1.13.15 darwin/amd64 什么 ...

  7. go中semaphore(信号量)源码解读

    运行时信号量机制 semaphore 前言 作用是什么 几个主要的方法 如何实现 sudog 缓存 acquireSudog releaseSudog semaphore poll_runtime_S ...

  8. go中sync.Once源码解读

    sync.Once 前言 sync.Once的作用 实现原理 总结 sync.Once 前言 本次的代码是基于go version go1.13.15 darwin/amd64 sync.Once的作 ...

  9. JDK容器类Map源码解读

    java.util.Map接口是JDK1.2开始提供的一个基于键值对的散列表接口,其设计的初衷是为了替换JDK1.0中的java.util.Dictionary抽象类.Dictionary是JDK最初 ...

随机推荐

  1. 【问题记录】记一次ConnectionTimeout问题排查

    最近做性能测试时,发现连接第三方系统时会有约1%的交易提示如下错误 nested exception is org.apache.commons.httpclient.ConnectTimeoutEx ...

  2. 软件工程作业0——The Road Not Taken

    目录 第一部分:结缘计算机 缘起 四顾 思考 第二部分:在计算机系里学习 挑战 落差 第三部分:未来规划 向前 未来四个月的软工课 项目 内容 这个作业属于 2020春季计算机学院软件工程(罗杰 任健 ...

  3. rabbitmq系列(四)死信队列

    一.什么是死信队列 当消息在一个队列中变成一个死信之后,它将被重新publish到另一个交换机上,这个交换机我们就叫做死信交换机,私信交换机将死信投递到一个队列上就是死信队列.具体原理如下图: 消息变 ...

  4. Python - 面向对象(三)公共变量,受保护变量,私有变量

    前言 在Python的类里面,所有属性和方法默认都是公共的:但Python也可以设置受保护.私有类型的变量or方法 受保护类型的变量.方法 一般称为:protected变量 #!/usr/bin/en ...

  5. pc 媒体查询

    PC端 按屏幕宽度大小排序(主流的用橙色标明) 分辨率   比例 | 设备尺寸 1024*500 (8.9寸) 1024*768 (比例4:3  | 10.4寸.12.1寸.14.1寸.15寸; ) ...

  6. Mol Cell Proteomics. | Elevated Hexokinase II Expression Confers Acquired Resistance to 4-Hydroxytamoxifen in Breast Cancer Cells(升高的己糖激酶II表达使得乳腺癌细胞获得对他莫昔芬的抗性)(解读人:黄旭蕾)

    文献名:Elevated Hexokinase II Expression Confers Acquired Resistance to 4-Hydroxytamoxifen in Breast Ca ...

  7. [BFS,A*,k短路径] 2019中国大学生程序设计竞赛(CCPC) - 网络选拔赛 path (Problem - 6705)

    题目:http://acm.hdu.edu.cn/showproblem.php?pid=6705 path Time Limit: 2000/2000 MS (Java/Others)    Mem ...

  8. pat 1044.火星数字 Java版

    个人网站:https://www.lingluan.xyz 火星人是以13进制计数的: 地球人的0被火星人称为tret. 地球人数字1到12的火星文分别为:jan, feb, mar, apr, ma ...

  9. Inception系列理解

    博客:博客园 | CSDN | blog 写在前面 Inception 家族成员:Inception-V1(GoogLeNet).BN-Inception.Inception-V2.Inception ...

  10. SpringBoot学习笔记(十一:使用MongoDB存储文件 )

    @ 目录 一.MongoDB存储文件 1.MongoDB存储小文件 2.MongoDB存储大文件 2.1.GridFS存储原理 2.2.GridFS使用 2.2.1.使用shell命令 2.2.2.使 ...