HashMap源码阅读启读
上一篇文章让是我对整个java集合的基础认识,让我能够使用集合,下面我将对其中几个重要集合的(ArrayList,LinkList,HashMap)源码进行阅读。
一、HashMap是什么?
弄清楚这点!!我觉得这是最重要的一点。
官方的解释是:
HashMap是基于哈希表的实现的Map接口。此实现提供了所有可选的Map操作,并允许null的值和null键。 ( HashMap类大致相当于Hashtable ,除了它是不同步的,并允许null)。这个类不能保证Map的顺序; 特别是,它不能保证订单在一段时间内保持不变。
为什么要用Hash
我了解了下为什么要用hashcode计算存入的键:
因为我们存入的键不能重复,那么我们怎么判断重复了,当然是用hashcode()方法把键转成hash码进行比较咯。为什么不直接比较呢,我的理解是,因为直接比较就跟肉眼去看两个人是否是双胞胎,而用hashcode计算后的哈希码比较就相当于把他们的DNA进行比较。
但别人是这样解释的:
也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。
哈希是什么 :
把任意长度的输入(输入叫做预映射,知道就行),通过一种函数(hashCode() 方法),变换成固定长度的输出,该输出就是哈希值(hashCode),这种函数就叫做哈希函数,而计算哈希值的过程就叫做哈希。哈希的主要应用是哈希表和分布式缓存。
哈希冲突又是什么:
哈希表选用哈希函数计算哈希值时,可能不同的 key 会得到相同的结果,一个地址怎么存放多个数据呢?这就是哈希冲突(哈希碰撞)。
解决哈希冲突 有两种方法,拉链法(链接法)和开放定址法。拉链法就是:将键值对对象封装为一个node结点(数组中的一个值),新增了next指向,这样就可以将碰撞的结点链接成一条单链表,保存在该地址(数组位置)中。
HashMap中并不是直接用的hashcode产生的哈希码,而是进行了一些位计算,但并未改变结果,至于为什么这么做 请参考这篇博客:HashMap中的hash算法中的几个疑问 总之就是为了提高性能
HashMap中的hash函数的源码如下:
static final int hash(Object key) {int h;// key.hashCode():返回散列值也就是hashcode// ^ :按位异或// >>>:无符号右移,忽略符号位,空位都以0补齐return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
二、HashMap的存储结构:
网上这张图片即表示了运用了拉链法的HashMap的存储结构(也叫桶数组):
当存在相同hashcode的key的时候,把他插入后面的链表,使用的是头插法(因为HashMap的发明者认为,后插入的key被查找的可能性更大)。
HashMap的这种特殊存储结构在获取指定元素前需要把key经过哈希运算,得到目标元素在哈希表中的位置,然后再进行少量比较即可得到元素,这使得 HashMap 的查找效率极高。
图中数组的索引= HashCode(Key)%length //length为HashMap的长度
均匀分布提高效率?
均匀分布其实是为了减少hash碰撞,因为减少了碰撞,效率才会提升。
哈希表长度越长,空间成本越大,哈希函数计算结果越分散均匀。哈希函数计算结果越分散均匀,哈希碰撞的概率就越小,map的存取效率(时间复杂度)就会越高。 因此我们的HashMap设计者进行了堪称完美的设计来提高效率。
实现均匀分布:
那么如何实现一个尽量均匀分布 的Hash函数呢?当然是把计算的哈希值当做数组索引进行存储在数组中咯,但是 Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,内存肯定放不下,所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标,所以开始的公式是:HashCode(Key)% length
后来经过研究发现取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方?
至于为什么能这样能均匀分布请参考:漫画讲解HashMap
看到一位大佬对使用HashMap的特点解释如下:下面我也将通过源码逐个讲解他们具体是如何通过代码实现的。
HashMap的出现是为了实现一种快速的查找并且插入、删除性能都不错的一种K/V(key/value)数据结构:
- 为了实现快速查找,HashMap 选择了数组而不是链表。以利用数组的索引实现 O(1) 复杂度的查找效率。
- 为了利用索引查找,HashMap引入 Hash 算法, 将 key 映射成数组下标: key -> Index。 引入 Hash 算法又导致了 Hash 冲突。
- 为了解决Hash 冲突,HashMap 采用链地址法,在冲突位置转为使用链表存储。 链表存储过多的节点又导致了在链表上节点的查找性能的恶化。
- 为了优化查找性能,HashMap 在链表长度超过 8 之后转而将链表转变成红黑树,以将 O(n) 复杂度的查找效率提升至 O(log n)。
三、HashMap.java
3.1、属性解释
下面是这个类中的属性解释:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {// 序列号private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量是16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 当桶(bucket)上的结点数大于这个值时会转成红黑树static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表static final int UNTREEIFY_THRESHOLD = 6;// 桶中结构转化为红黑树对应的table的最小大小static final int MIN_TREEIFY_CAPACITY = 64;// 存储元素的数组,总是2的幂次倍transient Node<k,v>[] table; // 存放具体元素的集transient Set<map.entry<k,v>> entrySet;// 存放元素的个数,注意这个不等于数组的长度。transient int size;// 每次扩容和更改map结构的计数器 例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化transient int modCount; //临界值(最大node结点(键值对)容量)当实际大小(容量*填充因子)超过临界值时,会进行扩容int threshold;// 填充因子final float loadFactor;
}
两个重点属性:
loadFactor加载因子:
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
threshold:
threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
3.2、HashMap的扩容
扩容分为两种:
①创建时如果不指定容量初始值,HashMap默认的初始化大小为16,之后每次扩充,容量变为原来的2倍。(区别一下Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1)
②创建时如果给定了容量初始值, HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()
方法保证,下面详讲)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。(Hashtable 会直接使用你给定的大小)
3.3、解决hash冲突
我们知道解决hash冲突的原理就是,当发生碰撞时,就把相同hash值的 K-V对 组成链表。当链表长度大于阈值(即属性TREEIFY_THRESHOLD
默认为8)时,将链表转化为红黑树,以减少搜索时间。
/**这个方法即保证了 HashMap 总是使用2的幂作为哈希表的大小。* Returns a power of two size for the given target capacity.* 返回给定容量的两倍大小*/static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}
3.4、构造方法
3.5、存数据-----put方法
3.6、取数据-----get方法
其他方法:
get,put方法参考
HashMap源码阅读启读相关推荐
- HashMap源码阅读笔记
HashMap是Java编程中常用的集合框架之一. 利用idea得到的类的继承关系图可以发现,HashMap继承了抽象类AbstractMap,并实现了Map接口(对于Serializable和Clo ...
- HashMap 源码阅读
前言 之前读过一些类的源码,近来发现都忘了,再读一遍整理记录一下.这次读的是 JDK 11 的代码,贴上来的源码会去掉大部分的注释, 也会加上一些自己的理解. Map 接口 这里提一下 Map 接口与 ...
- HashMap jdk1.7源码阅读与解析
转载自 HashMap源码阅读与解析 一.导入语 HashMap是我们最常见也是最长使用的数据结构之一,它的功能强大.用处广泛.而且也是面试常见的考查知识点.常见问题可能有HashMap存储结构是什 ...
- jdk源码阅读-HashMap
前置阅读: jdk源码阅读-Map : http://www.cnblogs.com/ccode/p/4645683.html 在前置阅读的文章里,已经提到HashMap是基于Hash表实现的,所以在 ...
- HashMap 源码详细分析(JDK1.8)
1. 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值 ...
- Java8 LinkedHashMap 源码阅读
如果你对 HashMap 的源码有了解的话,只需要一图就能知道 LinkedHashMap 的原理了,但是具体的实现细节还是需要去读一下源码. 一.LinkedHashMap 简介 1.1 继承结构 ...
- MyBatis 源码阅读 -- 核心操作篇
核心操作包是 MyBatis 进行数据库查询和对象关系映射等工作的包.该包中的类能完成参数解析.数据库查询.结果映射等主要功能.在主要功能的执行过程中还会涉及缓存.懒加载.鉴别器处理.主键自增.插件支 ...
- 走过的路-java源码阅读之路
源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心. 一.人生三种境界: 1.昨夜西风凋碧树,独上高楼望尽天涯路. 2.衣带渐宽终不悔,为伊消得人憔悴. ...
- 遍历HashMap源码——红黑树原理、HashMap红黑树实现与反树型化(三)
本章将是HashMap源码的最后一章,将介绍红黑树及其实现,HashMap的remove方法与反树型化.长文预警~~ 遍历HashMap源码--红黑树原理.HashMap红黑树实现与反树型化 什么是红 ...
最新文章
- 计算机组成原理-第3章-3.1
- 人工智能浪潮下的语音交互——VUI设计(基础篇)
- python适合零基础学习吗-零基础,经济学专业,适合自学Python吗?
- 解决Win10下_findnext()异常
- GDI绘制时钟效果,与系统时间保持同步,基于Winform
- Qt入门之基础篇 ( 一 ) :Qt4及Qt5的下载与安装
- Linux-Ubuntu中使用apt进行软件的安装与卸载
- 通过exp命令对Oracle数据库进行备份操作(提供两种情况的备份:备份本地,备份远程的数据库)
- Access 时间比较错误
- python 元类的call总结_Python 类与元类的深度挖掘 I【经验】
- 外显子和基因组基本概念(一)
- Java中循环删除list中元素的方法总结(总结)
- html中立体丝带菜单,使用CSS3实现绚丽的飘带样式菜单方法介绍
- Jackson Annotation Examples
- 移动开发语言Swift
- android looper介绍
- WiFi 四次握手Omnipeek抓包
- Ubuntu过去十年的10个关键时刻
- 王二 设计模式读书笔记
- 用PS制作墙壁上的时尚立体文字图案
热门文章
- vscode中setting.json配置详解
- busybox的实现原理分析(C语言实现简易版的busybox)
- jsoncpp在vs2012下的环境搭建(C++)
- mysql查询是第几条记录_MySQL查询第几行到第几行记录
- Transformer拿下CV顶会大奖,微软亚研获ICCV 2021最佳论文
- Excel切片器的使用
- excel切片器_听说你还不会用切片器?比筛选好用100倍,小白也能学会
- Unity后处理效果之边角压暗
- 2020年计算机专业研究生考试时间,2020计算机考研考试时间及考试内容
- 《牧羊少年奇幻之旅》读后感