Map
将键映射到值的对象。对于一个映射来说键不能重复,一个键不能对应到多个值上。
一般情况下通过equal来判别键是否重复,但是也存在特殊的Map使用“==”来判别是否重复,如IdentityHashMap。
Map有3中collection视图。①键集②值集③键-值映射关系集。由于实现机制的不同,有些实现类可以保证视图的迭代顺序,如TreeMap,有些无法保证,如HashMap。
这里有一点需要注意一下。值从类型上可以分为2种,一种是基本类型的封装类,这个就不说了;另外一种是普通的值对象,由于对于值对象使用的是它的引用关系,所以这里的值对象是可以修改它的状态,使用时尤其是涉及到多个值对象修改时。
对于用可变对象作为Map的键的情况,尽量不要对可变对象做可以修改equals返回值的操作,否则的话会发生很不好的事情。
Map.Entry
键值映射关系集(Entry)是Map中很重要的一个组成部分,属于Map的封闭接口。它的功能类似于列表中的迭代器,get、containKey、containValues、remove等操作都和它息息相关,Map的键集和值集也是通过它来生成。
AbstractMap
AbstractMap是Map的骨干实现,提供Map实现的功能,如size、get、containKey、containValues、remove等操作,以及键集(keySet)、值集(values)的构建。这些功能主要由Map.Enry来实现。
public Set<K> keySet() {
if (keySet == null) {
keySet = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
privateIterator<Entry<K,V>> i = entrySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public K next() {
return i.next().getKey();
}
public void remove() {
i.remove();
}
};
}
……
};
}
return keySet;
}
值集的实现与上述代码类似。
如果我们要实现不可修改的Map时,只需要扩展此类并提供 entrySet 方法的实现即可,该方法将返回映射的映射关系 Set 视图。
public abstract Set<Entry<K,V>>entrySet();
当然如果要实现可修改的Map,我们还需要重写put方法,并根据实际情况重写remove方法。如果还有其他的需求,我们可以重写别的相关方法。
HashMap
public class HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V>, Cloneable,Serializable{
……
}
HashMap是我们最常用到的Map了,基于哈希表实现,提供所有的映射操作,支持null键和null值,不保证映射顺序。与HashTable很类似,区别在于不支持同步操作和对null的处理上。
HashMap的数据结构是以Entry数组为主,利用哈希算法来将键对象的哈希值转换为数组的index值。
static int hash(int h) {
// 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);
}
Hash值的计算方法,HashTable采用的计算方式与这个略有不同。
如果不同的值对象对应到了同一个index上该怎么办呢?这里Entry实现时,采用了链式的结构,每一个Entry可通过next找到后续的Entry。
Entry1-->Entry2-->EntryN
比较有意思的一点就是,新增的值会放在Entry链的头上,而不是尾部。不太清除为什么这么设计,见Entry的构造器
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
保存时,是将键值映射关系作为整体保存的。读取的时候,首先通过键的hashCode计算哈希值,并通过数组大小一起计算得到对应的index值,对得到的Entry链逐个判别,每个entry.hash是否等于计算出的哈希值,entry.key是否等于查询的key。
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}
其余的操作类似,可以通过源码查看。
在HashMap中有2个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。
默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本,因为存的太满的话,Entry链上的数据就会明显增多。这个就需要根据实际情况来看了,对于大数据量来说,设置一个较大的初始容量是很重要的。
LinkedHashMap
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
……
}
Map接口的哈希表和连接表实现,具有可预知的迭代顺序。与HashMap的不同点在于维护了一个双向链表,在增加删除元素时,在维护其存储结构的同时也在维护一个双向链表,该链表记录了加载顺序。
private static class Entry<K,V>extends HashMap.Entry<K,V> {
Entry<K,V> before, after;
private void remove() {
before.after = after;
after.before = before;
}
private void addBefore(Entry<K,V> existingEntry) {
after =existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
……
}
可见链表也是使用Entry来实现的,但是在HashMap的Entry的基础上增加了前后Entry信息。其实我们发现Entry连包含了所有的元素信息,迭代器也是通过它来实现的。
注意,这里的after和before主要是给双向链表使用的,而保存时数据结构还是依赖于单向链表的,和HashMap一样。
LinkedHashMap还有另外一种迭代顺序--最近最少使用(LRU),注意变量 accessOrder ,默认为false,意味着迭代按照加载顺序来。我们可以将它设置为true,将按照访问情况迭代,从近期访问最少到近期访问最多的顺序,可以用来创建LRU缓存。
IndentityHashMap
public class IdentityHashMap<K,V> extendsAbstractMap<K,V> implements Map<K,V>, java.io.Serializable, Cloneable{
……
}
IndentityHashMap也是采用键的hash值来进行散列的Map,也可以支持null,但是与HashMap不同的是它在判别键是否重复时比较键(和值)时使用引用相等性代替对象相等性,及不是通过equals的而是通过“==”来进行的。
它不是通用的Map实现,一般情况下都是使用equals来对对象进行比较,它是为特殊情况使用的。
它为简单的线性探头哈希表,数组交替保持键和值。从源代码中可以看到,首先通过键的哈希值找到数组index,如果满足条件就将值保存在index+1上,不行就后移2位。
public V put(K key, V value) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
Object item;
while ( (item = tab[i]) != null) {
if (item == k) {
V oldValue = (V) tab[i + 1];
tab[i + 1] = value;
return oldValue;
}
i = nextKeyIndex(i, len);
}
modCount++;
tab[i] = k;
tab[i + 1] = value;
if (++size >= threshold)
resize(len);
return null;
}
我们知道HashMap使用了桶和链来共同实现,这里只使用了一个Object数组。可见在空间上IndentityHashMap有更大的需求,由于所有操作都是在数组上进行的,查询效率应该也会高于HashMap。
但是删除一个元素是很麻烦的,删除后需要判别后续的元素是否需要前移,这个花费也是不小的。
由于初始的数组最大值,如果超过最大值则会对数组扩容,这个代价是很高的。但过大的会影响到迭代效率,需要平衡处理。
WeakHashMap
public class WeakHashMap<K,V> extendsAbstractMap<K,V> implements Map<K,V> {
……
}
从结构上看同hashMap很相像,但是会维护一个弱引用队列
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
在维护每一个Entry时,都会将key加载入弱引用队列中。Entry只是保留一个对键值的弱引用。但可以值可不是弱引用,而是一个强引用。但WeakHashMap不会主动释放失效的弱引用,而是
在每次对Map进行操作时,都会调用
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;
}
}
}
}
将弱引用队列中对应的数据清除。
在 WeakHashMap中,当某个键不再正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除,因此,该类的行为与其他的 Map 实现有所不同。
WeakHashMap中的值对象由普通的强引用保持。因此应该小心谨慎,确保值对象不会直接或间接地强引用其自身的键,因为这会阻止键的丢弃。注意,值对象可以通过WeakHashMap本身间接引用其对应的键;这就是说,某个值对象可能强引用某个其他的键对象,而与该键对象相关联的值对象转而强引用第一个值对象的键。处理此问题的一种方法是,在插入前将值自身包装在 WeakReferences中,如:m.put(key, newWeakReference(value)),然后,分别用get进行解包。
EnumMap
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K,V>
implements java.io.Serializable,Cloneable{
……
}
EnumMap是一个比较特殊的Map,它只允许Enum元素作为它的键,否则的话就会抛出ClassCastException异常。不允许使用null键,但是允许使用null值。
所有基本操作都在固定时间内执行。虽然并不保证,但它们很可能比其 HashMap
副本更快。
其数据结构,由于在初始化时已通过
private static <K extends Enum<K>>K[] getKeyUniverse(Class<K> keyType) {
return SharedSecrets.getJavaLangAccess()
.getEnumConstantsShared(keyType);
}
获取到了键集,所以我们在保存时主要处理值数组,值数组的大小等于已知键集的大小。且值对应的位置有枚举(ordinal)键的序号确定。Entry尽需要保存index信息。
SortedMap
保证按照键的升序或是自然排序的映射,我们可以也可以自定其他的排序方式,这个有接口提供的comparator方法提供支持,我们可以按照实际需要来实现不同的比较方法。对有序映射的集合视图(由 entrySet、keySet 和 values 方法返回)进行迭代时,此顺序就会反映出来。
由于使用了比较器,所以就要求所有的键都必须实现Comparable接口,而且键之间应该是可以比较的。
注意,如果有序映射正确实现了 Map 接口,则有序映射所保持的顺序(无论是否明确提供了比较器)都必须保持相等一致性。这也是因为 Map 接口是按照 equals 操作定义的,但有序映射使用它的compareTo(或 compare)方法对所有键进行比较,因此从有序映射的观点来看,此方法认为相等的两个键就是相等的。即使顺序没有保持相等一致性,树映射的行为仍然是 定义良好的,只不过没有遵守 Map 接口的常规协定。
NavigableMap
该接口是在java1.6版本后增加的。
其中对键集(Key)的操作有:
- ceilingKey(key):用来获取大于或者等于给定的key的第一个键,如果没有的话就返回null。
- floorKey(key):用来获取小于或者等于给定的key的第一个键,如果没有的话就返回null。
- higherKey(key):用来获取大于给定的key的第一个键,如果没有的话就返回null。
- lowerKey(key):用来获取小于给定的key的第一个键,如果没有的话就返回null。
对键值映射关系集(Entry)的操作有:
- ceilingEntry(key):用于获取大于或等于给定key的第一个实体,如果没有则返回null。
- firstEntry():用于获取Map的第一个实体,如果没有则返回null。
- floorEntry(key):用于获取小于或等于给定的第一个实体key,如果没有则返回null。
- higherEntry():用于获取大于给定的key的第一个实体,如果没有则返回null。
- lastEntry():用于获取Map最后一个实体,如果没有则返回null。
- lowerEntry(key):用于获取小于给定key的第一个实体,如果没有则返回null。
这里有两个单步从Map获取和删除实体的方法。提供了一个简单的不用使用迭代器而遍历所有Map元素的方法。下面是具体的介绍:
- Map.Entry<K,V> pollFirstEntry():获取Map第一个键的实体并且从Map中移除该实体,如果Map为空则返回null。
- Map.Entry<K,V> pollLastEntry():获取Map最后一个键的实体并且从Map移除该实体,如果Map为空则返回null。
TreeMap
public class TreeMap<K,V> extendsAbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
……
}
基于红黑树的实现。此类保证了映射按照升序顺序排列关键字,根据使用的构造方法不同,可能会按照键的类的自然顺序进行排序,或者按照创建时所提供的比较器进行排序。
红黑树是一种自平衡的二叉树。每个节点是一个五元组:color(颜色),key(数据),left(左孩子),right(右孩子)和p(父节点)。其中可以保证left<self<right。具有以下特性:
性质1. 节点是红色或黑色
性质2. 根是黑色
性质3. 所有叶子都是黑色(叶子是NIL节点)
性质4. 如果一个节点是红的,则它的两个儿子都是黑的
性质5. 从任一节点到其叶子的所有简单路径都包含相同数目的黑色节点。
由于红黑树比较复杂,这里就避重就轻不讨论它的结构和操作,这样TreeMap中很多结构操作就先当做是透明的,以后详细研究红黑树时可以再将这里缺失的部分不全,这里主要精力还是放在TreeMap上,其实也没剩下什么。
TreeMap中红黑树的节点的数据包含两部分,键和值。这里使用Entry实现。创建一个初始的root,按照红黑树增加、删除节点的方式逐步操作,构建最终的数据结构。
由于TreeMap在插入或删除数据时需要对存储结构进行旋转操作,造成了效率上会低于HashMap。但是HashMap无法胜任需要对键集排序的情况,这时我们只能依靠TreeMap。但是其他的情况还是建议使用HashMap,呵呵