java.util.HashMap是最常用的java容器类之一, 它是一个线程不安全的容器. 本文对JDK1.8.0中的HashMap实现源码进行分析.

HashMap使用位运算巧妙的进行散列并使用链地址法处理冲突. 自JDK1.8后, 若表中某个位置元素数超过阈值 则会将其自动转换为红黑树来提高检索效率.

HashMap中的迭代器同样采用fail-fast机制, 即若迭代过程中容器发生结构性改变, 则会终止迭代.

HashMap主要有三个视图接口keySet(), values(), entrySet(). 它们都是基于迭代器实现的, 并不实际存储数据.

哈希表

自JDK1.8.0开始HashMap使用静态内部类Node来存储键值对结构, 不再使用Map.Entry:

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) {...} public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) {...} public final boolean equals(Object o) {...}
}

注意Node.next使得Node可以形成单向链表结构. 再来看一下HashMap中的主要字段:

transient Node<K,V>[] table;
transient int size;

HashMap的底层数据结构是存储在table域中的哈希表(Hash Table, 又称散列表). 哈希表是存储键值对的数组, 在查找元素时根据键的值计算出键值对在数组中的位置, 不需要扫描数组.

哈希表类似于词典, 可以通过词条快速地找出释义的位置, 不必从头开始逐个寻找. 哈希表访问元素的时间复杂度为O(1), 远高于普通数组的O(n)或树状结构的O(logn).

最简单的哈希函数自然是key.hashCode() % table.length, 这就引出了哈希表固有的哈希冲突问题.

table.length为16, key1.hashCode()为1202, key2.hashCode()为3218. 那么,key1key2的哈希值同为2, 但是table[2]只能放置一个元素于是产生了哈希冲突.

哈希冲突问题主要从两方面考虑, 一是尽量减少哈希冲突的发生, 二是在哈希冲突发生后仍然正常工作.

哈希函数

HashMap根据Node.key计算出Nodetable数组中的位置, 但是并没有采用上文提及最简单的哈希函数:

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash函数的代码非常简练, 我们稍微改写一下:

static final int hash(Object key) {
if(key == null) {
return 0;
} else {
int h = key.hashCode();
return h ^ (h >>> 16);
}
}

关键的位运算h ^ (h >>> 16), 将32位整数h逻辑右移16位后与原值进行异或操作:

h = 0xf0f00f0f:
1111 0000 1111 0000 0000 1111 0000 1111
h >>> 16 = 0x0000f0f0:
0000 0000 0000 0000 1111 0000 1111 0000
h ^ (h >>> 16) = 0xf0f0ffff
1111 0000 1111 0000 1111 1111 1111 0000

HashMapNodetable数组中的实际位置为(n - 1) & hash. n为当前table.length, HashMap的扩容机制保证n为2的整数次幂, 因此(n - 1) & hash == hash % n, 取n=16示例:

n - 1 = 15
0000 0000 1111
hash = 2562
1010 0000 0010
(n - 1) & hash = 2
0000 0000 0010

由于n一般较小, 当n < 65535时高16位为0. 若HashMap采用key.hashCode() % n来决定键值对的位置, 则hashCode()的高16位对结果产生影响较小.

高16位很可能不参与运算意味着产生哈希冲突的可能性增大, 因此HashMap先让高16位与低16位进行异或计算, 减少了哈希冲突的可能性.

链地址法

在实践中无论使用什么哈希函数仍然存在冲突的可能性, 因此必须设计合适的机制在发生冲突后仍然能够正常工作. 常用的方法有开放地址法和链地址法.

使用开放地址法的哈希表每个位置只能放一个元素. 当发生哈希冲突时, 按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止.

典型的如线性再散列: H = (e.hashCode + n) % table.length, 其中n为再散列的次数, 即发生第1次冲突时需要再散列时n = 1.

开放地址法的缺点在于再散列占据了哈希表中另一个位置, 增加了后续操作中发生哈希冲突的可能性.

HashMap采用了另一种冲突解决方案 - 链地址法. 即哈希表中每个位置是一个链表, 允许放置多个元素. 发生哈希冲突时, 新元素只需添加到链表尾即可.

注意到Node.next域可以让Node连接为一个单链表, 即可使用链地址法解决哈希冲突.

若链表长度过长仍会造成查询效率降低, 在JDK1.8中的HashMap实现中若某个位置链表长度达到阈值TREEIFY_THRESHOLD = 8则会将链表变形为红黑树. 当删除元素使红黑树中元素数低于UNTREEIFY_THRESHOLD = 6时会变回链表.

本文将在添加元素一节中详细介绍链地址法的实现.

构造器

ArrayList中的构造器类似, HashMap的构造器只是计算并写入参数, 当第一次添加元素时才会实际分配存储空间:

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);
} public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

三个构造器主要是设置initialCapacityloadFactor参数. initialCapcitytable的初始大小; 当元素数达到threshold时, HashMap会执行扩容.

loadFactor是影响threshold的参数:threshold = table.length * loadFactor. loadFactor默认为0.75, 这是在空间利用率和执行效率之间比较平衡的取值.

int tableSizeFor(cap)方法的返回值是大于cap的最小的2的整数幂. 注意到构造器只是设置了threshold, 保证在初次扩容时达到initialCapacity并没有实际分配存储空间.

添加元素

首先阅读添加单个键值对的put方法:

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
} final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// resize方法用于检查空间足够和扩容
// 构造器只指定了参数并没有实际分配空间, 此处调用resize的目的是分配初始空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 若没有发生哈希冲突, 直接添加一个Node对象
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 若键相同, 则对值进行更新
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 若该位置已经存在一个红黑树节点, 则将新元素添加到树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 若该位置存在一个链表, 则先查找链表中是否存在相同键
// 若存在相同键则更新值
// 若不存在相同键则添加节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 若添加后达到阈值, 则将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 发现相同键
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 存在相同键, 更新值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 添加元素后再次检查是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

然后阅读进行扩容的resize方法, HashMap的扩容并不是简单地创建一个更大的table并把原来的元素复制过去.

因为table.length发生了变化, 所以哈希地址hash(key) % table.length也会随之变化, 因此需要重新计算哈希地址. 除了保证正确索引外, 重新计算哈希值也可以将一个链表分散为多个较短的链表, 提高索引效率.

resize()的扩容策略为2倍扩容, 因为原大小为2的整数次幂, 扩容后仍然保持该性质使基于位运算的哈希函数不会失效.

容量变为2倍使哈希地址增加了1位, 原来哈希地址相同的元素将会根据新增位的0-1取值被分散到两个两个地址中.

如当容量为16时, 5 % 16, 21 % 16得到的哈希地址均为5, 容量加倍后5 % 32 = 5; 21 % 32 = 21. 注意到5的二进制表示00101与21的二进制表示10101仅有最高位不同.

计算最高位的取值非常简单, 若e.hashCode < oldCapacity则最高位取0, 否则最高位取1. 因为 oldCapacity是2的整数幂(二进制形式为1000...), 所以可以用e.hashCode & oldCapacity = 0代替e.hashCode < oldCapacity.

HashMap的实现采用了上述位运算策略将哈希表中的链表一分为二, 而避免重新计算哈希位置的开销.

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 计算新的容量, 默认为原容量的2倍
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 指定初始容量的构造器并没有实际分配缓冲区, 而是将大小写入threshold域
// 根据构造器写入的参数分配初始空间
else if (oldThr > 0)
newCap = oldThr;
else {
// 没有将初始容量写入threshold则按默认值分配
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 确保newThr被正确计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 创建新的哈希表
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 若只有一个元素, 重新计算哈希值
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 处理红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 处理链表,
Node<K,V> loHead = null, loTail = null; // low
Node<K,V> hiHead = null, hiTail = null; // high
Node<K,V> next;
do {
next = e.next;
// 若新哈希值最高位为0, 则添加到lo链表中
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 若新哈希值最高位为1, 则添加到hi链表中
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

批量添加元素的putAll(map)方法通过map.entrySet获得要添加的元素, 然后调用putVal方法逐个添加元素.

查找元素

在了解HashMap的数据结构和添加元素策略之后, 查找元素的实现也不难理解:

public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
} 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 &&
((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;
}

删除元素

删除元素同样考虑了单节点, 链表和树三种情况:

public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
} final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 确定哈希表非空, 且目标位置非空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 若目标位置的第一个节点即为要删除的节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 在红黑树中寻找要删除的节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 在链表中寻找要删除的节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 删除树节点
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 删除单个节点
else if (node == p)
tab[index] = node.next;
// 删除链表中的节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}

Java8 HashMap源码分析的更多相关文章

  1. 【JAVA集合】HashMap源码分析(转载)

    原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...

  2. Java中HashMap源码分析

    一.HashMap概述 HashMap基于哈希表的Map接口的实现.此实现提供所有可选的映射操作,并允许使用null值和null键.(除了不同步和允许使用null之外,HashMap类与Hashtab ...

  3. JDK1.8 HashMap源码分析

      一.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时 ...

  4. HashMap源码分析和应用实例的介绍

    1.HashMap介绍 HashMap 是一个散列表,它存储的内容是键值对(key-value)映射.HashMap 继承于AbstractMap,实现了Map.Cloneable.java.io.S ...

  5. 【Java】HashMap源码分析——常用方法详解

    上一篇介绍了HashMap的基本概念,这一篇着重介绍HasHMap中的一些常用方法:put()get()**resize()** 首先介绍resize()这个方法,在我看来这是HashMap中一个非常 ...

  6. 【Java】HashMap源码分析——基本概念

    在JDK1.8后,对HashMap源码进行了更改,引入了红黑树.在这之前,HashMap实际上就是就是数组+链表的结构,由于HashMap是一张哈希表,其会产生哈希冲突,为了解决哈希冲突,HashMa ...

  7. Java BAT大型公司面试必考技能视频-1.HashMap源码分析与实现

    视频通过以下四个方面介绍了HASHMAP的内容 一. 什么是HashMap Hash散列将一个任意的长度通过某种算法(Hash函数算法)转换成一个固定的值. MAP:地图 x,y 存储 总结:通过HA ...

  8. Java源码解析——集合框架(五)——HashMap源码分析

    HashMap源码分析 HashMap的底层实现是面试中问到最多的,其原理也更加复杂,涉及的知识也越多,在项目中的使用也最多.因此清晰分析出其底层源码对于深刻理解其实现有重要的意义,jdk1.8之后其 ...

  9. HashMap源码分析(史上最详细的源码分析)

    HashMap简介 HashMap是开发中使用频率最高的用于映射(键值对 key value)处理的数据结构,我们经常把hashMap数据结构叫做散列链表: ObjectI entry<Key, ...

随机推荐

  1. NopCommerce 3. Controller 分析

    1. 继承关系,3个abstract类 System.Web.Mvc.Controller Nop.Web.Framework.Controllers.BaseController Nop.Admin ...

  2. ios 指定页面禁用第三方键盘,使用系统的键盘

    因为项目需要,需要在添加银行卡和提现页面使用数字键盘, 如果用户没有安装第三方键盘是没啥大问题的,但是如果用户手机安装了第三方的键盘的话,有时候会无法调用起第三方的数字键盘,或者第三方键盘样式不符合, ...

  3. win10 uwp 九幽图床

    本文主要是图片加水印自动上传,代码已经上传github. 图片加水印,我的方法,简单,一个好的方法是毒逆天大神的图片加水印方法或http://daily.zhihu.com/story/8812028 ...

  4. Ubuntu下使用网易云音乐

    Ubuntu15真心各种崩溃啊 最后决定还是换成ubuntu14.04LTS了 在win.android平台上网易云音乐好用到爆 ubuntu下没有网易云音乐的客户端怎么能行 https://gith ...

  5. WebService的简单运用添加删除

    WebService是一种跨编程语言和跨操作系统平台的远程调用技术,简单来说就是将数据存储到项目的文件夹下 .NET中基于DOM核心类 XmlDocument 表示一个XML文档 XmlNode表示X ...

  6. C++点滴20130724

    warning c4627:"#include stdafx.h":在查找预编译头使用时跳过 原因: 1.没有添加 #include "stdafx.h" 2. ...

  7. Arduino.最小系统面包板搭建

    最早试过用万用板做过最小系统,主要用来烧录芯片 后来为了方便,用面包板也搭了一个最小系统, 但不采用杜邦线,因为飞来飞去的线太乱了 因此就有了这个简洁的版本,先上个成品图 用个烧录器就可以很方便的烧写 ...

  8. Nodejs.安装.非源码方式安装Node.js (Centos)

    已验证的适用环境: Centos6.x 树莓派官方ROM(Raspbian) 先去官网下载已编译好的安装包 https://nodejs.org/en/download/current/​ 以Cent ...

  9. 安利一波那个叫做 hutool 的通用工具类库

    摘自3.1.1版本作者发布原话,当时看到有点说不上的情绪,为作者的坚持.热爱点个赞. 已经想不起来是怎样结识 Hutool 的,但 Hutool 伴随几个项目的推进,获得了同事一致好评. 没经过实践和 ...

  10. LeetCode 617. Merge Two Binary Tree (合并两个二叉树)

    Given two binary trees and imagine that when you put one of them to cover the other, some nodes of t ...