Java-HashMap原理解析
本文分析HashMap的实现原理。
数据结构(散列表)
HashMap是一个散列表(也叫哈希表),用来存储键值对(key-value)映射。散列表是一种数组和链表的结合体,结构图如下:
简单来说散列表就是一个数组(上图纵向),数组的每个元素是一个链表(上图横向),类似二维数组。链表的每个节点就是我们存储的key-value数据(源码中将key和value封装成Entry对象作为链表的节点)。
哈希算法
对于散列表,不管是存值还是取值,都需要通过Key来定位散列表中的一个具体的位置(即某个链表的某个节点),计算这个位置的方法就是哈希算法。
大概过程是这样的:
- 用Key的hash值对数组长度做取余操作得到一个整数,这个整数作为数组中的索引得到这个索引位置的链表。
- 得到链表之后,就可以存值和取值了。
如果是存值,直接把数据插入到链表的头部或者尾部即可(或者已存在就替换);
如果是取值,就遍历链表,通过key的equals方法找到具体的节点。
例如一个key-value对要存到上图的散列表里,假设key的哈希值是17,由图可知(纵向)数组长度是16,那么17对16取余结果是1,数组中索引1位置的链表是 1->337->353 ,所以这个key-value对存储到这个链表里面(插到头还是尾可能不同Java版本不一样)。如果是取值,就遍历这个链表,由于这个链表每个节点的key的哈希值都一样,所以根据equals方法来确定具体是哪个节点。
通过上面的哈希算法,可以有如下结论:
- 不同的key具体相同的哈希值叫做哈希冲突,HashMap解决哈希冲突的方法是链表法,将具有相同哈希值的key放在同一个链表中,然后利用key类的equals方法来确定具体是哪个节点。
- Key的唯一性是通过哈希值和equals方法共同决定的,所以想要用一个类作为HashMap的键,必须重写这个类的hashCode和equals方法。同理,HashSet是基于HashMap实现的,它没有重复元素的特点是利用HashMap没有重复键实现的。所以,Set集合里面的元素类,也必须同时实现hashCode方法和equals方法。
- HashMap存储的数据是无序的。
为什么HashMap大小是2的整数次幂的时候效率最高
哈希算法主要分两步操作:1.通过哈希值定位一个链表; 2.遍历链表,通过equals方法找到具体节点。为了使哈希算法效率最高,应该尽量让数据在哈希表中均匀分布,因为那样可以避免出现过长的链表,也就降低了遍历链表的代价。
如何保证均匀分布?前面的哈希算法说到,通过取余操作将Key的哈希值转换成数组下标,这样可以认为是均匀的。但是,源码中并没有直接用%操作符取余,而是使用了更高效的与运算,源码如下:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
这样就多了一些限制,因为只有当length是2的整数次幂的时候,h & (length-1) = h % length才成立。当然,如果length不是2的整数次幂,h & (length-1)的结果也一定比length小,将Key转换成数组下标也没什么问题,但是,这样会导致元素分布不均匀严重影响散列表的访问效率。看下面的一个示例代码:
解释一下图中的代码,随机生成一组Key,然后利用与运算,把key全部转换成一个数组容量的索引,这样就得到一组索引值,这组索引中不相同的值越多,说明分布越均匀,输出结果的result就是 “这组索引中不相同的值的数量”。
从运行结果来看,容量是64的时候相比于其他几个容量大小,分布是最均匀的。容量是65的时候,每次结果都是2,原因很简单,当容量是65的时候,下标=h&64,64的二进制是1000000,很明显,与它进行与运算的结果只有两种情况,0和64,也就是说,如果HashMap大小被指定成65,对于任意Key,只会存储到散列表数组的第0个或第64个链表中,浪费了63个空间,同时也导致0和64两个链表过长,取值的时候遍历链表的代价很高。容量66和67的结果是4同理。如果容量是64,那么下标=h&63,63的二进制是111111,每一位都是1,好处就是对于任意Key,与63做与运算的结果可能是1-63的任意数,很多Key的话自然就能分布均匀。
通过这个示例代码的分析就可以找到一个规律了,容量length=2^n 是分布最均匀,因为length-1的二进制每一位都是1;相反的length=2^n+1是分布最不均匀的,因为length-1的二进制中的1数量最少。
结论:HashMap大小是2的整数次幂的时候效率最高,因为这个时候元素在散列表中的分布最均匀。
从上面的分析来看,使用与运算虽然效率高了,但是增加了使用限制,如果用%取余的做法,那么对于任何大小的容量都能做到均匀分布,可以把图中代码int a = keySet[j] & (c - 1);
改成 int a = keySet[j] % c;
试一下。
HashMap的容量
通过上面的分析,容量是2的整数次幂的时候效率最高,那么很容易想到,如果随着数据量的增长,HashMap需要扩容的时候是2倍扩容,区别于ArrayList的1.5倍扩容。
那么什么时候扩容呢?首先说明一下,我们所说的HashMap的容量是指散列表中数组的大小,这个大小不能决定HashMap能存多少数据,因为只要链表足够长,存多少数据都没问题。但是,数据量很大的时候,如果数组太小,就会导致链表很长,get元素的效率就会降低,所以我们应该在适当的时候扩容。源码默认的做法是,当数据量达到容量的75%的时候扩容,这个值称为负载因子,75%应该是大量实验后统计得到的最优值,没有特殊情况不要通过构造方法指定为其他值。
扩容是有代价了,会导致所有已存的数据重新计算位置,所以,和ArrayList一样,当知道大概的数据量的时候,可以指定HashMap的大小尽量避免扩容,指定大小要注意75%这个负载因子,比如数据量是63个的话,HashMap的大小应该是128而不是64。
对于容量的计算,源码已经封装好了一个方法
/**
* 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;
}
此方法在HashMap的构造方法中被调用,所以指定容量的时候无需自己计算,比如数据量是63,直接new HashMap<>(63)
即可。
HashMap的遍历
前面提到一点,散列表中的链表的节点是Entry对象,通过Entry对象可以得到Key和Value。HashMap的遍历方法有很多,大概可以分为3种,分别是通过map.entrySet()、map.keySet()、map.values()三种方式遍历。比较效率的话,map.values()方式无法得到key,这里不考虑。比较map.entrySet()和map.keySet()的话,结合散列表的结构特点,很明显map.entrySet()直接遍历Entry集合(所有链表节点)取出Key和Value即可(一次循环),map.keySet()遍历的是Key,得到Key之后在通过Key去遍历相应的链表找到具体的节点(多个循环),所以前者效率高。
扩展:LinkedHashMap和LruCatch
对于LinkedHashMap的理解,我觉得一张图就够了:
在散列表的基础上加上了双向循环链表(图中黄色箭头和绿色箭头),所以可以拆分成一个散列表和一个双向链表,双向链表如下:
然后使用散列表操作数据,使用双向循环链表维护顺序,就实现了LinkedHashMap。
LinkedHashMap有一个属性可以设置两种排序方式:
private final boolean accessOrder;
false表示插入顺序,true表示最近最少使用次序,后者就是LruCatch的实现原理。
LinkedHashMap和LruCatch的具体实现细节这里就不分析了。
Java-HashMap原理解析的更多相关文章
- java集合框架之java HashMap代码解析
java集合框架之java HashMap代码解析 文章Java集合框架综述后,具体集合类的代码,首先以既熟悉又陌生的HashMap开始. 源自http://www.codeceo.com/arti ...
- 【转载】Java类加载原理解析
Java类加载原理解析 原文出处:http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html 1 基本信息 摘要: 每个j ...
- Java:HashMap原理与设计缘由
前言 Java中使用最多的数据结构基本就是ArrayList和HashMap,HashMap的原理也常常出现在各种面试题中,本文就HashMap的设计与设计缘由作出一一讲解,并解答面试常见的一些问题. ...
- 深入理解Java类加载器(一):Java类加载原理解析
摘要: 每个开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这个异常背后涉及到的是Java技术体系中的类加载机制.本文简述了JVM三种预定义类加载器,即 ...
- Java 7 和 Java 8 中的 HashMap原理解析
HashMap 可能是面试的时候必问的题目了,面试官为什么都偏爱拿这个问应聘者?因为 HashMap 它的设计结构和原理比较有意思,它既可以考初学者对 Java 集合的了解又可以深度的发现应聘者的数据 ...
- Java集合详解(三):HashMap原理解析
概述 本文是基于jdk8_271版本进行分析的. HashMap是Map集合中使用最多的.底层是基于数组+链表实现的,jdk8开始底层是基于数组+链表/红黑树实现的.HashMap也会动态扩容,与Ar ...
- Java类加载原理解析
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt229 2 Java虚拟机类加载器结构简述 2.1 JVM三 ...
- 深入理解Java类加载器(1):Java类加载原理解析
1 基本信息 每个开发人员对Java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载.Java的类加载机制是技术体系中比较核心的 ...
- Java HashMap原理
HashMap存储结构 HashMap中数据的存储是由数组与链表一起实现的 数组寻址非常容易,其时间复杂度为O(1),但是当要插入或删除数据时,时间复杂度就会变为O(n).链表插入和删除操作的内存复杂 ...
- 【转】Java类加载原理解析
原链接 1 基本信息 每个java开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载. Java的类加载机制是j ...
随机推荐
- TypeError: cannot use a string pattern on a bytes-like object
一劳永逸解决:TypeError: cannot use a string pattern on a bytes-like object TypeError: cannot use a string ...
- flask_关注者
表的模型实现 class Follow(db.Model): __tablename__ = 'follows' follower_id = db.Column(db.Integer,db.Forei ...
- 如何从Maven中央存储库下载?
根据 Apache Maven说明: 下载时由项目的 pom.xml 文件的依赖来决定,目前不在本地存储库触发(当中央存储库包含了一个更新).默认情况下,Maven将从中央存储库下载. 在Maven中 ...
- 用qemu+gdb tcp server+CDT调试linux内核启动-起步
用qemu+gdb tcp server+CDT调试linux内核启动-起步 说明: 环境信息与 用virtualbox+模拟串口+CDT调试linux内核 TCP IP协议栈-起步 提到的一样,并且 ...
- 计算n的阶乘(n!)末尾0的个数
题目: 给定一个正整数n,请计算n的阶乘n!末尾所含有“0”的个数. 举例: 5!=120,其末尾所含有的“0”的个数为1: 10!= 3628800,其末尾所含有的“0”的个数为2: 20!= 24 ...
- python + selenium + unittest 自动化测试框架 -- 入门篇
. 预置条件: 1. python已安装 2. pycharm已安装 3. selenium已安装 4. chrome.driver 驱动已下载 二.工程建立 1. New Project:建立自己的 ...
- 微信小程序开发 -- 获取当前页面路径
Page.prototype就是this: 你在任何一个Page里面都可以使用route字段和setData()函数: 示例代码: /** * 生命周期函数--监听页面加载 */ onLoad: fu ...
- python闭包函数、装饰器
闭包函数的传值方式: 方式1:通过参数传值 def func(x): print(x)func(1) 方式2:闭包函数传值 def outter(x): def inner(): print(x) r ...
- TOJ4203: Domino Piece
4203: Domino Piece Time Limit(Common/Java):1000MS/3000MS Memory Limit:65536KByteTotal Submit: 5 ...
- Oracle 用户和权限
Oracle 用户和权限Oracle 中,一般不会轻易在一个服务器上创建多个数据库,在一个数据库中,不同的项目由不同的用户访问,每一个用户拥有自身创建的数据库对象,因此用户的概念在 Oracle中非常 ...