标准库中包含了几种Map的基本实现,包括:HashMap、TreeMap、LinkedHashMap、WeekHashMap、ConcurrentHashMap、IdentityHashMap。它们都有同样的基本接口Map,但是行为特性各不相同,这表现在效率,键值对的保存及呈现次序、对象的保存周期、映射表如何在多线程程序中工作和判定“键”等价的策略等方面。
Map可以将键映射到值。一个映射不能包含重复的键;每个键最多只能映射到一个值。Map 接口提供三种collection 视图,允许以键集、值集或键-值映射关系集的形式查看某个映射的内容。映射顺序定义为迭代器在映射的Collection视图上返回其元素的顺序。某些映射实现可明确保证其顺序,如TreeMap类;另一些映射实现则不保证顺序,如HashMap类。
这几种Map中HashMap是查询效率最高的Map,LinkedHashMap只比HashMap慢一点儿,但是它可以更快的遍历关键字,TreeMap中的关键字都是排序过的,所以可以按序输出。
HashMap* | Map基于散列表的实现(它取代了Hashtable)。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量和负载因子,以调整容器的性能。 |
LinkedHashMap | 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点;而在迭代访问时反而更快,因为它使用链表维护内部次序。 |
TreeMap | 基于红黑树的实现。查看“键”或“键值对”时,它们会被排序(次序由Comparable或Comparator决定)。TreeMap的特点在于,所得到的结果是经过排序的,TreeMap是唯一带有subMap方法的Map,它可以返回一个子树。 |
WeekHashMap | 弱键(week key)映射,允许释放映射所指向的对象;这是为解决某些类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则此“键”可以被垃圾收集器回收。 |
ConcurrentHashMap | 一种线程安全的Map,它不涉及同步加锁。 |
IdentityHashMap | 使用==代替equals对“键”进行比较的散列映射,专为解决特殊问题而设计。 |
Map接口中的(部分主要)方法
containsKey(Object key):如果此映射包含指定键的映射关系,则返回 true;
containsValue(Object value):如果此映射将一个或多个键映射到指定值,则返回 true;
entrySet():返回此映射中包含的映射关系的 Set 视图;
get(Object key):返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null;
keySet():返回此映射中包含的键的 Set 视图;
put(K key, V value):将指定的值与此映射中的指定键关联(可选操作)。
AbstractMap提供 Map 接口的骨干实现,以最大限度地减少实现Map接口所需的工作。要实现不可修改的映射,编程人员只需扩展此类并提供 entrySet 方法的实现即可,该方法将返回映射的映射关系Set视图。通常,返回的 set 将依次在AbstractSet上实现。此 set 不支持add或remove方法,其迭代器也不支持 remove 方法。要实现可修改的映射,编程人员必须另外重写此类的 put 方法(否则将抛出 UnsupportedOperationException),entrySet().iterator() 返回的迭代器也必须另外实现其remove方法。
HashMap及其实现方式
HashMap是基于哈希表实现的,它实现了Map接口,同时允许使用null为key和null为value(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同)。HashMap不保证映射的顺序,特别是它不保证该顺序恒久不变。
假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代collection视图所需的时间与HashMap实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
HashMap 的实例有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。
HashMap的数据结构
在HashMap中比较重要的两个参数时初始容量和加载因子:
在构造完成之后,loadFactor会被记录下来,initialCapacity会变成2的最小次方数,并与loadFactor相乘得到threshold:
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
这样HashMap的所有元素都被放进了Entry构成的数组中,Entry相当于Hash表中的“桶”(这里只称HashMap内的Entry数组为桶,而不包含链表中的Entry),它的内部包含着key,value以及指向下一个和前一个Entry的指针。
这个“桶”就是HashMap中的表现为table数组:
HashMap的get方法实际上就是为了找到某个Entry,这个Entry的key和给定对象相等:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {//根据key查找Entry
int hash = (key == null) ? 0 : hash(key);//找到hash值,确定桶的位置
for (Entry<K,V> e = table[indexFor(hash, table.length)];//找到桶中第一个元素,判断是否在桶中
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&//判断桶中的元素是否是要添加的元素
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
Put方法就是要把数据放入特定的Entry中:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);//计算位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//判断当前桶中是否已经包含该元素(判断key是否已经存在)
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {//防止空间不足进行rehash
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);//插入节点
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];//将当前bucketIndex元素存起来
table[bucketIndex] = new Entry<>(hash, key, value, e);//设置当前bucketIndex元素,并把原来的元素e,设置为新元素的后继(链表的头插法,新元素插入头部)
size++;
}
put时在使用indexFor找到下标后,需要注意当前桶中是否已经存在给定key对应的元素,这时需要遍历桶中的所有元素,然后在确定没有给定key对应的元素时,就可以将当前给定的元素插入这个桶的第一个位置(其他元素后移)。
做put操作时,可能会出现桶的空间不足(也就是size比threshold要大了,此时冲突的可能性会很大),就需要rehash一次,将空间变为当前空间的两倍(即,resize(2 * table.length)),然后将所有的桶移入新的桶中:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);//这里进行了转移操作
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//逐个移动桶中元素
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//依然采用头插法,会导致桶中元素逆序
newTable[i] = e;
e = next;
}
}
}
删除操作也是如此,找到对应的桶,然后遍历桶中元素,并在找到元素后删除它:
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {//遍历桶中元素
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&//检查到目标元素
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)//第一个元素就是目标元素
table[i] = next;
else//第一个元素不是目标元素
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
TreeMap及其实现方式(TreeMap的实现要看红黑树的实现方式)
TreeMap是基于红黑树(Red-Black tree,具体请参照博客:Red Black Tree)的 NavigableMap 实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。它能够保证containsKey、get、put 和 remove 操作的时间开销为 log(n)。这里的红黑树算法是依据算法导论中的红黑树算法实现的。
在TreeMap中保存着红黑树的树根:
当要使用put方法插入数据时,会依据红黑树的插入算法,将数据插入特定的位置,由于红黑树本身是二叉排序树,因此可以按照结点的大小找到目标位置,并插入当前位置,然后再维护红黑树的结构,使得它的结构符合红黑树的约束。
使用get方法获取数据时,会按照二叉排序树的规则比较从根到叶节点的元素,直到发现或找不到目标元素为止:
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
containsKey(Object)方法和get方法是一致的,也是使用getEntry来查找目标元素。
remove(Object)方法是利用查找到红黑树中的结点,然后删除该结点,并维护红黑树的特征来实现的。
对于TreeMap,重要的是遍历的方法,其遍历的方法是由EntryIterator来实现的,它继承自PrivateEntryIterator。在PrivateEntryIterator中可以发现,它使用了红黑树中求当前结点的前驱(比当前元素小的最大元素)和后继(比当前元素大且最小的元素)的算法来进行遍历。
LinkedHashMap及其实现方式
LinkedHashMap是基于哈希表和链表对Map接口的实现,它可以保存数据插入链表的顺序(使用额外于HashMap的链表实现)。此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。注意,如果在映射中重新插入键,则插入顺序不受影响。(如果在调用m.put(k, v)前m.containsKey(k)返回了true,则调用时会将键 k 重新插入到映射 m 中。)
注:这里虽然把链表和桶的图分开画了,但是实际上它们中的结点(除了header)都是共用的
LinkedHashMap继承自HashMap,所以它比HashMap的性能略差,但是可以维护元素间的插入顺序(使用一个双向链表来保存顺序):
private transient Entry<K,V> header;
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
…….//省略
}
当要调用put方法插入元素时,会调用HashMap的put方法,这个方法会调用addEntry()方法,这个方法在LinkedHashMap中被重定义了:
//LinkedHashMap的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);//调用HashMap中的addEntry方法,会创建结点,同时会维护新创建的结点到双向链表中
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
//HashMap中的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//LinkedHashMap中的createEntry,覆盖HashMap中的createEntry
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
从以上代码中我们可以看到LinkedHashMap的put方法的过程,首先LinkedHashMap中没有put方法,所以会调用HashMap中的put方法,这个put方法会检查数据是否在Map中,如果不在就会调用addEntry方法,由于LinkedHashMap覆盖了父类的addEntry方法,所以会直接调用LinkedHashMap的addEntry方法,这个方法中又调用了HashMap的addEntry方法,addEntry又调用了createEntry方法,这个方法也是LinkedHashMap覆盖了HashMap的,它会创建结点到table中,同时会维护Entry(继承自HashMap.Entry的LinkedHashMap.Entry)的前后元素。
//HashMap中的createEntry方法,对比以上LinkedHashMap中的createEntry方法发现,除了将Entry放入桶中之外,LinkedHashMap还维护了Entry指向之前元素和之后元素的指针
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
简单来讲,LinkedHashMap中的Entry是带有指向在它自己插入Map之前和之后的元素引用的对象,在put元素时,首先检查数据是否已经在Map中,如果不在就创建这个Entry,同时还要把这个Entry记录插入到之前元素构成的链表中(并没有真的简单的创建了个链表结点,而是这个链表本身就是这些Entry元素构成的)。这些Entry本身不但是Map中table的元素,还是链表元素。
在进行遍历时,它使用的是KeyIterator,而KeyIterator继承自LinkedHashIterator,在LinkedHashIterator内部有链表的头指针指向的下一个元素:
由于这些Entry本身是链表元素,也是table中元素,故直接找到其后继就可以得到所有元素。剩下的遍历过程就是对一个链表的遍历了,每遍历到一个Entry就可以获得它的key和value。
此外,LinkedHashMap还能维护一个最近最少访问的序列,其本质还是维护Entry指针,每次使用get访问元素时,都会将这个元素插入Map尾部,这样链表头部就是最近访问次数最少的元素了,整个链表就是从近期访问最少到近期访问最多的顺序。
其实现方式是,在get中找到要get的元素后调用元素的recordAccess方法,这个方法就把这个Entry的前后指针进行了调整。
//LinkedHashMap的get方法
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);//调整指针
return e.value;
}
//Entry的recordAccess方法,参数m就是一个LinkedHashMap
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {//是否按照最近最少访问排列
lm.modCount++;
remove();//从当前链中删除自己
addBefore(lm.header);//加入到链表尾部
}
}
总的来说,对于所有的集合类来说,对于List,如果随机存取多于修改首尾元素的可能,则应该选择ArrayList,如果要实现类似队列或者栈的功能或者首尾添加的功能较多,则应该选择LinkedList;对于Set,HashSet是常用的Set,毕竟通常对Set操作都是插入和查询,但是如果希望产生带有排序的Set则可以使用TreeSet,希望记录插入顺序则要使用LinkedHashSet;而Map和Set类似,如果需要快速的查询和添加,则可以用HashMap,如果需要Map中的元素按照一定的规则排序,则可以用TreeMap,如果需要记录数据加入Map的顺序,或者需要使用最近最少使用的规则,则可以用LinkedHashMap。