目录
5 KeySet()、Values()、EntrySet()
前言
本人使用的是jdk8。
Map集合继承结构
1 底层实现
底层真正存储数据的是一个table数组,实际的数据结构是一个散列表,用链表来处理冲突,散列表相关知识可查阅数据结构教材(盗个图)。
下面是哈希表的结构,注意这里table中并不是只是存了我们put的value,把key也存到了table,通过把它们构造成一个Node对象。这里的Node是链表的节点,可以看到它有一个next指针,指向下一个节点。
// 存储数据的数组
transient Node<K,V>[] table;
// table中的Node元素
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 冲突链表指针,指向下一个冲突节点
......
}
2 Put(K,V)方法分析
下面貌似注释有点多,其实总结一下就是:
- 若table为空或长度为0,则先进行扩容,将table的容量初始化到16,扩容详见下面介绍。
- 通过key计算出Node在table中映射的位置,位置 = (table长度 - 1) & hash(key),而hash(key)=
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)。
- 记映射的位置为pos,令Node p = table[pos],若p == null,则直接构造Node,插入pos处,然后跳到第7步;若不为null,则进行第4步。
- 比较p的key与我们插入的key是否相同,即:p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))为true,则相同,用我们插入的Node替换p,然后跳到第7步;为false则不相同,进行第5步。 - 判断p是否是红黑树,是的话调用插入红黑树节点的方法,然后跳到第7步;否则对table中pos处的冲突链表进行遍历,进行第6步。
- 若遍历位置节点为空,则说明遍历完冲突链表也没有冲突的key,所以直接将我们插入的数据构造成Node插入冲突链表尾部,同时判断冲突链表的节点个数,若不少于8个且hashtable的容量>=64,则将冲突单链表转化成红黑树,这样能够提升以后的查找效率,若冲突节点个数减少到6,就换回链表(是为了平衡时空复杂度);若遍历位置节点不为空,则同样比较它的key和我们插入的key是否相同,相同则替换,不相同则继续遍历下一个节点。为什么阈值是8,详见:https://blog.csdn.net/kyle_wu_/article/details/113578055。
- 到这里就已经完成了对node的替换或插入,下一步判断改变后的table的长度是否到达了阈值,达到则扩容。if (++size > threshold) resize();
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;
//若table为空或长度为0则先进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/*通过(n - 1) & hash计算出要插入数据的key在table(散列表)中的散列位置,并让P
指向table中该位置上的原Node对象。
如果P为null,就直接创建一个Node放到该位置就行了*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
/*根据key的hash值和equals()判断p与插入数据的key是否相同,都相同则e=p
若插入数据的key非空且p.key.equals(插入数据的key),则e=p*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //最后的代码会让插入数据覆盖p
else if (p instanceof TreeNode) //若p是红黑树节点,则调用插入红黑树节点的方法
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //否则遍历p上的单链表,binCount 用来记录已遍历单链表上节点的个数
for (int binCount = 0; ; ++binCount) {
//如果当前遍历节点为null,直接创建一个节点插入到该位置
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;
}
/*当前遍历节点不为null,同时key等于插入数据的key,直接退出循环,此时e指
向了当前遍历的这个节点*/
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //以上条件都不满足,p指向e,后面e=p.next
}
}
//e不为null的话,即在table中找到了和插入数据的key相同的Node,然后替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
//oldValue 为null或onlyIfAbsent 为false时进行覆盖
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//若此时table的长度到达了阈值,则进行扩容,扩容后的table长度为现在的两倍
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3 Hashmap的resize()扩容过程
resize()方法介绍如图,每次将新table的体积扩充为原来的两倍:
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
......
}
这里介绍一下Hashmap的扩容过程,从上面代码可以看出当:size>threshold时,执行resize()方法。而threshold在Hashmap的构造方法中被初始设为16,后面每次resize(),都会令:threshold=newCapacity * laodFactor,故我们可以推出一个大致的扩容过程:
hashmap的capacity到达threshold(16)——>resize()——>capacity *= 2——>threshold = capacity * loadFactor(0.75)——>.......
比如:第一次满了(是真的把长度为16的table填满了),扩容后新的容量是32,也就是说hashmap中的table数组长度为32,此时threshold = 32*0.75 = 24,也就是说下次当table中的元素数量达到24个时就开始扩容,而不是填满32个,这也就是loadFactor的作用。loadFactor本身是表示table的填满程度,是一个值在0~1之间的数,Hashmap里面就是为了实现:table装满75%的时候就进行扩容。因为table太满,哈希表产生冲突的几率就越大,但时空间利用率高;反之自推。loadFactor设置为0.75是一个对时空效率的考虑。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始最小容量16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f; //装填因子0.75
3.1 hashmap 的长度始终为 2 的幂次方除了减少哈希冲突还有什么用?(腾讯)
提高效率。hash表的长度可以用计算元素插入的位置,插入位置 = hash(key) % length,这样能保证插入位置是在hash内部的,但是取模运算的效率不高,尤其是与&比较时,有人做过实验,在Java中,用一个变量%另一个变量1亿次,然后用这个变量&一个变量1亿次,前者的执行时间是后者的27倍,见:https://blog.csdn.net/weixin_30877181/article/details/101340719。
因此,HashMap内部是这样计算插入位置的:插入位置 = hash & (length-1)。因为length是2的幂次方,所以length-1是一个连续低位全部是1的数,hash & (length-1) 就等价于 hash % length,这样就进行了等价转换,既能让key插入到hash表内部,也提升了效率。
4 get(key) 方法介绍
代码不长,看看代码中的注释应该就可以了。
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;
//table长度大于0且key的在table中的散列位置上的Node不为空,则执行方法
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//若first的key与参数key相同,返回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) {
//当前遍历节点e是红黑树节点,则按红黑树的方法来查找目标Node
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;
}
5 KeySet()、Values()、EntrySet()
HashMap中提供了这三个方法对table中所有元素的key、value和Node的遍历,这三个方法分别返回了KeySet、Values和EntrySet三个HashMap的内部类实例,这三个类都继承或实现了Java集合类或接口,都提供了size()、clear()、contains()、Iterator方法的实现,其中Iterator的实现不同,这里就来了解一下。
// ########################### KeySet ########################
final class KeySet extends AbstractSet<K> {
// 返回了KeyIterator
public final Iterator<K> iterator() { return new KeyIterator(); }
......
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
// 只获取Node的key
public final K next() { return nextNode().key; }
}
// ########################### Values ########################
final class Values extends AbstractCollection<V> {
// 返回了ValueIterator
public final Iterator<V> iterator() { return new ValueIterator(); }
......
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
// 只获取Node的value
public final V next() { return nextNode().value; }
}
// ########################### EntrySet ########################
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
// 返回了EntryIterator
public final Iterator<Map.Entry<K,V>> iterator() {return new EntryIterator();}
......
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
// 返回整个Node
public final Map.Entry<K,V> next() { return nextNode(); }
}
6 HashIterator
从上面代码可以看到,KeyIterator、ValueIterator和EntryIterator都继承了HashIterator,他们的取值和删除值的操作都来自HashIterator。
6.1 属性及构造方法
从构造方法可以看到,从索引为0处开始,直到找到table中不为空的元素。令next指向这个元素。
Node<K,V> next; // 指向下一个要返回的Node
Node<K,V> current; // 指向当前Node
int expectedModCount; // for fast-fail
int index; // 当前遍历到table中的哪个位置
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
// 从0处开始,直到找到table中不为空的元素。令next指向这个元素
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
6.2 hashNext()和nextNode()
nextNode方法直接返回next所指向的Node,同时继续寻找不为空的节点。首先从next的冲突链表上找,若冲突链表上没有则继续按构造器里面的方法找。
public final boolean hasNext() {
return next != null;
}
// 直接返回next指向的元素
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
// 这里继续为next找到下一个不为null的元素
// 首先从next的冲突链表上找,若冲突链表上没有则按构造器里面的方法找
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
6.3 reomve()
remove方法比较简单,直接删除table中上一次通过nextNode返回的元素,不过这个方法如果调用2次,第二次current可能为null,从而抛出IllegalStateException。
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
7 HashMap的remove()
代码入下,可以看到是调用的主要是调用的removeNode方法。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
7.1 removeNode()
与put方法有很多相似之处,还是分两步,一是比较table[index]处,二是比较table[index]上的冲突链表,找到key相同的Node则删除。若目标元素在table[index]处,则用table[index]上的冲突链表的第一个Node取代table[index];若目标元素在冲突链表上,则直接从链表中删除。
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 && // 比较table[index]
(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) // 说明node=p=table[index],直接让冲突链表上
tab[index] = node.next; // 第一个元素替代table[index]
else
p.next = node.next; // 说明在冲突链表上匹配成功,此时node = p.next
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
Hashmap的线程不安全
见这篇文章,写的非常通俗易懂,图文并茂。http://www.importnew.com/22011.html