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() 发生的三个重要步骤:

  1. 通过调用 计算 Hash Code 方法计算密钥的哈希码。
  2. 将计算的散列码传递到内部散列函数indexFor()以获取表的索引。
  3. 迭代通过在索引处出现的链表,并调用 equals() 方法来查找匹配键。

所以在这之前要先理解 equals()hashCode() 这两个方法。

在 Java8 中的改善

在Java 8中,对HashMap有一个性能上的改进。当密钥中存在很多哈希冲突(不同的密钥终于具有同样的哈希值或索引)时。平衡树将用于存储 Entry 对象,而不是链表。做法是。一旦 bucket 中的 Entry 数量增长超过某一阈值。则 bucket 将从 Entry 链表切换到平衡树。

HashMap 内部原理的更多相关文章

  1. HashMap实现原理

    学习笔记之HashMap篇,简单学习了解HashMap的实现原理和扩容. 大家都知道HashMap处理数据很快,时间复杂度O(1),那么是怎么做到的呢?那就先了解一下常见数据结构. 一般来说,我们把存 ...

  2. 理解HashMap的原理

    HashMap内部数据结构        HashMap内部采用数组和链表结合的方式来存取数据(见下图).这种方式有什么好处呢? 我们知道,数组操作对于检索是O(1)的,能够很快的根据数组的下标定位对 ...

  3. HashMap实现原理及源码分析之JDK8

    继续上回HashMap的学习 HashMap实现原理及源码分析之JDK7 转载 Java8源码-HashMap  基于JDK8的HashMap源码解析  [jdk1.8]HashMap源码分析 一.H ...

  4. java基础之hashcode理解及hashmap实现原理及MD5

    HashCode值 1. hashcode值是int的,64位.int hashCode(). 2. java object类默认的hashcode()计算方法是根据对象的内存地址来计算的.所以可由此 ...

  5. 深入分析 JDK8 中 HashMap 的原理、实现和优化

    HashMap 可以说是使用频率最高的处理键值映射的数据结构,它不保证插入顺序,允许插入 null 的键和值.本文采用 JDK8 中的源码,深入分析 HashMap 的原理.实现和优化.首发于微信公众 ...

  6. Java HashMap实现原理 源码剖析

    HashMap是基于哈希表的Map接口实现,提供了所有可选的映射操作,并允许使用null值和null建,不同步且不保证映射顺序.下面记录一下研究HashMap实现原理. HashMap内部存储 在Ha ...

  7. hashMap的原理

    hashMap的原理分析(转载) 1.总结: HashMap是基于哈希表实现的,用Entry[]来存储数据,而Entry中封装了key.value.hash以及Entry类型的next HashMap ...

  8. HashMap实现原理一步一步分析(1-put方法源码整体过程)

    各位同学大家好, 今天给大家分享一下HashMap内部的实现原理, 这一块也是在面试过程当中基础部分被问得比较多的一部分. 想要搞清楚HashMap内部的实现原理,我们需要先对一些基本的概念有一些了解 ...

  9. [翻译]Java HashMap工作原理

    大部分Java开发者都在使用Map,特别是HashMap.HashMap是一种简单但强大的方式去存储和获取数据.但有多少开发者知道HashMap内部如何工作呢?几天前,我阅读了java.util.Ha ...

随机推荐

  1. CSS3 @media 查询,根据屏幕screen大小调节前端显示;媒体查询方法的使用

    ------------------- 1.媒体查询方法在 css 里面这样写 -------------------- @media screen and (min-width: 320px) an ...

  2. [转]SSIS ConnectionManager.ConnectionString Property

    本文转自:http://msdn.microsoft.com/en-us/library/microsoft.sqlserver.dts.runtime.connectionmanager.conne ...

  3. iOS:quartz2D绘图 (动画)

    quartz2D可以用来绘制自己需要的图形,它们绘制出来的是一个静态的图形,那么如何绘制一个动态的图形呢?动态的图形就是动画,所谓动画,其实就是很多张图片在短时间内不停的切换所产生的一种视觉效果.qu ...

  4. unity mipmap 糊

    unity 开miapmap会糊很多 尤其在editor里面 我估计和editor的 tempRT resolution 957x380有关 -----确实是这样 手机上糊的程度低很多 中间rt我用的 ...

  5. 更改Mysql数据库存储位置

    默认安装位置 C:\Program Files\MySQL\MySQL Server 5.7 一.首先把mysql的服务先停掉. 二.更改MySQL配置文件My.ini中的数据库存储主路径 打开MyS ...

  6. wifi简单笔记

    什么是wifi: Wi-Fi是一种可以将个人电脑.手持设备(如PDA.手机)等终端以无线方式互相连接的技术.Wi-Fi是一个无线网路通信技术的品牌,由Wi-Fi联盟(Wi-Fi Alliance)所持 ...

  7. Android微信支付V3版

    由于公司需求做微信APP支付,在集成过程中也遇到各种问题,比如说签名错误,body编码必须为UTF-8.APP端无法调用支付页面直接到支付结果页面.结果为null,code=-1等等: 1.签名错误问 ...

  8. AWS RDS mysql无法连接的问题

    rds创建后,无法连接mysql 检查安全组规则是否配置了 1. 2. 这样你的EC2就可以访问了.如果还不行,检查数据库是否和EC2在同一 VPC内. 官方文档:https://docs.amazo ...

  9. JMeter 十:录制脚本--使用bodboy

    1. 下载bodboy 下载地址:http://www.badboy.com.au/download 这里填写完基本信息,点击下方的Continue即可跳转到下载页面. 任选一个version,点击后 ...

  10. vue - 选项

    1.计算属性(computed):主要是对原数据进行改造输出.改造输出:包括格式化数据(价格,日期),大小写转换,排序,添加符号 2.methods(methods):用于绑定html中的事件对应的方 ...