一、简介
映射表(也称关联数组)的基本思想是它维护的是键-值(对)关联,因此你可以使用键来查找值。标准的Java类库中包含了Map的几种基本实现,包括:HashMap、TreeMap、LinkedHashMap,WeakHashMap,ConcurrentHashMap。他们都有同样的基本接口Map,但是行为特性各不相同。这主要表现在效率、键值对的保存及呈现次序、对象的生命周期、映射表如何在多线程程序中工作和判定“键”等价的出来方面。
二、映射表的性能
性能是映射表的一个重要问题,当在get()中使用线性搜索时,执行速度会相当地慢。而这正是HashMap提高速度的地方。HashMap使用了特殊的值,称作散列码,来取代对键的缓慢搜索。散列码是相对唯一的、用以代表对象的int值,它是通过将该对象的某些信息进行转换而成的。散列码是通过散列函数hashCode()计算出来的一个整数值。
hashCode()是根类的Object方法,因此所有的Java对象都能产生散列码。
HashMap就是使用对象的hashCode()进行快速查询的,此方法能够显著提高性能。
散列是映射存储元素时最常用的方式。
2.1散列码
Object的hashCode()方法用来生成散列码,它默认是使用对象的地址计算散列码。如果只覆盖hashCode方法,并不能保证能正常运行。除非你同时覆盖equals方法,它也是Object的一部分。HashMap使用equals()判断当前的键是否与表中存在的键相同。默认的Object.equals()只是比较对象的地址。如果需要使用自己的类作为HashMap的键,必须同时重载hashCode()和equals()。
hashCode并不需要总是返回唯一的标识码,但是equals()方法必须严格地判断两个对象是否相同。
散列的价值在于速度:散列使得查询得以快速进行。由于瓶颈位于键的查询速度,因此解决方案之一是保持键的排序状态,然后是使用Collections.binarySearch()进行查询。散列则更进一步,它将键保存在某处,以便能快速找到。存储一组元素最快的数据结构是数组,所以使用它来表示键的信息。但数组不保存键本身,而是通过键对象生成一个数字,将其作为数组的下标,这个数字就是散列码。可以通过hashCode方法计算该散列码。
为解决数组容量被固定的问题,不同的键可以产生相同的下标,也就是说,可能会有冲突。解决散列冲突的方法有链地址法、开放地址法。
查询的过程是首先利用hashCode()计算散列码,然后使用散列码查询数组。但是通常是存在冲突的,冲突由链地址法处理:数组并不直接保存值,而是保存值的list。然后,对list中的值使用equals()方法进行线性查找。
设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该生成同样的值。要想使hashCode实用,它必须速度快,并且必须有意义。它必须基于对象的内容生成散列码。散列码不必是独一无二的,但是通过hashCode和equals,必须能够完全确定对象的身份。
2.2 HashMap的性能因子
可以通过手工调整HashMap来提高性能。
容量:表中的桶位数;
初始容量:表在创建时所拥有的桶位数。
尺寸:表中当前存储的项数。
负载因子:尺寸/容量。空表的负载因子是0,而半满载表的负载因子是0.5。负载轻的表产生冲突的可能性小,因此对插入和查找都是理想的。 HashMap和HashSet都允许指定负载因子的构造器,表示当负载情况达到该负载因子的水平时,容器将自动增加其容量(桶位数),实现方式是使容量大致加倍,并重新将现有对象分布到新的桶位集中。(再散列)
HashMap使用的默认负载因子是0.75,这个因子在时间和空间之间达到了平衡。更高的负载因子可以降低所需要的空间,但是会增加查找的代价。
如果知道在HashMap中存储多少项,那么创建一个具有恰当大小的初始容量将可以避免自动再散列的开销。
三、Map集合的一些实现
1.Map<K,V>接口
- Map接口映射key到value。一个map是不允许包含重复的key,一个key最多可以映射到一个值。
- Map接口提供三种集合视图,一个map可以包含一个key值的set集合,一个value值的Collection集合,和一个key-value值的set集合。
- 一个通用的Map应该实现两类构造器:一类是不带参数的构造器;一类是指定key-value类型的构造器。
- Entry<K,V>一个map的entry,键值对(key-value)。可以通过Map的entrySet()返回该Map的Entry<K,V>。
2.AbstractMap<K,V>抽象类
AbstractMap抽象类提供了一个Map接口的最基本的实现。
AbstractMap中的很多实现都是基于entrySet()返回的键值对的Set集合。在AbstractMap中entrySet()方法是一个抽象方法,由具体的子类实现。
AbstractMap中实现了keySet()和values方法,这两个方法分别返回key的Set集合以及value的Collection集合。实现如下:
public abstract Set<Entry<K,V>> entrySet();
transient volatile Set<K> keySet;//保存key值的set集合
transient volatile Collection<V> values;//保存value值的Collection集合
/*
*该实现返回一个AbstractSet的子类,该子类的迭代器iterator方法是由该
* 集合的entrySet返回的迭代器的相应方法实现的。
*
*/
public Set<K> keySet() {
if (keySet == null) {
keySet = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();//返回entrySet的迭代器
public boolean hasNext() {
return i.hasNext();
}
public K next() {
return i.next().getKey();//获取值元素
}
public void remove() {
i.remove();
}
};
}
public int size() {
return AbstractMap.this.size();
}
public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}
public void clear() {
AbstractMap.this.clear();
}
public boolean contains(Object k) {
return AbstractMap.this.containsKey(k);
}
};
}
return keySet;
}
/*
* 该实现返回一个AbstractCollection的子类,子类的iterator方法是由该
* 集合的entrySet返回的迭代器的相应方法实现的。
*
*/
public Collection<V> values() {
if (values == null) {
values = new AbstractCollection<V>() {
public Iterator<V> iterator() {
return new Iterator<V>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();//返回entrySet的迭代器
public boolean hasNext() {
return i.hasNext();
}
public V next() {
return i.next().getValue();
}
public void remove() {
i.remove();
}
};
}
public int size() {
return AbstractMap.this.size();
}
public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}
public void clear() {
AbstractMap.this.clear();
}
public boolean contains(Object v) {
return AbstractMap.this.containsValue(v);
}
};
}
return values;
}
/*
* 一个Map entry,键值对。Map的entrySet()方法返回一个Set集合的entry。
* 获取一个map的键值对entry,只能通过Set集合的迭代器才可以获取到,即
* 通过entrySet().iterator()方法获取到迭代器,然后再获取Entry<K,V>。
*
*/
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
}
3.HashMap<K,V>类
- HashMap实现了Map接口,HashMap提供了所有可选map操作,允许添加null value和null key。
- HashMap基本上等同于Hashtable,除了HashMap不支持同步,并且允许添加null元素。
- HashMap不保证元素在集合中的顺序。
- HashMap的一些基本操作,例如get、put操作,可以控制在常数时间内。迭代操作的性能主要受HashMap的容量和元素个数的多少影响。
- HashMap集合有两个参数影响它的性能:初始化容量capability和加载因子load factor。capability代表在hash table中卡槽的数量;加载因子是一个衡量集合被填满的程度,用来指示容量被自动增长前可以填充最多的元素。当HashMap中元素的数量超过了加载因子所允许的最大值,HashMap将增加一半的容量,并重新映射元素。因此,在初始化HashMap的时候,不要把capability设置的过大或者将load factor设置的过小,如果迭代性能很重要的话。
- 作为一个通用的规则,默认的加载因子设置为0.75,提供了一个在时间和空间耗费上的平衡。
- HashMap不是一个同步的集合,如果多个线程同时操作该集合的,需要有同步操作。可以通过以下方法来创建一个同步集合Collections.synchronizedMap,例如:
Map m = Collections.synchronizedMap(new HashMap(...);
- HashMap默认初始化的容量capability是16,最大容量是1<<30。默认的加载因子是0.75。
/*
* 返回该Map映射的key-value集合,该Set集合是被Map共享的,
* 任何在该Set上的修改,都会反应到该Map中。
*/
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
接下来看EntrySet的实现
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();//返回Entry的迭代器
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
}
EntrySet继承了AbstractSet,并且迭代器方法iterator返回了一个EntryIterator。EntryIterator的实现如下:
//继承自HashIterator
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
EntryIterator继承自HashIterator,并且实现了迭代器的接口Iterator。HashIterator的实现如下:
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;//保存entry的数组
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
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();
//寻找下一个节点
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
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;
}
}
table为保存Node节点的数组,
首次使用时初始化,必要时重新定义大小。定义如下:
transient Node<K,V>[] table;
HashMap采用的是散列表形式来保存键值对节点Node,通过计算key值的散列码找到table数组中对应的index。如果index对应的卡槽没有保存Node节点,则将Node节点保存在该卡槽中。如果已经有Node节点保存在该卡槽中,说明发生了冲突,需要采用链地址法解决冲突,即把所有散列到同一个卡槽位置的Node节点以链表的形式保存起来。如下图所示:
在HashIterator类中,实现了迭代器方法hasNext()、remove()方法,这些方法都是基于table数组来操作的。其中nextNode()节点方法是返回下一个节点,具体的实现过程是,先从卡槽位置index = 0的位置开始,查找table数组中是否有对应Node节点,如果有对应的Node节点,则沿着该Node节点所指向的链表查找下一个节点,直到查找到链表的末尾位置。如果该卡槽没有对应的Node节点,则继续查找下一个卡槽。
可以看出来,返回元素的顺序与元素插入的顺序是不一致的,因为在元素插入的时候,是根据key值的散列码插入对应卡槽的链表,而元素的遍历是从数组卡槽位置依次逐个遍历的。因此HashMap的元素插入顺序与返回顺序是不一样的。
key值散列码的计算方法如下:
/*
* hash方法是求key值对于的hash值,并且将hash高16位与低16位进行XOR(异或)操作,作为key值最终的hash值。
* 这样做的目的是为了减少冲突
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在HashIterator的remove方法调用了removeNode()方法,接下来看下removeNode()方法的实现:
/*
* 实现Map.remove和相关方法
* HashMap采用的是链地址法解决冲突,而且采用的是单链表
* hash key值的hash值
* value 需要匹配的value值,如果matchValue为true的话,否则忽略
* matchValue 如果为true的话,则只删除value相等的元素
* movable 如果为false的话,在删除的时候,不移动其他元素
*/
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 &&
(p = tab[index = (n - 1) & hash]) != null) {//节点p是开始查找的起始节点
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;//节点p就是所要查找的节点
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {//循环查询链表,直到查找到hash值和key值符合的节点
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)
tab[index] = node.next;// 删除的节点是头结点
else
p.next = node.next; // 删除的节点是中间节点
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
可以看到,removeNode()删除一个节点的大致流程如下:
1.先查找table数组卡槽中节点即链表的头结点,是否为需要删除的节点;
2.如果不是,遍历链表找到需要删除的节点,并保存删除节点的前驱节点;
3.通过前面两步,找到了需要删除的节点,查看是否需要matchValue;
4.如果不需要matchValue,则将节点删除;
5.返回删除的node;
在EntrySet的contains方法中,我们看到调用了getNode()方法,接下来看下getNode()方法的实现:
/*
* 实现Map的get和相关的方法
* hash key值的散列码
* key key值
*/
final Node<K,V> getNode(int hash, Object key) {
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 && // 找出key和hash都符合的node
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
根据key值获取对应value值的流程是先查询table数组中的头结点是否满足,如果不满足的话,则沿着头结点指向的链表往下查找,直到找到满足条件的节点为止。
有获取节点的办法,就应该有相应存储节点的办法,HashMap存储节点的办法为putVal(),实现如下:
/*
* 实现Map的put和相关方法
* hash key的hash值
* key key值
* value 需要保存的value值
* onlyIfAbsent 如果为true,则不改变已经存在的值
* evict 如果为false,则table处于创建模式
*/
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;//table为空,重新创建一个table
if ((p = tab[i = (n - 1) & hash]) == null)//如果table中hash值对应的第一个值为空,创建一个新的node节点
tab[i] = newNode(hash, key, value, null);
else {//先查找是否已经存在对应key值的node节点
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//找到了头结点与key值相等
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);//如果没有找到节点与key值相等,则在链表的尾部添加一个新的节点
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;//找到了中间某个节点与key值相等
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
//如果onlyIfAbssent为空或者oldValue值为空时,才更新value值,并返回oldValue,否则的话,返回null
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果size大小超出了HashMap的容量的临界值,则扩展HashMap的容量threshold = capability*load factor。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
putVal方法的实现过程大致为:
1.通过hash值查找table数组对应卡槽位置是有节点存储,如果没有节点存储,则在该卡槽位置存储一个新节点;
2.如果该卡槽位置有节点存储,则查找是否有对应key值的节点存在,如果没有对应key值的节点存,则说明该key对应的节点是新插入的,在链表的尾部添加一个新的节点。
3.如果有对应key值的节点存在,则说明先前已经映射过该key值了,只有onlyIfAbsent为false或者oldValue为null时才更新原来存在的value。
4.如果添加元素过后,HashMap的大小超过了HashMap的临界值threshold,则需要扩展HashMap的容量。
5.如果是新添加节点,则返回null,如果是更新key值对于的value,则返回更新前对应的value。
/*
* 初始化或者加倍table的大小,如果为table为空,则根据初始容量threshold来分配。
* 另外,由于table的大小是两倍的扩展,扩展后容器内的元素要么保存在原来的位置,要么保存在原来位置的两倍偏移位置。
*/
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 (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {//旧表的容量已经超出了最大值,则直接返回
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) //旧表的容量大于默认初始化容量16,新表的容量是旧表容量的2倍。
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;// 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 16*0.75 = 12
}
// 如果新的table临界值为0的话,则通过新表格的容量乘以加载因子得出新的临界值
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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建一个新table数组
table = newTab;
//将旧表oldTab元素拷贝到新表newTable中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;//释放旧表保存的node节点
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;//如果卡槽j处只有一个node节点, 则直接保存到新表数组中
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// table扩展容量后,可以分为两部分,一部分还是原来的那部分,用loHead和loTail表示
// 新增的那一部分,用hiHead和hiTial来表示
// 旧表格的元素要么保存在原来的那部分位置,要么被保存在新添加的那部分,他们之间相差一个capability。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;//链表中下一个节点
// 保存在旧表格中节点,通过loHead和loTail来指定需要保存的链表
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}// 保存在新表格中的节点,通过hiHead和hiTail来指定需要保存的链表
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容量调整的大致流程如下:
1.根据原来的表计算出新表的容量newCap和新表的临界值newThr。
2.根据新表的容量创建一个新的table数组;
3.将原来表中元素拷贝到新表中,元素存储的位置有两种情况:一种是保持原来表中的位置j,另一种是在原来表中的位置基础之上偏移原来表格的大小,即节点保存在j+oldCap位置。
最后来看看table数组中保存的Node节点:
/*
* Node节点实现Map的Entry
* next指向下一个Node节点,维护的是单链表结构
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
Node节点是以单链表的形式来连接。
EntrySet的类图如下:
4.LinkedHashMap<K,V>类
- LinkedHashMap是一个实现了Map接口的哈希链表。
- LinkedHashMap不同于HashMap,它维护一个双向链接链表。迭代器返回的元素顺序就是元素插入时的顺序。
- LinkedHashMap可以被用来产生Map集合的一份拷贝,并且元素的顺序保持不变,不管原始的map实现是什么,例如:
void foo(Map m) {
Map copy = new LinkedHashMap(m);
...
}
- LinkedHashMap集合允许添加null元素,像HashMap一样,LinkedHashMap在常数时间内完成一些基本的操作,例如add、contains、remove等操作。LinkedHashMap的性能要稍微低于HashMap,因为LinkedHashMap需要维护一个链表。
- LinkedHashMap有两个因素影响它的性能:初始容量capability、加载因子Load Factor。
- LinkedHashMap方法不是同步的,如果是多个线程使用的话,需要同步。可以通过Collections.synchronizedMap方法来获取同步的容器。例如:
Map m = Collections.synchronizedMap(new LinkedHashMap(...));
LinkedHashMap中保存的Entry是基于HashMap的Node节点,只是Entry多了两个指针,一个指针指向前一个entry,另一个指针指向后一个entry,由此构成双链表。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
在LinkedHashMap中还保存了双向链表的头entry指针和尾entry指针。
// 保存双向链表的头Entry,存留时间最久的节点
transient LinkedHashMap.Entry<K,V> head;
// 保存双向链表的尾Entry,存留时间最短的节点
transient LinkedHashMap.Entry<K,V> tail;
LinkedHashMap的双链表结构如下图所示:
final boolean accessOrder;// 该变量决定是以访问顺序遍历还是以插入顺序遍历,如果为true则以访问顺序遍历,否则则以插入顺序遍历。
LinkedHashMap是继承自HashMap,构造函数最终还是会调用到LinkedHashMap。
//LinkedHashMap最终调用的还是HashMap的构造函数
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
LinkedHashMap的entrySet方法实现如下:
/*
* 返回该Map的key-value集合
*/
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
LinkedHashMap的entrySet由LinkedEntrySet实现,该实现继承自AbstractSet:
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();//返回LinkedEntryIterator迭代器
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);//调用HashMap的getNode去获取key对于的Node节点
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
}
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;//指向下一个Entry
LinkedHashMap.Entry<K,V> current;//指向当前Entry
int expectedModCount;
LinkedHashIterator() {
next = head;//指向头Entry
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;//保存当前Entry到current
next = e.after;//next指向下一个Entry
return e;
}
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);//调用HashMap的removeNode移除节点
expectedModCount = modCount;
}
}
LinkedHashMap有自己的get方法实现,如果设置了按访问顺序迭代元素,则还需要调用HashMap的钩子函数
afterNodeAccess。
/*
* 获取key值对应的value,value可能为null
*/
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果设置了以访问顺序迭代元素,则调用afterNodeAccess钩子函数
// 该函数在HashMap中定义了。
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
/*
* 将被访问过的Node移动到最后,在HashMap中定义该钩子函数
*/
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {//节点e不是最后的节点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;//p保存e节点,b保存e的前一个节点,a保存的是e的下一个节点
p.after = null;
if (b == null)// e的前一个节点为空,则说明e是头结点
head = a;// 将头结点保存为e的下一个节点
else
b.after = a;//节点e从链表中被移除
if (a != null)
a.before = b;
else
last = b;//e的下一个节点为空,则说明e是尾节点,将尾节点指向e节点的前一个节点
if (last == null)
head = p;
else {
p.before = last;
last.after = p;//将e节点链接到链表的尾部
}
tail = p;//更新tail指针
++modCount;
}
}
可以看到 afterNodeAccess函数只做了一件事,将访问过的节点移至双向链表的尾部,这样可以以访问顺序来迭代该集合。 LinkedHashMap还有几个其他的HashMap钩子函数实现,分别是afterNodeInsertion()、afterNodeRemoval()、newNode(),分别在插入元素、删除元素、创建新的元素时回调。
/*
* 在插入元素后,回调该方法,在HashMap中定义
* evict
*/
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);//调用removeNode方法移除该Node
}
}
/*
* 如果返回true的话,将移除保存最久的元素。
* 该方法在调用put或者putAll方法插入一个新元素时调用。
* 这样可以让实现者有机会在添加新元素的时候,可以删除保留最久的元素。
* 这种场景非常适合用于cache,通过删除过时的entry来减少内存消耗。
* 一个简单的例子,覆写该方法,并且设定最大保留的缓存数量为100,这样当超过100后,就会删除一些元素。
* private static final int MAX_ENTRIES = 100;
* protected boolean removeEldestEntry(Map.Entry eldest) {
* return size() > MAX_ENTRIES;
* }
* eldest 最近最少访问的entry,如果该方法返回true,则该entry将被删除。
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
/*
* 在删除元素后,回调该方法,在HashMap中定义
*/
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;//断开节点e到前一个节点和后一个节点的链接
if (b == null)//e的前一个节点为空,则说明e是头结点
head = a;//将头结点head指向e的下一个节点
else
b.after = a;
if (a == null)//e的下一个节点为空,则说明e是尾节点
tail = b;//将尾节点tail指向e的前一个节点
else
a.before = b;
}
/*
* 创建一个新的节点,在HashMap中定义了该方法
*/
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);//将节点链接到链表的尾部
return p;
}
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
LinkedEntrySet类图如下图所示:
5.TreeMap<K,V>类
- TreeMap<K,V>底层是基于红黑树实现的。
- TreeMap是一个有序的Map,根据关键字key的Comparable属性排序或者外部提供的Comparator比较排序。
- TreeMap能在log(n)时间内执行完containsKey、get、put、remove等操作。
- TreeMap通过compareTo()或者compare()方法,执行所有的元素比较操作。
- TreeMap集合不是同步的,如果是多线程操作,必须提供相应的同步。可以通过Collections.synchronizedSortedMap方法创建一个同步的容器。例如:
SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));
TreeMap底层是基于红黑树实现的,保存的节点数据结构如下:
//在TreeMap中保存的节点
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;//保存key键值
V value;//保存value值
Entry<K,V> left;//左节点
Entry<K,V> right;//右节点
Entry<K,V> parent;//双亲节点
boolean color = BLACK;//红黑树的颜色
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
}
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
}
}
6.SortedMap<K,V>接口
SortedMap提供一个key值排序好的Map集合,排序的规则根据key的Comparable属性或者外部提供的Comparator比较器。
所有插入
SortedMap的元素必须实现Comparable接口或者提供外部的Comparator比较器。
所有实现SortedMap接口的构造器必须实现以下四类构造器:
- 默认无参构造器;创建一个空的排序Map,排序规则根据元素自带的比较属性。
- 带一个Comparator的构造器;创建一个空的排序Map,元素的排序规则根据Comparator来决定。
- 带一个Map类型参数的构造器;创建一个新的排序Map,并且包含参数Map中的元素和排序规则。
- 带一个SortedMap参数的构造器;创建一个新的排序Map,并且包含参数SortedMap的元素和排序顺序。
四、对Map的选择
所有的Map实现的插入操作都会随着Map尺寸的变大而明显变慢,但是,查找的代价通常要比插入的要小得多,这是个好消息,因为我们执行查找元素的操作要比执行插入元素的操作多很多。
TreeMap通常要比HashMap要慢,与使用TreeSet一样,TreeMap是一种创建有序列表的方式。当使用Map时,你的第一选择应该是HashMap,只有在你要求Map始终保持有序,才需要使用TreeMap。
LinkedHashMap在插入时比HashMap慢一点,因为它维护散列数据结构的同时还要维护链表。正是由于这个列表,使得其迭代速度更快。
HashMap(默认的选择)
|
Map基于
散列表的实现。在
插入和查询“键值对”的开销是固定的。可以通过构造器设置
容量和
负载因子,以调整容器的性能。
|
LinkedHashMap
| 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入的次序,或者是最近最少使用LRU的次序,只比HashMap慢一点,而在迭代访问时反而更快,因为它使用链表维护内部次序。 |
TreeMap
|
基于红黑树的实现。查看“键”或者“键值对”时,
他们会被排序(次序由Comparator或者Comparable决定)。TreeMap的特点在于,所得到的结果是
经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。
|
WeakHashMap
|
弱键映射,允许释放映射所指的对象;这是解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则此“键”可以被垃圾收集器回收。
|
ConcurrentHashMap
|
一种线程安全的Map,它不涉及同步加锁。
|