Java8中,新加了很多新特性,特别是集合,分割迭代器,Stream,Functional Interface等等,Java8中的HashMap也和以往的实现略有不同。
这些天看了好久的HashMap,理清了HashMap的结构以及实现原理,听我慢慢分析。
/**
* 基于Map接口实现,允许null值和null键。
* HashMap和HashTable很相似,只是HashTable是同步的,以及不能为null的键
* HashMap有两个重要参数,capacity和load factor 默认的load factor大小为0.75
* iterator是fail-fast的。
*
*/
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>,
Cloneable, Serializable {
如上,基本的特性在代码里面注释了,HashMap实现了Map接口,是一个基于散列表的Map类,Map接口的特性就是存储键值对。散列表是一种存储结构,它可以通过散列函数直接访问到目标数据值,所以在定位下标方面可认为为o(1)。
/**
* Hash的默认大小
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* HashMap最大存储容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 增长因子,意思就是当table已经用到table.length*0.75时,就需要扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 由链表存储转变为由树存储的门限,最少是8
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 由树存储节点转化为树的节点,默认是6,即从8到6时,重新转化为链表存储
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当由链表转为树时候,此时Hash表的最小容量。 也就是如果没有到64的话,就会进行resize的扩容操作。
* 这个值最小要是TREEIFY_THRESHOLD的4倍。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
上述代码中解释了HashMap中重要字段的意思,相信大家一看就会有大概理解了。
由于在Java8的实现中,当经过hash函数计算得出的下标地址冲突到一定范围时,就会 把冲突的数据用链表的形式连起来,而当用链表数据大于一定范围时,就会将链表转化为红黑树存储。
关于链表,Java中典型应用就是LinkedList,可以看:LinkedList
而红黑树,Java中典型应用是TreeMap, 可以看:TreeMap
首先HashMap会有一个基准数组table:
/**
* 存储数据的table集合,长度一定为2的倍数
*/
transient Node[] table;
第一步,table是一个数组,所以会有下标,HashMap首先会根据传入每个节点的(key,value)中的key,算出应该放到哪一个下标的数组中。
第二步,如果此下标数组为null,那么就直接放入,不为null,就走到第三步。
第三步,如果不为null,就说明冲突了,检查key的equals方法,看是否和原节点的key相同,相同就直接替换,否则进入第四步。
第四步,很明显冲突了,而且是不相等的冲突,这是检查是否需要将此下标的存储结构换为红黑树,不需要就是链表直接在末尾插入节点,否则进入第五步。
第五步,原有的链表结构不足以支撑存储了,所以换为红黑树存储了,此时就是往红黑树中插入该节点。
上述步骤省略了链表与红黑树之间转换。
整个存储结构图如下(没有放入红黑树存储结构)(省略了value值)
首先看HashMap的Node节点代码,这就是table数组所使用的结构。如果冲突的是链表存储,则直接是这种结构存储。
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
V value;
// 可能要连接下面的链表,所以会有个next
Node next;
...
省略
再看红黑二叉树存储的结构:
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
TreeNode parent; // red-black tree links,红黑树,保证是一棵平衡二叉树
TreeNode left; //左子树
TreeNode right; //右子树
TreeNode prev; // 指向下一个节点,类似于线索二叉树, needed to unlink next upon
// deletion,删除时记得置null
boolean red; //红黑特性
...
省略
这是当不用链表表示冲突值时候,用红黑树表示时候的节点。由上可知,TreeNode继承自LinkedHashMap.Entry,
而它的结构如下:
static class Entry<K,V> extends HashMap.Node<K,V>
所以,其实TreeNode是Node的一个子类,所以table中也是可以存放TreeNode的。
这里首先介绍hash值的计算方法,这也是一门有学问有艺术性的东西。
在HashMap中要注意区分hashCode和hash两个方法,他们是不通的!!
这里就不细说hashCode了,看下面hash方法
/**
* 自己低位和高位异或操作,能够降低冲突 计算冲突,结合高16位与低16位
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashCode()是一个native方法,意味着方法的实现和硬件平台有关,默认实现和虚拟机有关,对于有些JVM,hashCode()返回的就是对象的地址,大多时候JVM根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,并返回。
所以hashCode返回的是一个32位的2进制数值,而Java8中这样的实现,保证了对象的hashCode的高16位的变化能反应到低16位中,相比较而言减少了过多的位运算,是一种折中的设计。
table容量为2的倍数时,有利于下一个缓解的计算table的下标,另一个方面,虽然在HashMap中,提供了一个构造方法:
public HashMap(int initialCapacity, float loadFactor)
看似提供了初始容量的方法,但是这个方法最后一行代码中调用了另一个方法tableSizeFor
来确定table的容量:
/**
* hashMap大小只能为map的倍数。 最终会返回一个最适合cap的2的倍数
* capacity.
*/
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;
}
所以最终table的length只能是2的倍数。
有点基础的读者应该知道,一般索引,都是传入的是一个键(key),而这里的key是一个引用类型,通俗点,就是个类(class),既然是个class,那又怎么获取下标呢?即怎么与下标联系起来呢?看下面代码:
tab[(n - 1) & hash]
没错,就是通过这样的方式,其中hash=hash(key),n=table.length
。这行端代码就相当有艺术了。
由前面知道,table.length是一个2的倍数,随意化成2进制就是开头一个1,后面n个0。随意当减1后,就会变成一排1,
之后,在与刚刚得到的hash(通过高位和低位计算后得到的hash)值做二进制与操作,因为(n-1)的高位都是0,所以最终只会截
取到hash的后log(n)-1
位,会得到一个范围在0~table.length的值,这个值,就是数组的下标。是不是很有艺术。
由于hash是由key的hashCode的高16位与低16位经过异或而得,混合了原始哈希码的高低位,大大的提升了随机性,也让碰撞机率大大降低。
前面已经讲了put方法的基本过程,下面再细看看putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
,其中,如果key=null,那么hash(key)=0,所以是能够存放null值的。方法实现代码:
/**
* 插入值, onlyIfAbsent,为真的话,就是不替换,无就插,有就不插 Implements Map.put and related
* methods evict,表示需要调整二叉树结构,LinkedHashMap中需要
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab; //存放table
Node p; //存放以前存放在table[(n-1)&hash]的节点,如果有
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 {
Node e;
K k;
if (p.hash == hash
&& ((k = p.key) == key || (key != null && key.equals(k))))
// 一模一样,连key也equals后相等时
e = p;
else if (p instanceof TreeNode)
// 判断是二叉树
e = ((TreeNode) 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;
}
if (e.hash == hash
&& ((k = e.key) == key || (key != null && key
.equals(k))))
break;
//其中,如果key的equals也相等,就直接替换
p = e;
}
}
// 替换操作,key一样,旧值换为新值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
//LinkedHashMap使用
afterNodeInsertion(evict);
return null;
}
具体代码分析已经注释到了代码里面。
如下代码:
/**
* 根据key返回它的值。
*/
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
如上代码所示,可以获取null值。
/**
* 根据key返回值。 也就是先算hash,在找到其位置,在看是否有因冲突而产生的链表或者二叉树。
*/
final Node getNode(int hash, Object key) {
Node[] tab; //指向table,这样如果对table加锁,自己还是能够只读的
Node 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) 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获取节点的过程大致是这样,代码中已有注释,这里就不多讲。
如下代码:
/**
* 根据key,删掉这个节点。
*/
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null
: e.value;
}
接下来看具体的removeNode方法:
/**
* 删除某一个节点。
* @param matchValue
* 如果为真,那么只有当value也想等时,才能删除。
* @param movable 能否删除
*/
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab;
Node p;
int n, index;
if ((tab = table) != null && (n = tab.length) > 0
&& (p = tab[index = (n - 1) & hash]) != null) {
//寻找node节点过程
Node 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) 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) 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方法相似,先找到节点,然后在判断哪种方法删除,以及删除之后的调整。
和containsKey方法不同,它可以通过先散列,在判断key是否equals来判断是否含有这个key,而containsValue方法,则是直接暴力枚举所有value,然后得出有这个value,性能较差。
/**
* 在map中如果至少有一个value的值为value,就返回true。 ,注意下面有个双重循环,一个是循环数组,一个是循环链表(二叉树)。
*/
public boolean containsValue(Object value) {
Node[] tab;
V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node e = tab[i]; e != null; e = e.next) {
//在TreeNode中,next属性也够用,因为TreeNode的父类是Node
if ((v = e.value) == value
|| (value != null && value.equals(v)))
return true;
}
}
}
return false;
}
前面讲过,当table的使用量到达length*loadFactor时,机会触发扩容操作,扩容操作的基本流程为:
1、判断是否需要扩容
2、将老数组table的元素,一个一个遍历并插入到新数组newTable中
3、更改相应的字段属性值。
/**
* 初始化使用,
* 或者将hashmap大小调整为2的倍数级使用。
*/
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果当前size大于最大容量,则下一次就是int的最大值
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
} else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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" })
Node[] newTab = (Node[]) new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
//旧值放置的位置。
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 当这个位置没有东西时候,就直接取莫放在这里。,重新计算hash值以便。
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 是二叉树节点
((TreeNode) e).split(this, newTab, j, oldCap);
else { // preserve order
// 仅仅是链表节点。
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} 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;
}
HashMap里面具有下面几种Iterator:
HashIterator:普通Iterator的父类
KeyIterator:key的Iterator,继承自HashIterator
ValueIterator:Value的Iterator,继承自HashIterator
EntryIterator:key和value的Iterator,继承自HashIterator
同样的,HashMap里面也有Spliterator:
关于何为Spliterator,请看这篇: 集合源码学习:Spliterator
但是就目前Java8的源码来看,HashMap里面的分割列表,它是基于table的元素进行迭代的,啥意思呢?
在就是在trySplit方法里面,仅仅是对table进行横向的分割,类似于对数组的分割。
而在tryAdvance中,只会对table[current]进行以下 的遍历,即遍历链表或二叉树。如果current为null,则向下找一个不为空的table[current],找到后,只遍历一个table[current]找的终点则是本Spliterator的fence。
而在forEachRemaining中,遍历多个,和tryAdvance不同的时,它会遍历本Spliterator所有不为null的table[current]。
学习过程中,从很多文章中学到了知识:
http://www.importnew.com/20121.html
https://www.zhihu.com/question/20733617/answer/111577937
http://www.cnblogs.com/tonyluis/p/5671873.html
http://blog.csdn.net/ghsau/article/details/16843543