文章出处:【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的更多相关文章

  1. jdk源码阅读笔记-LinkedHashMap

    Map是Java collection framework 中重要的组成部分,特别是HashMap是在我们在日常的开发的过程中使用的最多的一个集合.但是遗憾的是,存放在HashMap中元素都是无序的, ...

  2. jdk源码阅读笔记-HashSet

    通过阅读源码发现,HashSet底层的实现源码其实就是调用HashMap的方法实现的,所以如果你阅读过HashMap或对HashMap比较熟悉的话,那么阅读HashSet就很轻松,也很容易理解了.我之 ...

  3. jdk源码阅读笔记-ArrayList

    一.ArrayList概述 首先我们来说一下ArrayList是什么?它解决了什么问题?ArrayList其实是一个数组,但是有区别于一般的数组,它是一个可以动态改变大小的动态数组.ArrayList ...

  4. jdk源码阅读笔记

    1.环境搭建 http://www.komorebishao.com/2020/idea-java-jdk-funyard/ 2. 好的源码阅读资源 https://zhuanlan.zhihu.co ...

  5. jdk源码阅读笔记-Integer

    public final class Integer extends Number implements Comparable<Integer> Integer 由final修饰了,所以该 ...

  6. JDK源码学习笔记——HashMap

    Java集合的学习先理清数据结构: 一.属性 //哈希桶,存放链表. 长度是2的N次方,或者初始化时为0. transient Node<K,V>[] table; //最大容量 2的30 ...

  7. jdk源码阅读笔记-LinkedList

    一.LinkedList概述 LinkedList的底层数据结构为双向链表结构,与ArrayList相同的是LinkedList也可以存储相同或null的元素.相对于ArrayList来说,Linke ...

  8. jdk源码阅读笔记-AbstractStringBuilder

    AbstractStringBuilder 在java.lang 包中,是一个抽象类,实现 Appendable 接口和 CharSequence 接口,这个类的诞生是为了解决 String 类在创建 ...

  9. jdk源码阅读笔记-String

    本人自学java两年,有幸初入这个行业,所以功力尚浅,本着学习与交流的态度写一些学习随笔,什么错误的地方,热烈地希望园友们提出来,我们共同进步!这是我入园写的第一篇文章,写得可能会很乱. 一.什么是S ...

随机推荐

  1. 关于.net 保存 decimal类型数据到SQLServer2012数据库时自动取整的问题

    公司同事问我有没有遇到过decimal类型数据入库时,会自动取整的问题(比如12.3入库后值是12,12.8入库后值是13,入库后自动四舍五入自动取整): 之前就遇到过从数据去decimal类型数据时 ...

  2. php中获取用户登陆的IP地址以及常规处理

    本文为原创,转载请注明!  在我们开发多站点业务网站中,经常需要获取客户端的ip地址来给用户推荐其所在地址的信息的业务,用php获取客户端的ip地址,我们一般用到的PHP内置方法是$_SERVER[' ...

  3. C#实现的HttpGet请求

    话不多说,代码贴上: /// <summary> /// HTTP Get请求 /// </summary> /// <param name="url" ...

  4. Spring+Redis的部署与Redis缓存使用示例

    由于项目的业务需要,这两天折腾了一下Spring-redis配置,有了前面用Spring托管hibernate的经验,这次可以说是顺风顺水,大概说一下流程. ubuntu 安装 redis sudo ...

  5. cocos2d-x 欢乐捕鱼游戏总结

    这几天一直都在做一个捕鱼游戏Demo,大概花掉了我快一个礼拜的时间.游戏主体是使用的cocos2d-x高级开发教程里面提供的小部分框架基本功能.然后自己加入所有的UI元素和玩法.变成了一个体验不错的捕 ...

  6. Java程序算法设计视频分享,需要的来

    每年都会有人说,IT行业饱和了,根本就找不到工作,其实,我想说的是,不是工作难找,而是你自己不够好! 前几天看到一CEO在微博上吐槽: 前几天招一算法工程师我们给了8万月薪*14+奖金,人家去阿里拿5 ...

  7. Cookie与 Session使用详解

    Cookie概念 在浏览某些 网站 时,这些网站会把 一些数据存在 客户端 , 用于使用网站 等跟踪用户,实现用户自定义 功能. 是否设置过期时间: 如果不设置 过期时间,则表示这个 Cookie生命 ...

  8. Base64 image

    [前端攻略]:玩转图片Base64编码 什么是 base64 编码? 我不是来讲概念的,直接切入正题,图片的 base64 编码就是可以将一副图片数据编码成一串字符串,使用该字符串代替图像地址. 这样 ...

  9. .NET Orm 性能测试

    .NET Orm 性能测试 简介 OrmBenchmark 这个项目主要是为了测试主要的Orm对于 SqlServer 数据库的查询并将数据转换成所需 POCO 对象的耗时情况(好吧,实际上不完全or ...

  10. 一文看懂 Github

    GitHub 介绍 GitHub 是为开发者构建的一个开发平台.GitHub 是一个受开发者工作方式启发的开发平台,从开源到商业,能够在上面进行托管和查看代码.管理项目和数百万其他开发人员一起开发软件 ...