本文是实现Map的几个接口的类的总结
Map的整体结构
Map是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承Collection接口。
- AbstractMap:实现了Map接口的抽象类。Map的基本实现,其他Map的实现类可以通过继承AbstractMap来减少编码量。
- SortedMap:继承Map。保证按照键的升序排列的映射,对entrySet、keySet和values方法返回的结果进行迭代时,顺序就会反映出来。
- NavigableMap:继承SortedMap,含有返回特定条件最近匹配的导航方法。
- HashMap:Map接口基于哈希表的实现,是使用频率最高的用于键值对处理的数据类型。它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,特点是访问速度快,遍历顺序不确定,线程不安全,最多允许一个key为null,允许多个value为null。可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap类。
- HashTable:Hashtable和HashMap从存储结构和实现来讲有很多相似之处,不同的是它承自Dictionary类,而且是线程安全的,另外Hashtable不允许key和value为null。并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以使用HashMap,需要线程安全的场合可以使用ConcurrentHashMap。
- LinkedHashMap: LinkedHashMap继承了HashMap,是Map接口的哈希表和链接列表实现。它维护着一个双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
- WeakedHashMap: 以弱键实现的基于哈希表的Map。在WeakHashMap中,当某个键不再正常使用时,将自动移除其条目。
- TreeMap : Map接口基于红黑树的实现。
1. HashMap
HashMap的数据结构是数组+链表+红黑树(红黑树since JDK1.8)。我们常把数组中的每一个节点称为一个桶。当向桶中添加一个键值对时,首先计算键值对中key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这种现象称为碰撞,这时按照尾插法(jdk1.7及以前为头插法)的方式添加key-value到同一hash值的元素的后面,链表就这样形成了。当链表长度超过8(TREEIFY_THRESHOLD)时,链表就转换为红黑树。
HashMap中有以下三个成员变量需要特别注意,
transient Node<K,V>[] table; // 扩容时按两倍增加
transient int size; // 当前Map中数据数量
int threshold; // 当size大于threshold,Map就进行扩容。threshold初始时等于 initialCapacity * loadFactor,扩容时2倍增加
final float loadFactor;
HashMap中根据Key计算所在桶的位置的方法如下:
hash = hash(key);
first = tab[(n - 1) & hash]);
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
Resize:
向hashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,hashMap就需要扩大数组的长度,以便能装入更多的元素。当然数组是无法自动扩容的,扩容方法使用一个新的数组代替已有的容量小的数组。resize方法非常巧妙,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。
/**
* 对table进行初始化或者扩容。
* 如果table为null,则对table进行初始化
* 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。
*/
final Node<K,V>[] resize() {
//新建oldTab数组保存扩容前的数组table
Node<K,V>[] oldTab = table;
//使用变量oldCap扩容前table的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//保存扩容前的临界值
int oldThr = threshold;
int newCap, newThr = 0;
//如果扩容前的容量 > 0
if (oldCap > 0) {
//如果当前容量>=MAXIMUM_CAPACITY
if (oldCap >= MAXIMUM_CAPACITY) {
//扩容临界值提高到正无穷
threshold = Integer.MAX_VALUE;
//无法进行扩容,返回原来的数组
return oldTab;
}
//如果现在容量的两倍小于MAXIMUM_CAPACITY且现在的容量大于DEFAULT_INITIAL_CAPACITY
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)
//临界值变为原来的2倍
newThr = oldThr << 1;
}//如果旧容量 <= 0,而且旧临界值 > 0
else if (oldThr > 0)
//数组的新容量设置为老数组扩容的临界值
newCap = oldThr;
else {//如果旧容量 <= 0,且旧临界值 <= 0,新容量扩充为默认初始化容量,新临界值为DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//在当上面的条件判断中,只有oldThr > 0成立时,newThr == 0
//ft为临时临界值,下面会确定这个临界值是否合法,如果合法,那就是真正的临界值
float ft = (float)newCap * loadFactor;
//当新容量< MAXIMUM_CAPACITY且ft < (float)MAXIMUM_CAPACITY,新的临界值为ft,否则为Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将扩容后hashMap的临界值设置为newThr
threshold = newThr;
//创建新的table,初始化容量为newCap
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//修改hashMap的table为新建的newTab
table = newTab;
//如果旧table不为空,将旧table中的元素复制到新的table中
if (oldTab != null) {
//遍历旧哈希表的每个桶,将旧哈希表中的桶复制到新的哈希表中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果旧桶不为null,使用e记录旧桶
if ((e = oldTab[j]) != null) {
//将旧桶置为null
oldTab[j] = null;
//如果旧桶中只有一个node
if (e.next == null)
//将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置
newTab[e.hash & (newCap - 1)] = e;
//如果旧桶中的结构为红黑树
else if (e instanceof TreeNode)
//将树中的node分离
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { //如果旧桶中的结构为链表。这段没有仔细研究
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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的时候,先估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。看完代码后,可以将resize的步骤总结为:
a.计算扩容后的容量,临界值。
b. 将hashMap的临界值修改为扩容后的临界值根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
c. 将旧数组的元素复制到table中。
putVal:
向HashMap中插入数据调用的是put方法,put又调用的内部putVal,此处直接分析putVal的源码:
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
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 {
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);
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;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
putVal方法可以分为下面的几个步骤:
a. 如果哈希表为空,调用resize()创建一个哈希表。
b. 如果指定参数hash在表中没有对应的桶( tab[i = (n - 1) & hash]== null
),即为没有碰撞,直接将键值对插入到哈希表 中即可。
c. 如果有碰撞,遍历桶,找到key映射的节点
(1) 桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
(2) 如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方法插入键值对。
(3) 如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。如果没有 找到,在链表尾部插入节点。插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链 表转为红黑树(红黑树是采用hash值比较大小的,小左大右)。
d. 如果找到了key映射的节点,且节点不为null
(1) 记录节点的vlaue。
(2) 如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。
(3) 返回记录下来的节点的value。
e. 如果没有找到key映射的节点(b、c步中讲了,这种情况会插入到hashMap中),插入节点后size会加1,这时要检查size是 否大于临界值threshold,如果大于会使用resize方法进行扩容。
HashMap的其他方法也是按照上面结构图所展示的准则来处理的,就不再做具体的介绍了。
HashMap不是线程安全的,如果想使用线程安全的HashMap,除了使用ConCurrentHashMap外,还可以使用Collections.synchronizedMap对HashMap进行封装,它使用synchronized关键字同步信号量,重写了HashMap的公共方法,因此性能不高。
2.LinkedHashMap
LinkedHashMap继承自HashMap,主要多了两个成员变量head和tail,LinkedHashMap处理保持的上面HashMap的桶结构外,还用head和tail将插入数据按先后顺序用链表链接起来,因此LinkedHashMap重写了HashMap的迭代器方法。
3.TreeMap
TreeMap是用红黑树存储数据的,它是有序的,它必须要有一个Key的比较器comparator,或者数据的Key必须是实现了Comparable接口的,否则插入数据会抛强转异常。
4.HashTable
HashTable继承自Dictionary,它的实现和HashMap大同小异,通过在public的方法上加synchronized关键字实现线程安全,但是HashTable的性能并不高,不建议使用。
5.ConcurrentHashMap
ConcurrentHashMap是线程安全的,它同HashMap的数据结构是一样的,不同之处在于它是通过分段锁来实现线程安全的,在插入或删除节点时,先同hash找到节点所在的桶,再用synchronized关键字锁定桶后再进行操作,最大的保证了性能同时保证线程安全。
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
6.WeakHashMap
WeakHashMap继承自AbstractMap,它的键是弱键,即当一个键不再正常使用,键对应的键值对将自动从WeakHashMap中删除。它是采用数组存储数据,它计算数组索引的算法和HashMap略有区别,方法如下:
final int hash(Object k) {
int h = k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
private static int indexFor(int h, int length) {
return h & (length-1);
}
此外,WeakHashMap没有实现用红黑树存储数据的逻辑。
WeakHashMap有一个ReferenceQueue类型的成员变量queue,往WeakHashMap里插入数据时,会将生成Key的WeakReference,当key不再正常使用后,key的WeakReference就被JVM的垃圾回收器插入到queue中,在适当的时候(put、remove、size、resize基本上所有public的操作),WeakHashMap会根据queue里数据,移除对应的Value。移除方法如下所示:
/**
* Expunges stale entries from the table.
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}