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. Unity3D 避免玩家作弊

    如果你的Unity项目快上线了,我强烈建议你看一下Anti-Cheat这个插件.因为IOS和Android分别越狱和Root后玩家可以使用 @八门神器 @烧饼修改器 等一些列作弊的软件来修改游戏内存, ...

  2. 调用wsdl的接口-用axis

    // 创建一个服务(service)调用(call) org.apache.axis.client.Service service = new org.apache.axis.client.Servi ...

  3. OSC的原理

    OSC是Online Schema Change简写,即在线架构改变.其实现步骤: 1. init,即初始化阶段,会对创建的表做一些验证工作,如检查表是否有主键,是否存在触发器或者外键等.2. cre ...

  4. [转]ASP.NET Core 1.0: Using Entity Framework Core 1.0 - Transaction

    本文转自:http://blog.csdn.net/alvachien/article/details/51576961 跟Entity Framework之前的版本不同,Class DbContex ...

  5. pyPdf - 用Python方便的处理PDF文档

    pyPdf库 ( http://pybrary.net/pyPdf/ ) ,操作起来相当直接易懂,把代码贴在这儿,做个记录.  1 from pyPdf import PdfFileWriter, P ...

  6. 流畅的python第九章符合Python风格的对象学习记录

    对象表示形式 每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式.Python提供了两种方式 repr()便于开发者理解的方式返回对象的字符串表示形式 str()便于用户理解的方式返回对 ...

  7. 3dsMAX 插件

    SDK C++ 对性能有要求 底层接口 MAXScript 上层接口 a few more function whick sdk does not afford MCG像蓝图一样的东西 http:// ...

  8. scrapy-splash抓取动态数据例子六

    一.介绍 本例子用scrapy-splash抓取中广互联网站给定关键字抓取咨询信息. 给定关键字:打通:融合:电视 抓取信息内如下: 1.资讯标题 2.资讯链接 3.资讯时间 4.资讯来源 二.网站信 ...

  9. ZOJ3622 Magic Number(水题)

    分析: 举个样例xxx(三位数)为魔力数,则xxx|(xxx+1000*y),那么xxx|1000,这个就是结论 同理:四位数xxxx|10000,五位数xxxxx|100000 代码: #inclu ...

  10. python处理csv

    python处理csv 学习了:https://blog.csdn.net/qq_33363973/article/details/78783481  竟然pip install csv 无果: 学习 ...