本文接着上篇继续探讨更多的Map实现和Set实现。
LinkedHashMap
LinkedHashMap是一个有序的HashMap,它 继承了HashMap类,并且通过内部维护一个链表来保证迭代时有序。
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<K,V> head; LinkedHashMap.Entry<K,V> tail;
关于LinkedHashMap,它的构造器有个有趣的参数accessOrder,这个参数代码有序模式,默认是false,表示按照插入顺序(insertion-order), 如果传入true,则是按照访问顺(access-order):
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
访问顺序的意思是每次插入元素都会在链表尾部插入,因为这是最近访问的元素,如果是获取元素,则将获取的元素移到链表尾部,链表尾部是最近访问,如果我们限定LinkedList大小,很容易实现一个 LRU缓存算法:
package com.deepoove.datastructure; import java.util.LinkedHashMap; public class LRUCache implements Cache { private LinkedHashMap<String, Object> cache; private int size; public LRUCache(int capacity) { size = capacity; cache = new LinkedHashMap<>(capacity, 0.75f, true); } @Override public void put(String key, Object value) { synchronized (this) { if (cache.size() >= size){ cache.remove(cache.keySet().iterator().next()); } cache.put(key, value); } } @Override public Object get(String key) { return cache.get(key); } @Override public String toString() { return cache.toString(); } }
这里的元素没有保存访问次数,改进的LRU算法可以将访问次数也作为排序的一个条件。
LinkedHashMap还提供了 removeEldestEntry一种方法,可以重写该方法,这个方法的返回值是个boolean值,以便在将新映射添加到Map时强制执行自动删除过时映射的策略,这使得实现自定义缓存变得非常容易。
TreeMap
TreeMap是一个按照顺序排序的结构,关于顺序性(Comparable和Comparator),在第一篇概览中介绍过了。
TreeMap间接实现了SortedMap接口,实现基础是一个红黑树, 正如HashMap中对恶化的链表的优化。每个节点的数据结构如下:
static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK; }
性能
从实现角度来说,如果不希望保证顺序,那应该选用HashMap,因为LinkedHashMap还额外维护了一个链表,HashMap查找一个元素最快是O(1),性能最差的情况下会是O(logN)。
HashMap的性能取决于以下三点:
- 负载因子:默认值0.75是个空间和时间上最优的考量,不建议修改
- 容量:我们应当根据预期的存储元素个数来选择合适的初始容量,从而避免重新计算Hash去扩容
- 哈希函数:默认是通hashcode方法通过计算来实现的
接下来会介绍几个专用的Map,这些都是一些优化这三点或者特殊场景的实现。
Map特殊用途实现
EnumMap
如果我们希望一个枚举类型映射一个对象,代码可能是这样的:
HashMap<DAY, String> map = new HashMap<DAY, String>(); map.put(DAY.MONDAY, "星期一");
JDK提供了一个特殊的Map实现EnumMap来做这样的事情,顾名思义,它并不是一个基于哈希表的实现(名称上没有hash单词):
// DAY是一个枚举类 EnumMap<DAY, String> eummap = new EnumMap<>(DAY.class); eummap.put(DAY.MONDAY, "星期一");
那么这两者之间有什么差异,为何要新实现一个EnumMap呢?
这是由于枚举的特殊性,每个枚举值都有个整型常量值,它是枚举声明的序数ordinal所以我们无需哈希函数,或者说哈希函数就是
f(x)=ordinal=index
因为枚举常量值的个数一定的,所以根本无需扩容,它的容量就是枚举常量的个数,所有不需要大材小用使用哈希去实现枚举对象的映射关系。
EnumMap是一个基于固定长度数组Object[] vals为实现的Map,长度为枚举常量的个数,下标是枚举的整型值,当使用枚举作为Key时,我们应当使用这个类来代替HashMap。
WeakHashMap
WeakHashMap是一个Key为弱引用的Map,当这个Key没有强引用的时候,就有可能被垃圾回收器回收。
WeakReferenc类是对一个对象的装饰,继承Reference类,提供了get方法获取这个对象,WeakHashMap的Entry继承了这WeakReferenc。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { V value; final int hash; Entry<K,V> next; Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) { super(key, queue); this.value = value; this.hash = hash; this.next = next; } @SuppressWarnings("unchecked") public K getKey() { return (K) WeakHashMap.unmaskNull(get()); } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>)o; K k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { V v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } // 略 }
源码中可以看到,每个key都会调用super(key, queue);
构造为弱引用,在getKey方法中,通过java.lang.ref.Reference.get()方法获取对象。
IdentityHashMap
IdentityHashMap是一个哈希函数特别的基于哈希表的Map结构,它的Key是通过引用相等(reference-equality)来判断的,而我们熟知的HashMap的key是通过对象相等(object-equality)来判断。
我们看看它计算hash的算法:
private static int hash(Object x, int length) { int h = System.identityHashCode(x); // Multiply by -127, and left-shift to use least bit as part of hash return ((h << 1) - (h << 8)) & (length - 1); }
System.identityHashCode
会返回这个对象的hashcode,无论这个对象的类是否实现了hashCode方法,即当且仅当Key1=Key2,它们才会被认为是相同的关键字,IdentityHashMap可以利用在深拷贝的实现上。
Map并发实现
并发包下提供了接口ConcurrentMap和基于哈希表的高并发实现ConcurrentHashMap,具体会在并发章节中阐述。
Set
Set通用实有:HashSet,TreeSet和LinkedHashSet,和Map的命名很相似,Set接口继承于Collection接口,而Collection和Map接口是相对独立的,为什么要把Map和Set放在同一个章节里介绍呢?
因为JDK做了一个巧妙的实现,这三种Set实现底层都是基于对应的Map实现的,Set的集合就是Map中Key的集合(不允许重复),Map重的value统一设置成了一个固定对象new Object():
private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); public HashSet() { map = new HashMap<>(); } public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); }
关于Set实现原理请参见Map的实现原理,Set也提供了高性能的枚举实现EnumSet,在并发包下,提供了写时复制的类CopyOnWriteArraySet。