HashMap源码理解
Posted 尹凯文
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap源码理解相关的知识,希望对你有一定的参考价值。
导语
HashMap是常用的数据结构,了解HashMap,对提高代码的效率有很大的帮助。HashMap在JDK1.8中对数据结构进行了优化:提高了查询和删除的效率。当然,这也导致了结构更加的复杂;但通过认真阅读源码,还是可以掌握其要领的。
读完本篇文章,你应该理解的内容
说明:HashMap的数据结构是个Hash表(可以理解为数组),每个槽中存放着一些节点。
- 一般情况下,一个槽中存放一个节点;
- 数据量较大时,一个槽中可能存放多个节点,此时,各个节点以链表的方式连接在一起;
- 当一个槽中的节点数很多时(8个以上),会以红黑树的方式来保存这些节点
源码理解
成员变量
//数组默认的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//数组的最大容量
static final int MAXIMUM_CAPACITY = 1 << 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;
//数组,真正用来保存数据的容器
transient Node<K,V>[] table;
//用于遍历,本篇不做介绍
transient Set<Map.Entry<K,V>> entrySet;
//大小
transient int size;
//修改的次数
transient int modCount;
//阈值:当数组中的数据的个数大于该值时,数组会扩充
int threshold;
//加载因子
final float loadFactor;
说明:从table中可以看出,HashMap最基本的数据结构是个数组;其余的成员变量单独分析是得不到什么结果的,需要结合下面的内容来理解。从常用到的put(),get(),remove()开始理解。
构造方法
在此之前,当然要看看它的构造方法是怎样的:
public HashMap() {
//加载因子为默认值
this.loadFactor = DEFAULT_LOAD_FACTOR;
//这里并没有初始化数组
}
//自定义initialCapacity,加载因子使用默认值
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//自定义initialCapacity,和loadFactor
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的次方
//例如:initialCapacity = 17 ,阈值为 32
// initialCapacity = 5 ,阈值为 8
// initialCapacity = 55 ,阈值为 64
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(Map<? extends K, ? extends V> m) {
//加载因子为默认值
this.loadFactor = DEFAULT_LOAD_FACTOR;
//将m中的数据存到当前的Map中
putMapEntries(m, false);
}
说明:前三个构造方法中,只是初始化了一些参数,没有过多的操作;第四个构造方法比较复杂,本篇读完后,再去看源码就容易理解了,这里不做讨论。
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) {
// tab --数组,用来保存数据的容器
// p --i所对应数组槽中的第一个节点
// n --数组的大小
// i --当前键值对应该存储在数组中的位置
Node<K,V>[] tab; Node<K,V> p; int n, i;
//第一次添加数据的处理
if ((tab = table) == null || (n = tab.length) == 0)
//数组大小使用默认值
n = (tab = resize()).length;
//相应的槽中没有节点的处理
if ((p = tab[i = (n - 1) & hash]) == null)
//添加新的节点
tab[i] = newNode(hash, key, value, null);
//相应的槽中节点的处理
else {
// e-- 用来标记符合条件的节点
// k-- 键
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);
//如果链表的长度大于阈值(8),那么将链表转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//说明当前e符合条件,结束遍历
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;
//LinkedHashMap中会用到,这里没处理
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果当前的大小大于阈值,扩充数组的大小
if (++size > threshold)
resize();
//LinkedHashMap中会用到,这里没处理
afterNodeInsertion(evict);
return null;
}
说明:实现的细节非常繁琐,但是总结起来就很简单了:
- 没有相应的节点,就创建节点,并放到合适的位置
- 有相应的节点找到对应的节点,更新其中的数据
额外说明:
- put()不会重复保存key,HashSet就是利用了这点来实现去重的
- LinkedHashMap会重写其中的一些方法来实现相应的特性
get()方法
//根据key找到value
public V get(Object key) {
Node<K,V> e;
//找到相应的节点,返回value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//找到对应的节点
final Node<K,V> getNode(int hash, Object key) {
// tab -- 数组
// first -- 数组对应槽中的第一个节点
// e -- 对应的节点
// n -- 数组的长度
// k -- 键
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;
}
说明:get()可以分为这么几个步骤:
- 锁定槽
- 从槽中查找相应的节点
- 返回合适的数据
remove()方法
//移除相应的节点
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) {
// tab -- 数组,用来保存数据的容器
// p -- index所对应数组槽中的第一个节点
// n -- 数组的大小
//index -- 当前键应该存储在数组中的位置
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 -- 符合要求的节点
// e -- 标记当前节点的下一个节点
// k -- key
// v -- value
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);
}
}
//判断node是否符合删除的条件
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;
//LinkedHashMap中会用到,这里没处理
afterNodeRemoval(node);
return node;
}
}
return null;
}
说明:简单来说就是:找到相应的节点并删除并且按照规则移动槽中剩余的节点。
结语
- 这时再去看第四个构造方法,无非就是变量传进来map,将数据封装到HashMap中来。
- 本文对链表以及红黑树的的操作没有做进一步的分析。个人认为,阅读源码,如果过分的关注细节可能会难以把握整体的思路;当然,有些时候看源码需要关注细节,这之间需要我们进行平衡,源码看多了,这种平衡感就会有的。(链表和红黑树的操作之后的文章会单独做一些说明)
- 最后,再一次将核心部分,也就是最开始的那张图贴一下。
转载请标明出处http://blog.csdn.net/qq_26411333/article/details/51723828
以上是关于HashMap源码理解的主要内容,如果未能解决你的问题,请参考以下文章