jdk源码阅读笔记-HashMap
文章出处:【noblogs-it技术博客网站】的博客:jdk1.8源码分析
在Java语言中使用的最多的数据结构大概右两种,第一种是数组,比如Array,ArrayList,第二种链表,比如ArrayLinkedList,基于数组的数据结构特点是查找速度很快,时间复杂度为 O(1),但是删除的速度比较慢,因为每次删除元素的时候需要把后面的所有的元素都要相应的往前移动一位,最坏的情况删除第一个元素,时间复杂度为O(n)。基于链表实现的数据结构的特点是删除的速度比较快,但是查找的速度比较慢,每次查找数据的时候都需要从链表头部开始往下遍历,链表查找最坏时间是O(n)。HashMap 就整合和了数组和链表的有点而设计出来的,它的查找速度为 O(1) + O(a),a为链表长度,事实上hashMap的hash算法能够很好的避免了在插入数据的碰撞问题,所以链表的长度基本不会很长,所以hashMap的查找速度还是很快的。一般地,我们平衡一种结构的性能是看平均时间复杂度的,在 jdk1.8以前hashMap在最糟糕的情况下查找的时间复杂度为 O(1) +O(n) ,n 为数据的大小。在jdk1.8时sun公司对hashMap进行了优化,hashMap的存储结构由原来的数组+链接的结构改成 数组+链表+红黑树的形式。时间复杂度由O(1) + O(n) 降为 O(1) + O(logn)。下面的源码都是基于jdk1.8的。
HashMap中几个重要的参数:
1、threshold : 数组的大小,默认长度为16,可以在构造函数中指定初始化大小,但是必须是2的n次方,具体原因在下面将会说到。注意:该值是指数组的大小,并不是指HashMap中已经存放了的数据量,存放的数据的大小总是小于等于 threshold * loadFactor。
2、loadFactor: 负载因子,默认值为0.75。当HashMap中存储的数据大于阈值(threshold * loadFactor)时,threshold会进行翻倍,执行resize方法,对原数组中所有的元素进行一次重新hash计算,根据hash计算得出的下表放在新的数组中。负载因子的设计是为了减少在put操作时发生的碰撞,因为当我们put的数据越来越多的时候,数组中空的位置也会越来越少,那么发生碰撞的概率也随之增大,碰撞的次数越多对性能由一定的影响。一般地我们不需要对这个值进行设置,使用默认值就可以了。
3、TREEIFY_THRESHOLD:转换红黑树的阈值,默认值为8。即当数组中链表的长度达到这个值之后,链表就是转换成红黑树,以提高性能。
4、UNTREEIFY_THRESHOLD:红黑树转链表的阈值,默认值为6。
HashMap结构
HashMap是以key-value的形式存储数组中,将数据存在Node节点中,每个Node节点存储了一个key,对应的value和指向下一个Node的指针。HashMap的结构为数组+链表(红黑树),链表为单向链表。结构如下:
或:
HashMap原理:
HashMap在进行put(key,value)操的时候,我们源码
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == )
n = (tab = resize()).length;
/**
* 通过位与的方式来确定下标位置,判断当前下标位置是否为空,如果为空直接放入到该位置上
* 不为空则通过equals方法来寻找当前位置上面的元素,如果有相同的key,则将覆盖掉,如果没有则将node放置在对应
* 位置上面
*/
if ((p = tab[i = (n - ) & hash]) == null)//直接放到数组中
tab[i] = newNode(hash, key, value, null);
else {//当前位置不为空
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//已存在相同的key的数据,将其覆盖
e = p;
else if (p instanceof TreeNode)//当前位置是红黑树,将Node节点放到红黑树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//为链表的情况
for (int binCount = ; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表的长度超过转换红黑数的阈值,则将该链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - ) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//覆盖相同key的node
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//快速失败机制
if (++size > threshold)//每次插入数据都要判断一下当前存储的数据是否需要扩容
resize();
afterNodeInsertion(evict);
return null;
}
当我们当HashMap中put数据的时候,首先会对传进来的key进行hash计算:
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? : (h = key.hashCode()) ^ (h >>> );
}
jdk1.8开始hash的计算比之前的简单一些,就是对key的hashCode的高16位和低16位进行异或运算。这样做的目的是让key的HashCode 的高位也有计算参与运算,这样计算出来的hash值更加均匀,put数据时能够减少碰撞,提供性能。
第二步根据key计算出来的值获取到对应的下标,这里并不是使用取模的方式来确定,因为取模的方式相对于位与运算来说性能更低下。下标的计算公式为:当前数组的长度减一 按位与 hash值,得到下标,比如当前数组长度为 16,hash值:54707624,则计算如下:(注意:位与的运算规则为,当两个数均为1时结果才为1,否则结果为0)
从上面的运算结果,可以得到一个规律,能够参与有效运算的位只有与数组长度减一的位的长度,比如 数组长度为16,那么16-1的二进制为 1111,那么不管key的hash值有多大,最终参与运算的只有后4位,根据位与运算规则,运算结果的最大值为 1111,转换成十进制后即数组的长度减一,最小值为 0000,十进制为0,即结果的范围为 0 ~ size - 1,这个取模的结果是一致的。又因为数组的长度总是2的n次方,对应的二进制 为 1,11,111 ,11111等等,这也是为什么每次扩容时都要扩大至原来的两倍的原因。那么,另外一个问题又来了,为什么一定要时2的n次方呢?其他的值可以吗?下面我们来做一个实验:
public static void main(String[] args) {
for (int i = ; i < ; i++) {
System.out.print((i & ) + " ");
}
} 运算结果:
当我们使用 2 的n次方-1来运算时,每个余数都有可能得到
public static void main(String[] args) {
for (int i = ; i < ; i++) {
System.out.print((i & ) + " ");
}
}
运算结果:
当我们使用 非2的n次方运算时,看运算结果可以看到,有些值是不可能得到的,这样数组的某些位置就永远为空,不仅造成空间的浪费,同时也会大大的提高碰撞的概率。根据位与运算规则,很容易想到其中的原因,首先将13转成二进制:1101,在位与运算时,那个 0 位永远不参与运算,如上面的结果一样,2,3,6等数值是没有的。当且仅当二进制数字全为1的时候,才有可能所有的位都能计算,得到的结果才会更加均匀。这个很容易理解,想一下就明白了的。
第三步,根据生成的index去数组寻找位置,如果该位置为空直接将node放进去,如果不为空则调用equals方法判断key值是否一致,一致的话就替换成新值,否则寻找下个节点,最终在插入链表的时候会判断当前链表长度是否达到了转换成红黑树的条件(默认链表长度达到8时会转)。
第四步,数据put成功后判断当前存储的数据大小是否超过了 threshold * loadFactor 的值,超过了就会执行resize方法:
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? : oldTab.length;
int oldThr = threshold;
int newCap, newThr = ;
if (oldCap > ) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << ) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << ; // double threshold
}
else if (oldThr > ) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == ) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = ; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - )] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == ) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
进行扩容的时候将所有的node节点进行hash计算e.hash & (newCap - 1),这样的结果不是在原来的就是在 当前位置加原来threshold长度的位置。至此整个put操作结束。
get(Object key)的原理:
弄懂了put操作之后,其实get就很容易理解了,首先根据传入key找到index,然后再对应的位置上获取就行了。
最后,我看了HashMap源码之后,自己也手写了一个HashMap,不同之处在于我没有用到红黑树,而是使用二叉树代替,经测试插入1千万条uuid所需时间都差粗多,都是在二十几秒左右。关于二叉树与红黑树的区别可以自行百度,红黑树最主要解决的问题是在极端的情况下二叉树只有一条路径,时间复杂度位O(N),红黑树为了避免这种情况,每次都会自动调节树的深度,将最坏的情况的时间复杂度降低到O(logN)。
因为完全是手写的,所以可能代码的可读性不是很好,但是基本的功能都能够实现了。如果大家有兴趣的话,可以下载过来看一下,也欢迎大家指出错误或提意见。项目地址: https://github.com/rainple1860/MyCollection
jdk源码阅读笔记-HashMap的更多相关文章
- jdk源码阅读笔记-LinkedHashMap
Map是Java collection framework 中重要的组成部分,特别是HashMap是在我们在日常的开发的过程中使用的最多的一个集合.但是遗憾的是,存放在HashMap中元素都是无序的, ...
- jdk源码阅读笔记-HashSet
通过阅读源码发现,HashSet底层的实现源码其实就是调用HashMap的方法实现的,所以如果你阅读过HashMap或对HashMap比较熟悉的话,那么阅读HashSet就很轻松,也很容易理解了.我之 ...
- jdk源码阅读笔记-ArrayList
一.ArrayList概述 首先我们来说一下ArrayList是什么?它解决了什么问题?ArrayList其实是一个数组,但是有区别于一般的数组,它是一个可以动态改变大小的动态数组.ArrayList ...
- jdk源码阅读笔记
1.环境搭建 http://www.komorebishao.com/2020/idea-java-jdk-funyard/ 2. 好的源码阅读资源 https://zhuanlan.zhihu.co ...
- jdk源码阅读笔记-Integer
public final class Integer extends Number implements Comparable<Integer> Integer 由final修饰了,所以该 ...
- JDK源码学习笔记——HashMap
Java集合的学习先理清数据结构: 一.属性 //哈希桶,存放链表. 长度是2的N次方,或者初始化时为0. transient Node<K,V>[] table; //最大容量 2的30 ...
- jdk源码阅读笔记-LinkedList
一.LinkedList概述 LinkedList的底层数据结构为双向链表结构,与ArrayList相同的是LinkedList也可以存储相同或null的元素.相对于ArrayList来说,Linke ...
- jdk源码阅读笔记-AbstractStringBuilder
AbstractStringBuilder 在java.lang 包中,是一个抽象类,实现 Appendable 接口和 CharSequence 接口,这个类的诞生是为了解决 String 类在创建 ...
- jdk源码阅读笔记-String
本人自学java两年,有幸初入这个行业,所以功力尚浅,本着学习与交流的态度写一些学习随笔,什么错误的地方,热烈地希望园友们提出来,我们共同进步!这是我入园写的第一篇文章,写得可能会很乱. 一.什么是S ...
随机推荐
- java面试题,附个人理解答案
一,面向对象的特征:1.抽象 包括数据抽象跟行为抽象,将对象共同的特征取出形成一个类2.继承 被继承类为基类/超类,继承类为子类/派生类3.封装 多次使用道德数据或方法,封装成类,方便多次重复调用4. ...
- id为194024的进程当前未运行
.NET MVC 编译运行时 提示 >> id为194024的进程当前未运行 << 清理解决方案,重新运行
- MySQL的日志(二):事务日志
本文目录:1.redo log 1.1 redo log和二进制日志的区别 1.2 redo log的基本概念 1.3 日志块(log block) 1.4 log group和redo log fi ...
- polysh安装与使用-同时查看多台机器日志
polysh简介 polysh 是一个交互式命令,可以在一台服务器上批量的对一批服务器进行处理,运行交互式命令.官方的简介如下: Polysh is a tool to aggregate sever ...
- Visual Studio 和 c# 正则表达式
今天集中说说VS生产环境下的正则. Visual Sturdio 2012以上版本查找替换 对于VS的正则,准确说,是VS2012之后的IDE下VS的正则. VS的查找和替换功能支持基础的正则表达式, ...
- 凸包问题——Graham Scan
Graham Scan 概述: 对于凸多边形的定义不在这里做详细叙述,这里给出算法的实现原理. Step 1: 找出x值最小的点的集合,从其中找出y值最小的点作为初始点 Step 2: 获得新序列后, ...
- 40多行python代码开发一个区块链。
40多行python代码开发一个区块链?可信吗?我们将通过Python 2动手开发实现一个迷你区块链来帮你真正理解区块链技术的核心原理.python开发区块链的源代码保存在Github. 尽管有人认为 ...
- 加密原理介绍,代码实现DES、AES、RSA、Base64、MD5
阅读目录 github下载地址 一.DES对称加密 二.AES对称加密 三.RSA非对称加密 四.实际使用 五.关于Padding 关于电脑终端Openssl加密解密命令 关于网络安全的数据加密部分, ...
- socket编程中客户端常用函数
1 常用函数 1.1 connect() int connect(int sockfd, const struct sockaddr *servaddr, socklen_taddrlen); 客 ...
- “史上更难就业季”暴露出啥隐情?
如果说,2013年中国高校毕业生达到699万,被称为"史上最难就业季".那么2014年将成为去年之后的"更难就业季".据最新资料显示,2014年应届大学毕业 ...