1.数据结构

1.7

数组+链表,键值对是以Entry内部类的数组存放的。键计算得到哈希值是该数组的下标。又称桶数组当存在哈希冲突时,会通过Entry类内部的成员变量 Entry<k,v> next; 形成一个链表,哈希值相同的元素会以头插法添加到链表中,即拉链法。

1.7有两个主要弊端

  • 头插法在多并发情况下,扩容使会导致两个线程中出现元素的互相指向而形成循环链表,在执行 get() 时会触发死循环而消耗CPU资源
  • 链表的搜索时间复杂度时O(n),不太好。

1.8

  • 数组+链表+红黑树,具体是当链表节点数的大于8(树化阈值),且数组的长度大于最小树化容量64时,链表就会树化成红黑树,将查询的时间复杂度维持在O(logN)级别;
  • 采用尾插法,避免了并发扩容后面get产生的死循环;
  • 使用继承了Entry的Node来作为键值对的存储内部类

2.HashMap(1.8)详解

2.1 继承/实现

  1. public class HashMap<K,V> extends AbstractMap<K,V>
  2. implements Map<K,V>, Cloneable, Serializable {}

2.2 静态变量

  1. /**
  2. * The default initial capacity - MUST be a power of two.
  3. */
  4. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  5. static final int MAXIMUM_CAPACITY = 1 << 30;
  • 默认桶数组初始容量:16(1 << 4,必须是 \(2^{n}\) ) ,最大 \(2^{30}\)
  1. static final float DEFAULT_LOAD_FACTOR = 0.75f; //扩容加载因子
  2. static final int TREEIFY_THRESHOLD = 8; //树化阈值
  3. static final int UNTREEIFY_THRESHOLD = 6; //链表化阈值
  4. static final int MIN_TREEIFY_CAPACITY = 64; //树化容量阈值

2.2 数据存储结构

  1. static class Node<K,V> implements Map.Entry<K,V> {
  2. final int hash;
  3. final K key;
  4. V value;
  5. Node<K,V> next;
  6. ...
  7. }
  8. transient Node<K,V>[] table;

内部静态类 Node 继承了 Entry,保存了键值对,hash值,下一个节点的指针。然后组成了 table数组。

2.3 哈希值计算

  1. static final int hash(Object key) {
  2. int h;
  3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  4. }
  5. // 位与运算
  6. i = (n-1) & hash // 更快的取余

HashMap 可以存储Null,强制将其放在索引为0的桶内。当key不是null时,首先正常调用 hashCode 计算得到int类型(4个字节,32位),然后将 位右移运算 >> 高位32移到低位16,再和自己做 ^ 位异或运算,得到 hash码,再和 (n-1) 做 位与 运算得到最终的 数组下标位置。

& 与运算:两个位都为1时,结果才为1;

^ 或运算:两个位相同为0,相异为1;

上述处理被称为 扰动函数,目的是使得计算的哈希值更加均匀,增加随机性,降低哈希冲突的概率。具体解释:如果直接用32位的 hashcode来与 15=16-1 做位于运算,那么Hash值在高位就全部被舍弃了,全部为0,哈希冲突就会很严重。但是如上述 扰动函数处理后,通过 位右移,将32位高位半区和低位半区做异或,混合后低位掺杂了高位的差异特征,相当于保留了高位的差异信息,由此增加了低位的随机性。使得后面计算得到 i下标更加均匀。用另一句话来总结就是

因为有些数据计算出的哈希值差异主要在高位,而HashMap里的哈希寻址时忽略容量以上高位,扰动处理就可以有效避免类似情况下的哈希碰撞

这里还隐藏一个问题,那就是为什么n必须是 2的幂,因为 \(2^{n}-1\) 用二进制表示位数从第一个非空的位数往后都是1,没有0位,做与运算每一位就有两种可能,如果是其他的数据,那么就会有很多0位,与运算后只有一种可能,大大限制了索引i的范围。因此目的还是让桶数组中的桶分布的更加均匀一些。

2.4 初始化-构造函数

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. if (initialCapacity < 0)
  3. throw new IllegalArgumentException("Illegal initial capacity: " +
  4. initialCapacity);
  5. if (initialCapacity > MAXIMUM_CAPACITY)
  6. initialCapacity = MAXIMUM_CAPACITY;
  7. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  8. throw new IllegalArgumentException("Illegal load factor: " +
  9. loadFactor);
  10. this.loadFactor = loadFactor;
  11. this.threshold = tableSizeFor(initialCapacity);
  12. }

如果没有指定初始容量和加载因子,默认是加载因子 0.75,容量=16,此时并没有对 存储容器table的初始化,只有第一次 put 值的时候才会,因此是 Lazy_load,这里对table的初始化使用的是 resize() 方法初始化长16的数组。如果指定了 容量,那么最终实际的容量会调用 tableSizefor() 将容量设置位大于设置值最小的 \(2^{n}\)。

  1. static final int tableSizeFor(int cap) {
  2. int n = cap - 1;
  3. n |= n >>> 1;
  4. n |= n >>> 2;
  5. n |= n >>> 4;
  6. n |= n >>> 8;
  7. n |= n >>> 16;
  8. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  9. }

2.5 Resize 扩容

Resize兼初始化和扩容两个功能(本质上是一个,初始化也是扩容,默认的话是从0扩容到16)。主要参数是加载因子,如果当前桶数组元素已经超过 capacity * loadFactor ,就会扩容两倍

扩容之后,将原数组的元素计算其新的 索引并放入,新的索引可能是原来的索引 oldIndex ,也可能是 oldIndex+ oldCapacity(原数组容量),这里可以参考 赵小发的解释

进入到第 39 行代码,也是这篇文章最重要的链表的 rehash 方法。在分析之前,我们来思考一下,为什么原先在 index 为 2 位置上的元素要么停留在原位,要么跑到 index 为 18 的位置上呢?我们来举个例子:

假设张三的 hash 的后 5 位是 00010(在这种情况下,只有后 5 位比较重要,如果不知道为什么的同学先看看我之前的 hash 优化算法的那篇文章)。

0 0010(张三的hash)

0 1111(n - 1 = 16 -1 = 15)

0 0010

这是在扩容前(初始容量16),张三算出来的 index 是 2 。如果此时扩容,新容量为 32 ,那么 rehash 的结果为

0 0010(张三的hash)

1 1111(n - 1 = 32 -1 = 31)

0 0010

rehash 的结果,index 还是 2,位置不变。

那如果张三的 hash 的后 5 位是 10010 呢?

1 0010(张三的hash)

0 1111(n - 1 = 16 -1 = 15)

0 0010

即 10010 & 15 = 2 ,那此时扩容 rehash 后呢?

1 0010 (张三的hash)

1 1111(n - 1 = 16 -1 = 15)

1 0010

1 0010 的十进制是 18 。我们思考一个问题,18 有什么特殊的含义呢?

18 = 2 + 16 = 2 + oldCap 。我们找到了一个规律,在 rehash 之后,要么停留在原位置,要么移到原 index + 原数组长度的位置上。其实很容易理解,初始化和 (16 - 1)做与运算时,只有低 4 位的 hash 是有意义的,但是扩容的时候,和 (32 -1 )做与运算时,低 5 位也参与了运算,所以低 5 位的值决定了 rehash 后新的 index 的值。如果低 5 位为 0 ,index 值不变,如果低 5 位为1,则 index 改变,并且在原先基础上加了 2 的 4 次方,即 16 。

依次类推,如果从 32 扩容到 64 ,则低 6 位决定了 index 是否改变,如果改变,在原先基础上加 2 的 5 次方,即 32 。

综上所述:在 resize 方法中,数组 rehash 时,要么停留在原位,要么移到 oldIndex + oldCap 的位置上。

2.7 Put方法

  1. 检查是否初始化,若未初始化,则先调用 resize() 初始化;
  2. 对key求 Hashcode,再计算得到索引;
  3. 如果无哈希冲突,则加入桶中,如果冲突,则尾插入链表中;
  4. 如果链表长度超过树化阈值8,且容量超过64,就将链表转换为红黑树,如果链表长度低于6,则将红黑树转回链表,;
  5. 如果节点的key已经存在,则更新value;
  6. 如果容量没有超过64,容量超过总容量的0.75,就要扩容。

2.8 Get方法

参考jackMan-HashMap之get方法详解

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. return (e = getNode(hash(key), key)) == null ? null : e.value;
  4. }
  5. /**
  6. * Implements Map.get and related methods.
  7. *
  8. * @param hash hash for key
  9. * @param key the key
  10. * @return the node, or null if none
  11. */
  12. final Node<K,V> getNode(int hash, Object key) {
  13. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  14. if ((tab = table) != null && (n = tab.length) > 0 &&
  15. (first = tab[(n - 1) & hash]) != null) {
  16. if (first.hash == hash && // always check first node
  17. ((k = first.key) == key || (key != null && key.equals(k))))
  18. return first;
  19. if ((e = first.next) != null) {
  20. if (first instanceof TreeNode)
  21. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  22. do {
  23. if (e.hash == hash &&
  24. ((k = e.key) == key || (key != null && key.equals(k))))
  25. return e;
  26. } while ((e = e.next) != null);
  27. }
  28. }
  29. return null;
  30. }

根据key使用前面 的扰动函数 hash 计算哈希值,然后调用 getNode获取node,如果node为null,返回null,否则返回node.val。

在getNode()中,(n-1)*hash获取索引,即桶位置,然后判断首节点是否为空以及key是否等。如果首节点不是,则使用 遍历链表的用 equals 判断 key是否相等。如果是红黑树,则调用 getTreeNode去搜索。

如果使用对象作为key,最好覆写keyhashcodeequals方法

补充:

虽然HashMap 1.8使用尾插法避免了死循环,但是HashMap线程不安全,并发下应该使用ConcurrentHashMap 或者 HashTable。

如有错误,欢迎讨论指正。

参考

  1. 赵小发的解释
  2. jackMan-HashMap之get方法详解
  3. 阿进写字台-HashMap是如何工作的
  4. Sam哥哥-HashMap的put和get方法原理

HashMap底层实现-基础的更多相关文章

  1. HashMap底层结构、原理、扩容机制

    https://www.jianshu.com/p/c1b616ff1130 http://youzhixueyuan.com/the-underlying-structure-and-princip ...

  2. 最简单的HashMap底层原理介绍

    HashMap 底层原理  1.HashMap底层概述 2.JDK1.7实现方式 3.JDK1.8实现方式 4.关键名词 5.相关问题 1.HashMap底层概述 在JDK1.7中HashMap采用的 ...

  3. HashMap底层原理

    原文出自:http://zhangshixi.iteye.com/blog/672697 1.    HashMap概述: HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射 ...

  4. HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别(转)

    HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别 文章来源:http://www.cnblogs.com/beatIteWeNerverGiveU ...

  5. hashMap 底层原理+LinkedHashMap 底层原理+常见面试题

    1.源码 java1.7    hashMap 底层实现是数组+链表 java1.8 对上面进行优化  数组+链表+红黑树 2.hashmap  是怎么保存数据的. 在hashmap 中有这样一个结构 ...

  6. HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别

    ①HashMap的工作原理 HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象.当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算h ...

  7. hashMap底层put和get方法逻辑

    1.hashmap put方法的实现: public V put(K key, V value) { if (key == null) return putForNullKey(value); int ...

  8. ArrayList、LinkedList、HashMap底层实现

    ArrayList 底层的实现就是一个数组(固定大小),当数组长度不够用的时候就会重新开辟一个新的数组,然后将原来的数据拷贝到新的数组内. LinkedList 底层是一个链表,是由java实现的一个 ...

  9. 操作系统内核Hack:(二)底层编程基础

    操作系统内核Hack:(二)底层编程基础 在<操作系统内核Hack:(一)实验环境搭建>中,我们看到了一个迷你操作系统引导程序.尽管只有不到二十行,然而要完全看懂还是需要不少底层软硬件知识 ...

随机推荐

  1. Flask(8)- jinja2 模板入门

    前言 之前的文章有个栗子,视图函数可以直接返回一段 html 代码,浏览器可以自动渲染 但是当你的 HTML 非常复杂的话,也要整串写在代码里面吗,这显然不合理的,可阅读性也非常差 所以,就诞生了 J ...

  2. 深入理解 SynchronizationContext

    深入理解 SynchronizationContext 目录 深入理解 SynchronizationContext SynchronizationContext(后续以SC简称) 是什么? 1.1 ...

  3. (转发)forward与(重定向)redirect的区别

    (转发)forward与(重定向)redirect的区别 forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器.浏览器根本不知道服 ...

  4. Quartz和Spring Task定时任务的简单应用和比较

    看了两个项目,一个用的是Quartz写的定时器,一个是使用spring的task写的,网上看了2篇文章,写的比较清楚,这里做一下留存 链接一.菠萝大象:http://www.blogjava.net/ ...

  5. vue(17)vue-route路由管理的安装与配置

    介绍 Vue Router 是 Vue.js官方的路由管理器.它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌.包含的功能有: 嵌套的路由/视图表 模块化的.基于组件的路由配置 路由参 ...

  6. c++ vector用法详解

    1. 定义: 向量(Vector)是一个封装了动态大小数组的顺序容器(Sequence Container)可以认为是一个动态数组,其中一个vector中的所有对象都必须是同一种类型的. 2. 构造函 ...

  7. Selenium 自动化测试中对页面元素的value比较验证 java语言

    源代码: public boolean verifyText(String elementName, String expectedText) {String actualText = getValu ...

  8. C语言:统计字符个数及种类

    #include <stdio.h> int main(){ char c; //用户输入的字符 int shu=0;//字符总数 int letters=0, // 字母数目 space ...

  9. Installation failed with message INSTALL_FAILED_TEST_ONLY问题

    Android Studio连接手机进行app调试,遇到如下问题: Installation failed with message INSTALL_FAILED_TEST_ONLY. It is p ...

  10. 数据库里的回车字符导致取过来的json字符串不规范的问题

    转发:https://bbs.csdn.net/topics/380192638 你可以报保存数据库之前,进行 替换 str = str.Replace("\r\n"," ...