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 继承/实现

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

2.2 静态变量

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

2.2 数据存储结构

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
transient Node<K,V>[] table;

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

2.3 哈希值计算

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 位与运算
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 初始化-构造函数

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

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

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;
}

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方法详解

public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
} /**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

根据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. Quartz和Spring Task定时任务的简单应用和比较

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

  2. WAF集成:Acunetix和FortiWeb

    Acunetix API使您有机会自动化任务以提高效率,尤其是在您可以加速与工作流其他组件的集成功能时.在此示例中,我们将在上一篇文章的基础上,向您展示如何在Bash脚本中使用Acunetix API ...

  3. ARTS第五周

    -第五周.这两周在复习大学里的课程,发现当时觉得课上很多看不懂的,现在看起来轻松多了,也带来了新的感悟. 1.Algorithm:每周至少做一个 leetcode 的算法题2.Review:阅读并点评 ...

  4. nodejs安装+vue安装

    一.nodejs安装 电脑win7的,nodejs V12.16.2以前的版本支持win7 nodejs下载地址: http://mirrors.nju.edu.cn/nodejs/v12.15.0/ ...

  5. MapReduce处理数据1

    学了一段时间的hadoop了,一直没有什么正经练手的机会,今天老师给了一个课堂测试来进行练手,正好试一下. 项目已上传至github:https://github.com/yandashan/MapR ...

  6. [009] - JavaSE面试题(九):集合之Set

    第一期:Java面试 - 100题,梳理各大网站优秀面试题.大家可以跟着我一起来刷刷Java理论知识 [009] - JavaSE面试题(九):集合之Set 第1问:List和Set的区别? List ...

  7. 2018年成为Web开发者的路线图

    本文通过一组大图展示了Web开发技能图谱,给出了作为Web 开发者可以采取的路径,以及总结了想要成为Web工程师的朋友们.希望和大家一起交流分享 介绍 Web 开发的角色一般说来,包括前端.后端和de ...

  8. DIV+css排版问题技巧总结---v客学院技术分享

                DIV+css排版问题技巧总结 一.排版思路 1.从上到下,从左到右,从大到小. 2.首先确定排版分区,排除色块分布,然后再从简单的部分开始. 3.在某一块内将HTML部分写好 ...

  9. mybatis-6-动态sql

    动态sql简介&OGNL了解 动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处 理器相似. MyBatis 采用功能强大的基于 OGNL 的表达式来简化操作. if cho ...

  10. Markdown 样式美化大全

    Markdown 样式大全 目录 Markdown 样式大全 1. 键盘 2. 路径 3. 彩色字体背景 4. 折叠 5. 锚点链接 原生锚点1 原生锚点2 Hello Hello 6. 待办列表 7 ...