引言

HashMap在JDK1.8和1.7中差异较大,在JDK1.8中HashMap引入了红黑树,优化减少了哈希冲突,提高了哈希表的存取效率。

本篇文章分析的就是JDK1.8中的HashMap源码。

继承与实现

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

我们打开源码,首先看到的就是HashMap的结构,它继承了AbstractMap抽象类,实现了MapCloneableSerializable接口,而AbstractMap也实现了Map接口,具体继承实现关系如下图所示:

Map与Collection并列存在,使用Entry<K, V>接口保存具有映射关系的数据:key-value,也就是我们常说的键值对。

AbstractMap对于大部分Map中的方法进行了实现,其中多了两个变量:ketSetvalues,其中keySet是key值的一个集合,values是value的一个集合,在方法keySet()中会把keySet变量返回,在values()中会会把values变量返回。keySet方法源码如下,values与此类似:

public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public K next() {
return i.next().getKey();
}
public void remove() {
i.remove();
}
};
}
public int size() {
return AbstractMap.this.size();
}
public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}
public void clear() {
AbstractMap.this.clear();
}
public boolean contains(Object k) {
return AbstractMap.this.containsKey(k);
}
};
keySet = ks;
}
return ks;
}

Cloneable和Serializable这两个接口都是标记接口,Cloneable用于标记该类可以被克隆,只有实现这个接口后,然后在类中重写Object中的clone方法,然后通过类调用clone方法才能进行克隆,而Serializable则是表示这个类可以被序列化。

常量属性

    // 初始默认容量,这个是给table数组的初始化大小,默认为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // table数组存放元素的最大数量,2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30; // 负载因子表示的是一个散列表空间的使用程度
// 默认负载因子为0.75,负载因子=表中元素/表长度
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 链表转成红黑树的阈值,默认为8
static final int TREEIFY_THRESHOLD = 8; // 红黑树转为链表的阈值,默认为6
static final int UNTREEIFY_THRESHOLD = 6; // 最小转化成红黑树的容量,默认为64
// 注意,需要TREEIFY_THRESHOLD和MIN_TREEIFY_CAPACITY同时达到要求才会转为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;

变量属性

    // 用于存放键值对的数组,在JDK1.7中是Entry<k, v>[]
transient Node<K,V>[] table; // 用于存放Entry类型的元素集合
transient Set<Map.Entry<K,V>> entrySet; // 哈希表中元素的个数
transient int size; // 扩容和map结构更改的计数器
transient int modCount; // 扩容的阈值
int threshold; // 哈希表的负载因子
final float loadFactor;

Node详细实现如下:

    // 每个数组中都存储一个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) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
} public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; } // hashcode方法将key的哈希值和value的哈希值进行异或
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
} public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 重写equals方法
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

构造方法

    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;
// 指定数组容量为一个大于传入数组容量且最接近2的整数次幂的数
// 指定2的整数次幂是为了方便使用异或实现扰动函数
this.threshold = tableSizeFor(initialCapacity);
} // 调用第一个构造函数,传入数组容量,同时指定负载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} // 空构造函数,只指定负载因子为0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
} // 将一个map放入到一个新的map对象中
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}

添加put方法

public V put(K key, V value) {
// 这里面 提供了一个hash()方法 这个方法的一个主要作用 就是计算出当前key的hash值
return putVal(hash(key), key, value, false, true);
} // hash 寻址
static final int hash(Object key) {
int h;
// 将我们的hash值右移16位 减少hash碰撞
// 即使高位为0,异或特性: 0 异或任何数 都为任何数它本身 ,那就是返回key.hashCode() ,不会影响它本身
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

putVal方法源码:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 若是第一次插入,table数组为空长度为0
// 使用resize()方法初始化数组,初始容量为16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 进行取余操作,(n - 1) & hash就等于hash%length
// 若取到的下标没有元素,则就在这个下标存放元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 否则就会发生哈希冲突
Node<K,V> e; K k;
// 判断哈希值和key是否都相同,若都相同则将Entry元素p赋给恶,到倒数第二个if语句中进行替换操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断是否是树节点,是就用putTreeVal将元素插入红黑树中
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) // -1 for 1st
// 判断是否要转为红黑树
treeifyBin(tab, hash);
break;
}
// 找到hash值相同且key相同的元素时跳出遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
// 保留原有的value
V oldValue = e.value;
// 替换value
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 将节点移动到最后
afterNodeAccess(e);
// 返回原有的value
return oldValue;
}
}
// 计数器加一,元素个数加一
++modCount;
// 判断是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

treeifyBin 树化方法

final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 判断数组 是否为空 以及 容量 是否大于 我们的 阈值容量 MIN_TREEIFY_CAPACITY=64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 扩容方法
resize();
// 否则 就 树化
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
// node 转为 TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}

扩容resize方法

    final Node<K,V>[] resize() {
// 先将原数组给oldTab
Node<K,V>[] oldTab = table;
// 给oldCap赋值数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr用于记录原数组的扩容阈值
int oldThr = threshold;
// 新数组长度和扩容阈值
int newCap, newThr = 0;
if (oldCap > 0) { // 原数组中有元素,说明不是初始化,需要扩容
// 与数组容量最大值比较,大于等于则返回Integer最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容两倍后的容量与最大容量比较,不超过则赋值给newThr
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 指定了数组容量构造map的时候会进入
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 初始化的时候进入,默认容量16,阈值12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新数组的扩容阈值
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"})
// 确定了新数组长度和阈值后,需要创建新数组,遍历原数组重新计算hash值,把原数组中的元素给放到新数组中的新索引上
// 创建一个大小为newCap的Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// table指向新数组
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 判断旧数组j下标上有没有元素,没有就跳过,有就遍历链表上元素
// 旧数组中改下表元素置为null便于回收
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 { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
// 把e的下一个节点给next方便走到下一步
next = e.next;
// e的哈希最高位为0,那么在新数组中索引不变
if ((e.hash & oldCap) == 0) {
// 节点保存
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// e的hash值最高位是1,那么在新数组中索引就变为旧索引+旧数组的长度
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null); // e为null时说明遍历完毕
// 把索引不变的节点放到新数组中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 把索引变化的节点放到新数组中
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新数组
return newTab;
}

删除remove方法

    public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}

removeNode的源码:

    final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// tab表示数组,p表示节点,n表示数组长度,index表示数组下标
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 若数组存在且长度大于0,并且tab[index]这个位置有元素,进入if
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;
// 当第一个就是要删除的元素时,直接node=p
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; // key值相等,跳出循环
}
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;
}

获取get方法

    public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode方法源码:

    final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 在数组存在长度大于0,同时hash(key)对应的索引有元素才进入循环
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 第一个元素就是要找的,直接返回first
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 { // 普通节点找到key值相等后就返回该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 没有找到就返回null
return null;
}

以上就是JDK1.8中HashMap的主要源码啦!

HashMap源码解析-JDK18的更多相关文章

  1. 【转】Java HashMap 源码解析(好文章)

    ­ .fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wra ...

  2. HashMap源码解析 非原创

    Stack过时的类,使用Deque重新实现. HashCode和equals的关系 HashCode为hash码,用于散列数组中的存储时HashMap进行散列映射. equals方法适用于比较两个对象 ...

  3. Java中的容器(集合)之HashMap源码解析

    1.HashMap源码解析(JDK8) 基础原理: 对比上一篇<Java中的容器(集合)之ArrayList源码解析>而言,本篇只解析HashMap常用的核心方法的源码. HashMap是 ...

  4. 最全的HashMap源码解析!

    HashMap源码解析 HashMap采用键值对形式的存储结构,每个key对应唯一的value,查询和修改的速度很快,能到到O(1)的平均复杂度.他是非线程安全的,且不能保证元素的存储顺序. 他的关系 ...

  5. HashMap源码解析和设计解读

    HashMap源码解析 ​ 想要理解HashMap底层数据的存储形式,底层原理,最好的形式就是读它的源码,但是说实话,源码的注释说明全是英文,英文不是非常好的朋友读起来真的非常吃力,我基本上看了差不多 ...

  6. 详解HashMap源码解析(下)

    上文详解HashMap源码解析(上)介绍了HashMap整体介绍了一下数据结构,主要属性字段,获取数组的索引下标,以及几个构造方法.本文重点讲解元素的添加.查找.扩容等主要方法. 添加元素 put(K ...

  7. HashMap 源码解析

    HashMap简介: HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表. ...

  8. 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析

    前面了解了jdk容器中的两种List,回忆一下怎么从list中取值(也就是做查询),是通过index索引位置对不对,由于存入list的元素时安装插入顺序存储的,所以index索引也就是插入的次序. M ...

  9. 【Java深入研究】9、HashMap源码解析(jdk 1.8)

    一.HashMap概述 HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现.与HashTable主要区别为不支持同步和允许null作为key和value.由于HashMap不是线程 ...

  10. HashMap 源码解析(一)之使用、构造以及计算容量

    目录 简介 集合和映射 HashMap 特点 使用 构造 相关属性 构造方法 tableSizeFor 函数 一般的算法(效率低, 不值得借鉴) tableSizeFor 函数算法 效率比较 tabl ...

随机推荐

  1. 带你一起看看nginx如何部署安装

    nginx部署安装 Linux安装 源码构建Nginx 管理器安装 windows安装 首先需要下载Nginx软件包 nginx软件官方下载地址: nginx官方下载连接 建议选择稳定的软件版本,如果 ...

  2. 串(C语言实现)

    文章目录 1.串的数据类型定义 数据对象 1.1 数据关系 1.2 基本操作 2.串的存储结构 2.1 串的顺序存储 2.2 串的链式存储 3.串的模式匹配算法 3.1BF 算法 3.2KMP 算法 ...

  3. PostgresSQL创建一个只读用户

    create user readonlyuser with password 'R3333333341'; grant select on all tables in schema public to ...

  4. 工作中的技术总结_ form表单使用注意事项之form触发后台提交事件 _20220127

    工作中的技术总结_ form表单使用注意事项之form触发后台提交事件 _20220127 如无必要不要使用 form标签 来作为组件的父节点 事件过程: 项目使用的是 spring + jsp 的框 ...

  5. TCP-UDP-Socket调试工具以及使用教程(亲测好用!)

    前言 TCP-UDP老程序都不陌生吧,面试常问.所以在网络编程与网络应用开发的过程中,调试是一个至关重要的环节,它帮助开发者确保数据能够准确无误地在不同节点之间传输.尤其当涉及到TCP/IP.UDP等 ...

  6. .NET操作Excel高效低内存的开源框架 - MiniExcel

    .Net平台上对Excel进行操作主要有两种方式.第一种,把Excel文件看成一个数据库,通过OleDb的方式进行读取与操作:第二种,调用Excel的COM组件.两种方式各有特点. 今天给大家介绍第三 ...

  7. php xattr操作文件扩展属性后续

    由于之前看了xattr的写入效率,这里简单的实现一下生产者消费者模型的高速写入. 生产者(让他创建40万条数据) <?php // 生产者 不断的生产大量数据 但是总会有停止的时候(本业务功能结 ...

  8. 一键生成美观的彩页演示+AI的训练过程科普

    一键生成美观彩页 + AI训练揭秘:让你的内容瞬间高大上! 阅读时间: 8分钟 | 字数: 1300+ 你是否曾为制作精美的演示文稿而烦恼?是否对AI的训练过程充满好奇?今天,让我们一起探索如何用AI ...

  9. (Redis基础教程之十三) 如何从命令行更改Redis的配置

    介绍 Redis是一个开源的内存中键值数据存储.Redis有几个命令,可让您即时更改Redis服务器的配置设置.本教程将介绍其中一些命令,并说明如何使这些配置更改永久生效. 如何使用本指南 本指南以备 ...

  10. (Redis基础教程之九) 如何在Redis中使用Sorted Sets

    介绍 Redis是一个开源的内存中键值数据存储.在Redis的,排序集合类似于一个数据类型集在这两者都是串的非重复的组.不同之处在于,已排序集中的每个成员都与一个分数相关联,从而可以从最小分数到最大分 ...