Map
既有映射的意思,也有地图的意思.所蕴含的知识恰如一张‘地图’.
1. Map
public interface Map<K,V> {
Map
恰如其名'映射',将一个key
映射到value
,又恰如其名'地图',包含多组映射的对象.
以下是官方的Doc
注释(节选): 一个Map
对象不能包含重复的key
,一个key
最多只能映射一个value
,这个类代替了Dictionary
类,Map
的顺序为迭代器返回的顺序,有的实现对返回的顺序做出了保证,而有的则没有.对于key
和value
是否为空,也需要看具体的实现.
Dictionary
是一个和Map
接口类似的完全抽象类,用Map
代替该类也很简单,其一是因为抽象类的原因,Java
类本身是单继承的关系,如果继承了该抽象类,就不能继承其他类了.另一个原因就是因为他的类里面全是抽象方法,定义为一个接口更加合适.
在Map
里面还有一个比较重要的就是内部接口Entry
,他所对应就是一个key-value
映射对象.
2.HashMap
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
1. 概述
一提到Map
,相比大家自然想到的是HashMap
,在平时用得比较多的也是HashMap
,面试中问的最多的也是HashMap
,同时在其他Map
实现中,和HashMap
相关的也很多,所以我们就先来看看HashMap
.
HashMap
的结构贴在上面了,根据前面的文章,不难发现,AbstractMap
是对Map
的基本实现,用来解决实现Map
接口的工作量.这种设计可以说是不错的,如果你自己想实现的Map
和默认默认实现相差无几,你可以继承该抽象类然后重写指定的方法便可.
这里节省篇幅AbstractMap
就不贴了,具体的实现大家可以自行查看.而HashMpa
实现的其他接口在前面的文章中也讲到了,也不多提了.HashMap
是一个基于'哈希表'实现的Map
结构,有关'哈希表的内容,大家自行查阅.
接下来先看看HashMap
所包含的属性,先知道个大概的概念:
// 默认的初始化容量
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;
// Node实现了Map.Entry接口的对象,真正的数据存储的地方
transient Node<K,V>[] table;
// Entry集合
transient Set<Map.Entry<K,V>> entrySet;
// 数量个数
transient int size;
// 用于快速失败
transient int modCount;
// 初始容量 负载因子*总容量
int threshold;
// 加载因子
final float loadFactor;
2.方法
构造方法就不提,都是一看就懂的.但是在构造方法里面有一个比较特殊的方法:
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;
}
这个方法的作用就是把计算大于或等于cap
的第一个2的n次幂:
// 假设参数为100
n = 99 = 0110 0011;
---- 移位操作 >>>1 ----
n = 49 = 0011 0001
----- 或操作! -----
n = 115 = 01110011
----- 移位操作 >>>2 ----
n = 28 = 0001 1100
----- 或操作! -----
n = 127 = 0111 1111
----- 移位操作 >>>4 ----
n = 7 = 0000 0111
----- 或操作! -----
n = 127 = 0111 1111
.....
// 一系列步骤执行到最后
n = 127 = 0111 1111
n = n+1 = 128 = 1000 0000
上述一些列操作刚好得到了大于或等于cap
的第一个2的n次幂,至于开头减一的原因就是为了解决自身是2的n次幂的情况:自身是2的n次幂时会多乘一个2.至于为什么容量一定要是2的n次幂,后续再提(1).
hash()
构造完了,那肯定是添加了,但是在添加之前,使用到了另一个关键的函数hash()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
计算hash
值的公式为: key
的hashCode
低16位和高16位进行异或.为什么拿到hashCode
后还要进行^
和>>>
操作?后续解答(2).
put()
代码较长,部分解释直接写在里面了.
// hash值,key值,value值,是否替换原有值 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 table就是真正存的数据的地方,resize方法贴下面了
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<K,V> e; K k;
// 判断hash值和key值是否相同,相同直接将原来的替换
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 则变成链表,binCount从0开始...
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;
p = e;
}
}
if (e != null) { // existing mapping for key
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;
}
tab[i = (n - 1) & hash]
这个操作就是获取指定位置的元素,n是哈希表的长度,默认16,通过前面的方法我们知道,哈希表的长度永远都是2的n次幂,而2的n次幂有什么一个特点,除最高位外,其余位都是0,而这个减1操作,直接就总位数减1,同时值全为1,这样就变成了一个掩码操作:
假设hash值为 11101010
默认长度16: 16 - 1 = 15 = 1111
1110 1010
& = 1010 = 10 得出下标等于10
0000 1111
问题解释(1): 长度一定要是2的n次幂的原因就是: 利用2的n次幂二进制数的特殊性,便于形成掩码计算位置.
问题解释(2): 从上述看得出,如果直接用hashCode
做hash
值,那么要进行大量的计算(int
范围大,重复取余之类的),而进行异或和位移的原因也很简单: 上述元素定位,只针对最后几位为1的,和高位无关,容易产生大量的碰撞.而异或操作就是加大最后几位的随机性.
resize()
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 这个if就是扩容判断. 其他else就是处理各种初始化情况
if (oldCap > 0) {
// 最大值直接返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容为原来的2倍,阈值也为原来的2被
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
// 就是默认初始化值16
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);
}
// 初始化 不进入下面的if 直接返回
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 重新定位元素
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 {
next = e.next;
// 尾插法
// 最高位为0的情况 说明你不需要改变,保持原状就好
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 最高位为1的情况 说明你在新容量的位置已经发生了改变.
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;
}
为0不需要改变,而为1的就需要改变位置的原因:
// 旧容量扩容就是添加了一位0位:
16 = 10000
32 = 100000
假设key.hashCode = 101010
旧计算下标:
10000 - 1 = 1111 & 101010 = 1010 = 10
新的计算下标
100000 - 1 = 11111 & 101010 = 1010 = 10
位置还是一样的 则不需要进行换位置, newTab[j] = loHead 作用显而易见
假设key.hashCode = 111010
旧容量计算位置:
10000 - 1 = 1111 & 111010 = 1010 = 10
新容量计算位置:
100000 - 1 = 11111 & 111010 = 11010 = 26
新元素的位置就是在高位多了1,也就是加上了旧的容量.newTab[j + oldCap] = hiHead 作用显而易见
总结一下上面的扩容: 其实容量扩容后,所形成的掩码的长度多了一位,(e.hash & oldCap)
而这个操作刚好就是判断原值多出的一位是否为1,旧容量的最高位就是1.是否需要移动位置也取决于该位,其他位的结果都不变.
不得不说,这个2的n次幂数选得真对,太细节了!
3.杂谈
新增相关的就差不多了,关于树化的有兴趣的可以去看看.其实HashMap
写到这里,理解了上面这些核心方法,阅读其他部分并没有什么太大的难度.其他方法留给读者自行查看(写累了....).
还有一些老生常谈的问题: 为什么负载因子是0.75?
这个问题也很简单,先想想如果没有负载因子或者负载因子过大会怎么样,如果没有,也就是在容量快慢时才进行扩容,但是我们知道HashMap
时可以形成链表的,如果你的hashCode()
没有设计好就存在这种情况,就占了两个坑,但是实际数据非常多的情况.这时候有了负载因子就可以进行定位,使得分布均匀一些.而负载因子设置得过大也存在上面的情况.过小就容易频繁resize()
导致性能不佳. 总结: 为了更好的性能.
注意,HashMap
的put()
方法是有返回值的,返回的是该key
的旧值,如果没有,则为null
,不过该null
也可能是返回的旧值为null
.显然null
在此处存在二义性,这也就是为什么ConcurrentHashMap
中不允许为空的原因.
后面的类可能会水起来了....
3.HashTable
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
1. 概述
HashTable
这一''古老''的类,原本不想讲的,出于''惯例'',还是拿出来提一手(鞭尸).但是具体细节不多概述,就提一些比较关键的点.也可以说是和HashMap
进行一个比较吧.
首先,HashTable
的key
和value
都不能为空,扩容的话是原来的两倍加一.容量也没有像HashMap
那样做2的n次幂限制.再者也没有树化节点.但是,他是线程安全的,在不是那么高要求的并发下,可以考虑使用它.
他的获取操作也加上了synchronized
.
至此,了结.
4.ConcurrentHashMap
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
1. 概述
HashMap
再多线程下存在问题,诸如数据错乱之类的,这也都是熟知的.关于ConcurrentHashMap
的一些特性不对赘述,网上也很多相关文章.
如果像上面这样写ConcurrentHashMap
,那真的太过庞大,毕竟涉及到的知识点太多.这里就不多写了(主要是懒),写点总览,以后有时间再补上(很蓝的吧).
从ConcurrentHashMap
使用了CAS
和分段锁保证并发安全性.只锁该节点所在的链表:synchronized (f)
,就是哈希表的具体元素,其他不受影响,变量使用了volatile
保证了内存可见性,同时禁止指令重排序.总体变是这种情况,但其中细节很多.
5.LinkedHashMap
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
1. 概述
LinkedHashMap
提供了有序的迭代遍历(HashMap
和ConcurrentHashMap
无序),他再内部使用了一个双向链表维护顺序. 从上面的类定义信息来看,该类和HashMap
相差无几,但是性能相比略弱,这是因为维护链表所带来的开销,但是在迭代方面,性能比HashMpa
要好.
2.方法
LinkedHashMap
方法没什么好讲的,大部分都是回调钩子方法,用于维护链表.
// 用于使用put或get后移动指定元素到尾节点位置。该节点最近使用了
// 该方法依赖accessOrder,默认为false也就是默认不移动,如果想实现lru可以改为true
void afterNodeAccess(Node<K,V> p) { }
// 添加节点后是否需要对链表进行操作 依赖removeEldestEntry(first) 默认为false。删除头节点判断.
void afterNodeInsertion(boolean evict) { }
// 删除头节点后调整链表
void afterNodeRemoval(Node<K,V> p) { }
LinkedHashMap
继承了HashMap
,实现了钩子方法,这些钩子方法在HashMap
中都有调用:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
像一些对key
进行操作的地方,如果改key
是已经存在的,就会使用afterNodeAccess()
,LinkedHashMap
实现如下:
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
该方法就是把该节点在LinkedList
链表中移动最后,默认是不执行的,accessOrder
默认初始化为false
,移动至法仅在key
存在的情况下,不存在时不会调用该方法的. 如果不想要对最近使用过的key
进行删除,就需要在初始化时指定为true
,这样下面删除的时候就不会删除该映射.
afterNodeInsertion(boolean evict)
这个方法是针对添加一个节点后,对LinkedHashMap
中的链表进行何种操作:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
evict
默认为true
上面写HashMap
的时候提到过,而removeEldestEntry(first)
就是实现LRU
的关键方法,默认是false
也就是不执行删除头节点操作.
afterNodeRemoval
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
这个方法很简单,就是对删除的节点的前后节点进行维护.
3. 杂谈
LinkedHashMap
难点不多,就是有点饶,方法跳来跳去的,容易晕,慢慢看还是挺好接受的.像一些老生常谈的实现LRU
等你理解了就是基本操作了,同时你还可以利用HashMap
的钩子方法自己去做点什么,而不是使用LinkedHashMap
.
因为是继承HashMap
,key
和value
自然都允许为空.主要还是在维护链表.
他与TreeMap
的不同就是免受比较器的困扰,而没有TreeMap
的成本开销,但是这也是一个坏处,就是不能根据排序器来指定顺序.TreeMap
是基于红黑树实现的有序map
.
TreeMap
就留个各位自行查看了(写不动了...),哪天心情好补上!!!