转hashmap比较透彻的分析

wangdaijun wangdaijun     2022-12-20     411

关键词:

HashMap 的实现原理

众所周知,HashMap是用来存储Key-Value键值对的一种集合,这个键值对也叫做Entry,而每个Entry都是存储在数组当中,因此这个数组就是HashMap的主干。
HashMap数组中的每一个元素的初始值都是NULL 技术分享图片

Put方法的实现原理

HaspMap的一种重要的方法是put()方法,当我们调用put()方法时,比如hashMap.put("Java",0),此时要插入一个Key值为“Java”的元素,这时首先需要一个Hash函数来确定这个Entry的插入位置,设为index,即 index = hash("Java"),假设求出的index值为2,那么这个Entry就会插入到数组索引为2的位置。技术分享图片但是HaspMap的长度肯定是有限的,当插入的Entry越来越多时,不同的Key值通过哈希函数算出来的index值肯定会有冲突,此时就可以利用链表来解决。
其实HaspMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点,每一个Entry对象通过Next指针指向下一个Entry对象,这样,当新的Entry的hash值与之前的存在冲突时,只需要插入到对应点链表即可。
技术分享图片需要注意的是,新来的Entry节点采用的是“头插法”,而不是直接插入在链表的尾部,这是因为HashMap的发明者认为,新插入的节点被查找的可能性更大。

Get方法的实现原理

get()方法用来根据Key值来查找对应点Value,当调用get()方法时,比如hashMap.get("apple"),这时同样要对Key值做一次Hash映射,算出其对应的index值,即index = hash("apple")。
前面说到的可能存在Hash冲突,同一个位置可能存在多个Entry,这时就要从对应链表的头节点开始,一个个向下查找,直到找到对应的 Key值,这样就获得到了所要查找的键值对。
例如假设我们要找的Key值是"apple": 技术分享图片

第一步,算出Key值“apple”的hash值,假设为2。 第二步,在数组中查找索引为2的位置,此时找到头节点为Entry6,Entry6的Key值是banana,不是我们要找的值。 第三步,查找Entry6的Next节点,这里为Entry1,它的Key值为apple,是我们要查找的值,这样就找到了对应的键值对,结束。

HashMap的深入思考

上面所说的就是HashMap的基本原理,可以总结出HashMap的3个要素为:hash函数、数组、链表,如下图:技术分享图片接下来对于HaspMap还有很多深入的问题,比如: 1.HashMap默认的初始长度是多少?为什么这么规定? 2.高并发情况下,HashMap会出现死锁吗? 3.Java8中,HashMap有怎样的优化? 下面开始说明这几个问题:

HashMap的长度

1.HaspMap的默认初始长度是16,并且每次扩展长度或者手动初始化时,长度必须是2的次幂。之所以是16,是为了服务于从Key值映射到index的hash算法。前面说到了,从Key值映射到数组中所对应的位置需要用到一个hash函数:index = hash("Java");

那么为了实现一个尽量分布均匀的hash函数,利用的是Key值的HashCode来做某种运算。因此问题来了,如何进行计算,才能让这个hash函数尽量分布均匀呢?

一种简单的方法是将Key值的HashCode值与HashMap的长度进行取模运算,即 index = HashCode(Key) % hashMap.length,但是,但是!这种取模方式运算固然简单,然而它的效率是很低的, 而且,如果使用了取模%, 那么HashMap在容量变为2倍时, 需要再次rehash确定每个链表元素的位置,浪费了性能。 因此为了实现高效的hash函数算法,HashMap的发明者采用了位运算的方式。那么如何进行位运算呢?可以按照下面的公式:

index = HashCode(Key) & (hashMap.length - 1); 

接下来我们以Key值为“apple”的例子来演示这个过程:

1) 计算“apple”的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。

2) HashMap默认初始长度是16,计算hashMap.Length-1的结果为十进制的15,二进制的1111。

3) 把以上两个结果做 与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。

可以看出来,hash算法得到的index值完全取决与Key的HashCode的最后几位。这样做不但效果上等同于取模运算,而且大大提高了效率。

那么回到最初的问题,初始长度为什么是16或者2的次幂?如果不是会怎么样?

我们假设HaspMap的初始长度为10,重复前面的运算步骤:技术分享图片

单独看这个结果,表面上并没有问题。我们再来尝试一个新的HashCode 101110001110101110 1011 :技术分享图片

然后我们再换一个HashCode 101110001110101110 1111 试试 :技术分享图片这样我们可以看到,虽然HashCode的倒数第二第三位从0变成了1,但是运算的结果都是1001。也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111)!

所以这样显然不符合Hash算法均匀分布的原则。

而长度是16或者其他2的次幂,Length – 1的值的所有二进制位全为1(如15的二进制是1111,31的二进制为11111),这种情况下,index的结果就等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。这也是HashMap设计的玄妙之处。

HashMap的死锁

我们知道HashMap是非线程安全的,那么原因是什么呢?

由于HashMap的容量是有限的,如果HashMap中的数组的容量很小,假如只有2个,那么如果要放进10个keys的话,碰撞就会非常频繁,此时一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷。

为了解决这个问题,HashMap设计了一个阈值,其值为容量的0.75,当HashMap所用容量超过了阈值后,就会自动扩充其容量。

在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。

具体发生死锁的过程可以参考这篇文章:Java HashMap 的死循环(HashMap Infinite Loop)

技术分享图片

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!

本文原文出处:业余草: » HashMap 的实现原理

(转)hashmap分析

原文地址:http://www.cnblogs.com/ITtangtang/p/3948406.htmlHashMap的数据结构  HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来... 查看详情

hashmap与hashtable源码学习及效率比较分析

...见解,后续一次依次进行分析:    1、线程安全:HashMap是非线程安全的,HashTable是线程安全的(HashTable中使用了synchronized关键字进行控制),HashMap对应的线程安全的有concurrentHashMap,但如果不用concurrentHashMap的话,也可以... 查看详情

linkedhashmap和hashmap的比较使用(转)

由于现在项目中用到了LinkedHashMap,并不是太熟悉就到网上搜了一下。import java.util.HashMap;import java.util.Iterator;import java.util.LinkedHashMap;import java.util.Map;public class TestLinkedHashM 查看详情

全网把map中的hash()分析的最透彻的文章,别无二家。(代码片段)

你知道HashMap中hash方法的具体实现吗?你知道HashTable、ConcurrentHashMap中hash方法的实现以及原因吗?你知道为什么要这么实现吗?你知道为什么JDK7和JDK8中hash方法实现的不同以及区别吗?如果你不能很好的回答这些问题,那么你需... 查看详情

转:hashmap实现原理分析(面试问题:两个hashcode相同的对象怎么存入hashmap的)(代码片段)

...y[].length得到的index相同,会不会有覆盖的危险?  这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方,第一个键值对A进 查看详情

java集合框架hashmap简单使用以及深度分析(转)

java.util 类HashMap<K,V>java.lang.Object java.util.AbstractMap<K,V>   java.util.HashMap<K,V>类型参数:K-此映射所维护的键的类型V-所映射值的类型所有已实现的接口: Serializable,Cloneable,M 查看详情

java中hashset,hashmap和hashtable的区别(转)

HashMap、HashSet、HashTable之间的区别是Java程序员的一个常见面试题目,在此仅以此博客记录,并深入源代码进行分析:在分析之前,先将其区别列于下面1:HashSet底层采用的是HashMap进行实现的,但是没有key-value,只有HashMap的keyset的... 查看详情

hashmap源码分析

...下来我要讲的是那个HashSet,要明白HashSet就必须先要明白HashMap,所以在此出穿插一篇hashMap的文章,为了更好的学习HashSet。个人感觉初次看HashMap源码比较难,但是明白了,其实也不是很难,                 ... 查看详情

分析轮子-hashmap.java之概念梳理

...承关系图,这样能够比较清楚的知道此类的相关特性二:HashMap.java的代码比较难看,所以,我看了几天,写的话也分开来写,这样能表达的更清晰,HashMap.java的底层数据结构,本质是单向链表数组,如下所示是单向链中节点的结... 查看详情

源码分析:hashmap(代码片段)

写在前面作为以key/value存储方式的集合,HashMap可以说起到了极大的作用。因此关于HashMap,我们将着重使用比较大的篇幅。接下来会用到的几个常量staticfinalintDEFAULT_INITIAL_CAPACITY=1&lt;&lt;4;staticfinalintMAXIMUM_CAPACITY=1&lt;&lt;... 查看详情

几种常见容器比较和分析hashmap,map,vector,list...hashtable

转自:http://www.haogongju.net/art/1543058list支持快速的插入和删除,但是查找费时;vector支持快速的查找,但是插入费时。map查找的时间复杂度是对数的,这几乎是最快的,hash也是对数的。 如果我自己写,我也会用二叉检索树,它... 查看详情

hashmap源码分析基于1.8(代码片段)

1、个人总结及想法:(1)1.8相比较于1.7的变化?HashMap的底层数据结构大家应该都比较清楚了,就是数组+链表,链表主要用来解决hash冲突,使用了链地址法的方式来解决,1.8的改动主要就是hash冲突时候,一是在进行链表插入时... 查看详情

hashmap实现原理分析

1.HashMap的数据结构数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。     数组数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);... 查看详情

hashmap实现原理分析

1.HashMap的数据结构数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。     数组数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);... 查看详情

hashmap实现原理分析

1.HashMap的数据结构数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。     数组数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);... 查看详情

arraylist、linkelist、hashmap和sparsearray数据结构透彻的理解

...tlength:要copy的数组的长度它的特点就是,查找慢,增加快HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系 查看详情

hashmap的数据结构分析

jdk提供的HashMap作为一个性能很不错的集合类,其内部结构是如何的呢?以上的解决方案的思想是集合数组和链表各自的优点结合成为一种数据结构,当发生hash冲突后,从图中可以看出hashmap采用了拉链结构解决。... 查看详情

hashmap实现原理分析

1.HashMap的数据结构数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。数组数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容... 查看详情