java jdk 中HashMap的源码解读

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的值改变.

时间: 2024-11-16 05:39:00

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

sklearn中LinearRegression关键源码解读

问题的引入 我们知道,线性回归方程的参数,可以用梯度下降法求解,或者用正规方程求解. 那sklearn.linear_model.LinearRegression中,是不是可以指定求解方式呢?能不能从中获取梯度相关信息呢? 下面是线性回归最简单的用法. from sklearn import linear_model # Create linear regression object regr = linear_model.LinearRegression() # Train the model

浅析JDK中ServiceLoader的源码

前提 紧接着上一篇<通过源码浅析JDK中的资源加载>,ServiceLoader是SPI(Service Provider Interface)中的服务类加载的核心类,也就是,这篇文章先介绍ServiceLoader的使用方式,再分析它的源码. ServiceLoader的使用 这里先列举一个经典的例子,MySQL的Java驱动就是通过ServiceLoader加载的,先引入mysql-connector-java的依赖: <dependency> <groupId>m

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

下面是java se 7 API 对于java.io.BufferedWriter 继承关系的描述. BufferedWriter可以将文本写入字符流.它会将字符缓存,目的是提高写入字符的效率. buffer的大小必须明确,否则将会使用默认的大小.默认的大小对于大多数情况是足够大的. BufferedWriter提供了一个newLine()的方法,目的是用来换行.毕竟不是所有的平台都使用'\n'的换行方式. 一个Writer对象会将输出立即写入当前的字符流或者字节流. 通常来说,如果这个写入不是

JAVA源码解读---HashMap目录扩展的奥秘

摘要:为了探索JAVA1.7源码中HashMap类数据的组织方法与目录扩展方法,本文通过对JAVA1.7源码中HashMap类源码的阅读与分析,得出结论:hashmap中存储数据的数据结构采用的是链表数组,目录是个数组,数组的成员是链表.冲突解决方法:典型的链地址法,冲突后,在链表头部插入数据.目录扩展方法:已二倍的方式扩展,一直到目录的最大上限.目录扩展的触发条件:装载因子的方式触发.从java中hashmap的实现可以看出,桶数据的组织方式并不是一种非常高效的方式.对检索效率不利.同时,数据

源码解读—HashTable

在上一篇学习过HashMap(源码解读—HashMap)之后对hashTable也产生了兴趣,随即便把hashTable的源码看了一下.和hashMap类似,但是也有不同之处. public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable  实现接口:Map,Cloneable,Serializable 继承自Diction

Java之ArrayList源码解读(JDK 1.8)

java.util.ArrayList 详细注释了ArrayList的实现,基于JDK 1.8 . 迭代器SubList部分未详细解释,会放到其他源码解读里面.此处重点关注ArrayList本身实现. 没有采用标准的注释,并适当调整了代码的缩进以方便介绍 import java.util.AbstractList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.

深入理解JAVA集合系列:HashMap源码解读

初认HashMap 基于哈希表(即散列表)的Map接口的实现,此实现提供所有可选的映射操作,并允许使用null值和null键. HashMap继承于AbstractMap,实现了Map.Cloneable.java.io.Serializable接口.且是不同步的,意味着它不是线程安全的. HashMap的数据结构 在java编程语言中,最基本的结构就两种,一个是数组,另一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的.HashMap也不例外,它是一个“链表的数组”的数据结构

jdk1.8.0_45源码解读——HashMap的实现

jdk1.8.0_45源码解读——HashMap的实现 一.HashMap概述 HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是<key,value>对的映射,允许多个null值和一个null键.但此类不保证映射的顺序,特别是它不保证该顺序恒久不变.  除了HashMap是非同步以及允许使用null外,HashMap 类与 Hashtable大致相同. 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能.迭代col

JDK源码解读之Integer(1)

本系列文章使用的JDK版本为jdk1.8.0_131,一些基础的知识储备:原码.反码.补码,移位,建议参考文章:<原码,反码,补码 详解><Java 源码学习系列(三)--Integer> Integer是我们开发过程中最常用的一个类,因此JDK的源码解读就从它开始吧.凡是对Java有点了解的都知道,Integer是int的包装类型,长度为32位.因此我们可以看到如下定义 //可表示的最小值:-2^31,至于为什么是这个数,上面的文章讲的很清楚了 @Native public st