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. 大数据安装之Kafka(用于实时处理的消息队列)

    一.安装部署kafka 1.集群规划 hadoop102                                 hadoop103                          hado ...

  2. 使用TensorFlow进行训练识别视频图像中物体

    本教程针对Windows10实现谷歌公布的TensorFlow Object Detection API视频物体识别系统,其他平台也可借鉴. 本教程将网络上相关资料筛选整合(文末附上参考资料链接),旨 ...

  3. 新文预览 | IoU-aware Single-stage Object Detector for Accurate Localization

    论文基于RetinaNet提出了IoU-aware sinage-stage目标检测算法,该算法在regression branch接入IoU predictor head并通过加权分类置信度和IoU ...

  4. 洛谷1972 HH的项链 树状数组查询区间内不同的数的数量

    题目链接:https://www.luogu.com.cn/problem/P1972 题意大致是:给定一个序列长度为n,给出m个查询区间,要求响应是区间内不同的数的个数.为此我们考虑到树状数组的区间 ...

  5. CERN Root与CLING

    CERN Root on Arch Linux For WSL: 一个CLI才是本体的程序居然有图形启动界面,莫名的微妙感 接触到Root是在一个4chan上喷matlab的thread里.某anon ...

  6. RDD的Cache、Persist、Checkpoint的区别和StorageLevel存储级别划分

    为了增强容错性和高可用,避免上游RDD被重复计算的大量时间开销,Spark RDD设计了包含多种存储级别的缓存和持久化机制,主要有三个概念:Cache.Persist.Checkout. 1.存储级别 ...

  7. flume面试题

    1 你是如何实现Flume数据传输的监控的使用第三方框架Ganglia实时监控Flume. 2 Flume的Source,Sink,Channel的作用?你们Source是什么类型?1.作用 (1)S ...

  8. Building Applications with Force.com and VisualForce (DEV401) (二四):JavaScript in Visualforce

    Dev401-025:Visualforce Pages: JavaScript in Visualforce Module Objectives1.Describe the use of AJAX ...

  9. 深度学习中正则化技术概述(附Python代码)

    欢迎大家关注我们的网站和系列教程:http://www.tensorflownews.com/,学习更多的机器学习.深度学习的知识! 磐石 介绍 数据科学研究者们最常遇见的问题之一就是怎样避免过拟合. ...

  10. 简单理解vertical-align属性和基线问题

    vertical-align属性主要用于改变行内元素的对齐方式,对于行内布局影响很大,如果不了解的话,我们开发调整样式的时候很容易出错. 网上关于这个属性的原理说得很是复杂,看一眼就让人觉得望而生畏, ...