HashMap 内部原理
HashMap 内部实现
通过名字便可知道的是,HashMap 的原理就是散列。HashMap内部维护一个 Buckets 数组。每一个 Bucket 封装为一个 Entry<K, V> 键值对形式的链表结构。这个 Buckets 数组也称为表。表的索引是 密钥K 的散列值(散列码)。
例如以下图所看到的:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXloMzUyMDkxNjI2/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">
链表的每一个节点是一个名为 Entry<K,V> 的类的实例。 Entry 类实现了 Map.Entry 接口,以下是Entry类的代码:
private static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
final int hash;
V value;
Entry<K,V> next;
}
注: 每一个 Entry 对象仅与一个特定 key 相关联。但其 value 是能够改变的(假设同样的 key 之后被又一次插入不同的 value) - 因此键是终于的,而值不是。 每一个Entry对象都有一个名为 next 的字段,它指向下一个Entry,所以实际上为单链表结构。hash 字段存储了 Entry 对象在 Buckets 数组索引,也就是 key 的散列值。
假设发生Hash碰撞,也就是两个key的hash值同样,或者假设这个元素所在的位子上已经存放有其它元素了,那么在同一个位子上的元素将以链表的形式存放,新增加的放在链头,最早增加的放在链尾。
影响 HashMap 性能的两个因素是初始容量和负载因子。容量是表数组的长度,初始容量仅仅是创建哈希表时的容量。负载因子是衡量哈希表在容量自己主动增加之前是否同意获取的量度(比例)。
当散列表中的 Entry 数量超过负载因子和当前容量的乘积时,将会又一次散列该表(也就是重建内部数据结构)。使得散列表具有大约两倍的容量(这个事实上和ArrayList相似)。
理解 put() 方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key); // 计算hash值
int i = indexFor(hash, table.length); // 计算在数组中的索引
// 遍历链表
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// hash值同样而且key相等,就直接替换
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i); // 否则就增加到链表
return null;
}
注:这个计算出来的hash值被传递给内部哈希函数,哈希函数将返回密钥的散列值。这个值就是 bucket/数组 的索i引。
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);
}
这里就有个疑问了,我们怎样计算相应存储数组索引,首先想到的就是把hashcode对数组长度取模运算。也就是h%length,这样一来,元素的分布相对来说是比較均匀的。可是,“模”运算的消耗还是比較大的。能不能找一种更高速,消耗更小的方式那中?
首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。
看上去非常easy。事实上比較有玄机。比方数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。非常多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高。我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap訪问的性能最高。
例如以下图。左边两组是数组长度为16(2的4次方)。右边两组是数组长度为15。两组的hashcode均为8和9。可是非常明显,当它们和1110“与”的时候,产生了同样的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就须要遍历这个链表,得到8或者9,这样就减少了查询的效率。
同一时候。我们也能够发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这样的情况中,数组能够使用的位置比数组长度小了非常多。这意味着进一步增加了碰撞的几率。减慢了查询的效率!
上图參考自:http://blog.csdn.net/oqqYeYi/article/details/39831029
理解 get() 方法
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key); // 计算hash值
// 依据索引遍历链表,找出相等的key
for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
get 与 put 总结
以下总结了 put() 和 get() 发生的三个重要步骤:
- 通过调用 计算 Hash Code 方法计算密钥的哈希码。
- 将计算的散列码传递到内部散列函数
indexFor()以获取表的索引。 - 迭代通过在索引处出现的链表,并调用
equals()方法来查找匹配键。
所以在这之前要先理解 equals() 和 hashCode() 这两个方法。
在 Java8 中的改善
在Java 8中,对HashMap有一个性能上的改进。当密钥中存在很多哈希冲突(不同的密钥终于具有同样的哈希值或索引)时。平衡树将用于存储 Entry 对象,而不是链表。做法是。一旦 bucket 中的 Entry 数量增长超过某一阈值。则 bucket 将从 Entry 链表切换到平衡树。
HashMap 内部原理的更多相关文章
- HashMap实现原理
学习笔记之HashMap篇,简单学习了解HashMap的实现原理和扩容. 大家都知道HashMap处理数据很快,时间复杂度O(1),那么是怎么做到的呢?那就先了解一下常见数据结构. 一般来说,我们把存 ...
- 理解HashMap的原理
HashMap内部数据结构 HashMap内部采用数组和链表结合的方式来存取数据(见下图).这种方式有什么好处呢? 我们知道,数组操作对于检索是O(1)的,能够很快的根据数组的下标定位对 ...
- HashMap实现原理及源码分析之JDK8
继续上回HashMap的学习 HashMap实现原理及源码分析之JDK7 转载 Java8源码-HashMap 基于JDK8的HashMap源码解析 [jdk1.8]HashMap源码分析 一.H ...
- java基础之hashcode理解及hashmap实现原理及MD5
HashCode值 1. hashcode值是int的,64位.int hashCode(). 2. java object类默认的hashcode()计算方法是根据对象的内存地址来计算的.所以可由此 ...
- 深入分析 JDK8 中 HashMap 的原理、实现和优化
HashMap 可以说是使用频率最高的处理键值映射的数据结构,它不保证插入顺序,允许插入 null 的键和值.本文采用 JDK8 中的源码,深入分析 HashMap 的原理.实现和优化.首发于微信公众 ...
- Java HashMap实现原理 源码剖析
HashMap是基于哈希表的Map接口实现,提供了所有可选的映射操作,并允许使用null值和null建,不同步且不保证映射顺序.下面记录一下研究HashMap实现原理. HashMap内部存储 在Ha ...
- hashMap的原理
hashMap的原理分析(转载) 1.总结: HashMap是基于哈希表实现的,用Entry[]来存储数据,而Entry中封装了key.value.hash以及Entry类型的next HashMap ...
- HashMap实现原理一步一步分析(1-put方法源码整体过程)
各位同学大家好, 今天给大家分享一下HashMap内部的实现原理, 这一块也是在面试过程当中基础部分被问得比较多的一部分. 想要搞清楚HashMap内部的实现原理,我们需要先对一些基本的概念有一些了解 ...
- [翻译]Java HashMap工作原理
大部分Java开发者都在使用Map,特别是HashMap.HashMap是一种简单但强大的方式去存储和获取数据.但有多少开发者知道HashMap内部如何工作呢?几天前,我阅读了java.util.Ha ...
随机推荐
- android 检查能否上网
文章一: 首先在,AndroidManifest.xml 中增加访问权限: <uses-permission android:name="android.permission.ACCE ...
- c#中的??运算符
注意啦,c#中的??运算符是和?:运算符是不同的,这两者是有区别的. ??运算符称为null合并运算符,用于定义null值的类型和引用类型的默认值. 只当运算符的左操作数不为 null,此运算符将返回 ...
- 扩展 jQuery datebox控件按钮
功能需求: 自定义扩展,将原先的datebox控件按钮进行自定义的扩展: 1.问题: 对原先的时间按钮控件进行更改扩展,新增 “一刻钟” “半小时” “一小时” 选项. 获取原先的 datebox 对 ...
- iOS:quartz2D绘图(显示绘制在PDF上的图片)
quart2D既可以用来绘制图像到pdf上,也可以从pdf上读取图像并显示出来.在使用这种方式之前,还有一种方式可以用来读取显示pdf上的图像,即使用UIWebView网页视图控件- (void)lo ...
- :视频播放器与Handler 完美调用
Handler之消息循环 Handler是用于操作线程内部的消息队列的类.这有点绕,没关系,我们慢慢的来讲.前面Looper一篇讲到了Looper是用于给线程创建消息队列用的,也就是说Looper ...
- [置顶] struts2+hibernate+spring整合(annotation版)
本博文使用struts2,hibernate,spring技术整合Web项目,同时分层封装代码,包含model层,DAO层,Service层,Action层. 在整合hibernate时使用annot ...
- C# SQLite 创建数据库的方法增删查改语法和命令
SQLite介绍 SQLite是一个开源.免费的小型RDBMS(关系型数据库),能独立运行.无服务器.零配置.支持事物,用C实现,内存占用较小,支持绝大数的SQL92标准. SQLite数据库官方主页 ...
- Rails 枚举
Rails Model中使用枚举有两种方案,一种是rails内置的enum,一种使用enumerize这个gem,不管哪种都能达到相同的目的. 首先介绍第一种: 一. enum 基本使用方法,以一个案 ...
- Python 类变量 实例变量
类变量: 是可在类的所有实例之间共享的值(也就是说,它们不是单独分配给每个实例的).例如下例中,num_of_instance 就是类变量,用于跟踪存在着多少个Test 的实例. 实例变量: 实例 ...
- 细数Python的特殊方法一:集合类
1. 特殊方法汇总列表 2. 仿集合类 __len__:返回对象的长度 __getitem__:获得某一项的值 __setitem__:设置莫一项的值 __delitem__删除某一项 __c ...