java jdk 中HashMap的源码解读
HashMap是我们在日常写代码时最常用到的一个数据结构,它为我们提供key-value形式的数据存储。同时,它的查询,插入效率都非常高。
在之前的排序算法总结里面里,我大致学习了HashMap的实现原理,并制作了一个简化版本的HashMap。 今天,趁着项目的间歇期,我又仔细阅读了Java中的HashMap的实现。
HashMap的初始化:
- public HashMap(int initialCapacity, float loadFactor)
- public HashMap(int initialCapacity)
- public HashMap()
- public HashMap(Map<? extends K, ? extends V> m)
最近看到几篇精彩的文章:
这些文章让我收获良多, 但是有些地方说的不够详细, 在此写下本人对上述文章的总结和理解, 希望可以给需要的朋友带来一些帮助.
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对象.
- public V put(K key, V value) {
- if (key == null)
- return putForNullKey(value);
- // 对hashCode值进行二次hash得到最终的hash值
- int hash = hash(key.hashCode());
- // 根据hash值定位数组中的索引位置
- int i = indexFor(hash, table.length);
- // 遍历table[i]位置处的链表
- for (Entry<K, V> e = table[i]; e != null; e = e.next) {
- Object k;
- // 如果hash值相同且equals返回true, 则替换原来的value值
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- // 如果之前函数没有return, 将该键值对插入table[i]链表中
- addEntry(hash, key, value, i);
- return null;
- }
理解了put方法, 那么get方法就会很容易理解:
- public V get(Object key) {
- if (key == null)
- return getForNullKey();
- int hash = hash(key.hashCode());
- // 首先根据hash值计算index, 然后取出index处的链表的头结点. 遍历链表.
- for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
- return e.value;
- }
- return null;
- }
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). 与运算的效率比取模运算高效很多.
- public HashMap(int initialCapacity, float loadFactor) {
- if (initialCapacity < 0)
- throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
- if (initialCapacity > MAXIMUM_CAPACITY)
- initialCapacity = MAXIMUM_CAPACITY;
- if (loadFactor <= 0 || Float.isNaN(loadFactor))
- throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
- // 计算大于initialCapacity的最小的2的N次方数
- int capacity = 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
- this.loadFactor = loadFactor;
- // 求出极限容量
- threshold = (int) (capacity * loadFactor);
- // table的容量被设计为2的N次方
- table = new Entry[capacity];
- init();
- }
如果使用无参的构造函数创建HashMap, 则容量默认为16, 负载因子默认为0.75.
indexFor函数用于确定索引位置:
- static int indexFor(int h, int length) {
- // 当length为2的N次方时相当于h%table.length, 但效率要高效很多
- return h & (length - 1);
- }
4. rehash
前面提到过, 当size达到threshold时, 就需要对table数组扩容. 调用put函数向HashMap中插入一个键值对时会调用到addEntry(hash, key, value, i)方法:
- void addEntry(int hash, K key, V value, int bucketIndex) {
- // 取出索引处的Entry对象
- Entry<K, V> e = table[bucketIndex];
- // 更新索引处链表的头结点, 并使新的头结点的next属性指向原来的头结点
- table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
- // 当size大于threshold时扩容数组, 容量增加至原来的2倍. 保证table的容量始终是2的N次方
- if (size++ >= threshold)
- resize(2 * table.length);
- }
resize用于扩容数组. 数组的length增加大了, 那么HashMap中已有的键值对就必须重新进行hash, 这就是rehash. 如果不进行rehash, 就会使得put和get时table数组的length不同, 从而导致get方法无法取出原先put存入的键值对.
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
- Entry[] newTable = new Entry[newCapacity];
- transfer(newTable);
- table = newTable;
- threshold = (int) (newCapacity * loadFactor);
- }
- void transfer(Entry[] newTable) {
- Entry[] src = table;
- int newCapacity = newTable.length;
- // 对已有的键值对进行rehash
- for (int j = 0; j < src.length; j++) {
- // 得到j处的链表的头结点
- Entry<K, V> e = src[j];
- // 遍历链表
- if (e != null) {
- src[j] = null;
- do {
- // 进行rehash
- Entry<K, V> next = e.next;
- int i = indexFor(e.hash, newCapacity);
- e.next = newTable[i];
- newTable[i] = e;
- e = next;
- } while (e != null);
- }
- }
- }
从源码可以看出, 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异常:
- final Entry<K, V> nextEntry() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- Entry<K, V> e = next;
- if (e == null)
- throw new NoSuchElementException();
- if ((next = e.next) == null) {
- Entry[] t = table;
- while (index < t.length && (next = t[index++]) == null)
- ;
- }
- current = e;
- return e;
- }
modCount是HashMap的成员变量, 用于表示HashMap的状态. expectedModCount是遍历初始时modCount的值. 如果在遍历过程中改变了modCount的值就会导致modCount和expectedModCount不相等, 从而抛出异常. put, clear, remove等方法都会导致modCount的值改变.
java jdk 中HashMap的源码解读的更多相关文章
- Java中HashMap的源码分析
先来回顾一下Map类中常用实现类的区别: HashMap:底层实现是哈希表+链表,在JDK8中,当链表长度大于8时转换为红黑树,线程不安全,效率高,允许key或value为null HashTable ...
- 浅析JDK中ServiceLoader的源码
前提 紧接着上一篇<通过源码浅析JDK中的资源加载>,ServiceLoader是SPI(Service Provider Interface)中的服务类加载的核心类,也就是,这篇文章先介 ...
- java.io.BufferedWriter API 以及源码解读
下面是java se 7 API 对于java.io.BufferedWriter 继承关系的描述. BufferedWriter可以将文本写入字符流.它会将字符缓存,目的是提高写入字符的效率. bu ...
- java.io.writer API 以及 源码解读
声明 我看的是java7的API文档. 如下图所示,java.io.writer 继承了java.lang.Object,实现的接口有Closeable, Flushable, Appendable, ...
- go中sync.Cond源码解读
sync.Cond 前言 什么是sync.Cond 看下源码 Wait Signal Broadcast 总结 sync.Cond 前言 本次的代码是基于go version go1.13.15 da ...
- go中sync.Mutex源码解读
互斥锁 前言 什么是sync.Mutex 分析下源码 Lock 位运算 Unlock 总结 参考 互斥锁 前言 本次的代码是基于go version go1.13.15 darwin/amd64 什么 ...
- go中semaphore(信号量)源码解读
运行时信号量机制 semaphore 前言 作用是什么 几个主要的方法 如何实现 sudog 缓存 acquireSudog releaseSudog semaphore poll_runtime_S ...
- go中sync.Once源码解读
sync.Once 前言 sync.Once的作用 实现原理 总结 sync.Once 前言 本次的代码是基于go version go1.13.15 darwin/amd64 sync.Once的作 ...
- JDK容器类Map源码解读
java.util.Map接口是JDK1.2开始提供的一个基于键值对的散列表接口,其设计的初衷是为了替换JDK1.0中的java.util.Dictionary抽象类.Dictionary是JDK最初 ...
随机推荐
- Python基础 | 日期时间操作
目录 获取时间 时间映射 格式转换 字符串转日期 日期转字符串 unixtime 时间计算 时间偏移 时间差 "日期时间数据"作为三大基础数据类型之一,在数据分析中会经常遇到. 本 ...
- [Linux][C][gcc][tips] 在头文件中定义变量引发的讨论
概述 本人的原创文章,最先发表在github-Dramalife-note中.转载请注明出处. Define variable(s) in header file referenced by mult ...
- M-Renamer方法名修改器,iOS项目方法名重构,Objective-C/Swift,代码模型预判,减少误改的机率,替换速度更快,可视化操作,傻瓜式操作,一键操作,引用处自动修改,马甲包的福音
M-Renamer M-Renamer(Method-Name-Renamer)类方法名修改器,采用链式解析头文件,代码模型预判,减少误改的机率,替换速度更快:可以解析整个项目大多数类的方法,可视化操 ...
- 用sort 排序
这两天看了一个比较好的sort总结,所以转载了一下 阅读目录 1.sort 2.sort简介 3.sort扩展 1.sort 使用:#include <algorithm> using ...
- 【C++】Strassen算法代码
本文仅代码,无理论解释 实话实说,我觉得这个算法在C系列的语言下,简直垃圾到爆炸--毕竟是一群完全不懂程序数学家对着纸弄出来的,看起来好像非常的有用,实际上耗时是非常爆炸的. 但是<算法导论&g ...
- word2vec 和 glove 模型的区别
2019-09-09 15:36:13 问题描述:word2vec 和 glove 这两个生成 word embedding 的算法有什么区别. 问题求解: GloVe (global vectors ...
- java两数相乘基础算法
下面是别人给我的代码: package com.bootdo; public class Test { public static void main(String[] args) { System. ...
- TortoiseGit 与 Putty 配置冲突导致 Server refuse our key
tortoisegit是TortoiseSVN的Git版本,tortoisegit用于迁移TortoiseSVN到TortoiseGit,一直以来Git在Windows平台没有好用GUI客户端,现在t ...
- OpenCV-Python 图像阈值 | 十五
目标 在本教程中,您将学习简单阈值,自适应阈值和Otsu阈值. 你将学习函数cv.threshold和cv.adaptiveThreshold. 简单阈值 在这里,问题直截了当.对于每个像素,应用相同 ...
- 阿里开源首个移动AI项目,淘宝同款推理引擎
淘宝上用的移动AI技术,你也可以用在自己的产品中了. 刚刚,阿里巴巴宣布,开源自家轻量级的深度神经网络推理引擎MNN(Mobile Neural Network),用于在智能手机.IoT设备等端侧加载 ...