您的位置:首页 > 编程语言 > Java开发

HashMap源码理解

2016-06-22 07:14 686 查看

导语

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  源码 jdk hashmap