集合框架的具体实现
集合框架的实现有如下几类:
- 通用实现
- 特殊实现
- 并发实现
- 包装实现
- 简便实现
- 抽象实现
Set实现
Set
实现分为通用和特殊实现
通用实现
通用实现有三种:
HashSet
TreeSet
LinkedHashSet
HashSet
比TreeSet
快的多,它们的时间复杂度大约是O(1)
vs O(logn)
,但是前者是无序的。如果你需要SortedSet
接口中的操作,或者要求元素有序的迭代,使用TreeSet
。LinkedHashSet
介于HashSet
和TreeSet
之间,它是链表哈希表实现,提供按插入顺序迭代,并且运行几乎和HasSet
一样快。
对于HashSet
,需要记住的是,它的迭代时间和条目数及容量有关,是线性的。因此,选择太大的初始容量既浪费空间,也浪费时间。但选择太小的初始容量,也会导致每次扩容时浪费时间在拷贝数据结构上。如果不分配初始容量,默认值是16。手动指定时,建议选择2的幂次,大小则是预期容量增长的2倍,比如:
Set<String> s = new HashSet<>(64);
而LinkedHashSet
的迭代时间不受容量的影响。
特殊实现
特殊实现有两种:
EnumSet
枚举集合CopyOnWriteArraySet
写时复制数组集合
EnumSET
对针对枚举类型的高性能集合实现。它提供了一个静态工程方法range
支持按范围迭代:
for (Day d : EnumSet.range(Day.MONDAY, Day.FRIDAY))
System.out.println(d);
EnumSet
对传统的位标志也提供了一种丰富的,类型安全的替代:
EnumSet.of(Style.BOLD, Style.ITALIC)
CopyOnWriteArraySet
是写时复制数组(copy-on-write)备份的集合实现。所有的可变操作,比如add
,set
,remove
,都是通过创建一个新的数组拷贝实现的,不需要用锁。即使是迭代,也可以安全的与元素的插入删除同时进行。与大部分集合实现不同的是,它的add
,remove
,contains
方法的用时和集合的大小有关。它仅适用于需要频繁迭代但极少修改的集合,比如维护事件处理列表。
List实现
List
实现分为通用和特殊实现
通用实现
有两种:
ArrayList
LinkedList
ArrayList
使用的最多,它提供时间复杂度为o(1)
的位置访问。它不用为列表中的每个元素分配节点对象,并且在同时移动多个元素时,可以利用System.arraycopy
优势。可以将ArrayList
当作没有同步的Vector
。
如果你经常在列表的头部添加元素,或者在迭代时删除元素,可以考虑使用LinkedList
,这些操作在LinkedList
的时间复杂度是o(1)
,但是在ArrayList
是线性的。但如果是位置访问,则恰好相反。
ArrayList
有一个可选的参数,初始容量,LinkedList
没有初始容量参数,但是实现了队列接口,支持如下7种可选方法:clone
, addFirst
, getFirst
, removeFirst
, addLast
, getLast
, removeLast
。
特殊实现
CopyOnWriteArrayList
CopyOnWriteArrayList
是写时复制数组(copy-on-write)备份的列表实现,和CopyOnWriteArraySet
相似。即使在迭代时,也不需要同步,并且迭代器从来不会抛出ConcurrentModificationException
异常。这个实现适合维护事件处理列表,这种很少修改,但频繁遍历的情况。
如果你需要同步,那Vector
比ArrayList
的同步版Collections.synchronizedList
还要快一点,但是它会载入很多过时的操作,因此要小心使用。
如果列表大小固定,也不需要删除、增加或其他批量操作,那么简化实现的Arrays.asList
值得考虑。·
###Map实现
分为通用实现,特殊实现,以及并发实现
通用实现
有三种:
HashMap
TreeMap
LinkedHashMap
如果你需要SortedMap
接口中的操作,或者按照键有序对集合试图进行迭代,请使用TreeMap
;如果你在意速度,并不关心迭代的顺序,请使用HashMap
;如果你需要接近HasMap
的性能,并且按照插入顺序迭代,请使用LinkedHashMap
。从这方面来说,LinkedHashMap
和LinkedHashSet
很像。
LinkedHashMap
有两项能力LinkedHashSet
不具备。你可以基于key的访问而不是插入来排序,换句话说,只是查找某个key的值,就会导致这个key被重新排序(原文是带到map的末尾)。另外LinkedHashMap
提供了removeEldestEntry
方法,可以重写该方法来实现插入新的映射时自动移除最旧的映射的策略。利用这点可以很容易实现自定义的缓存。
比如,下面的示例维持map中最多100条映射:
private static final int MAX_ENTRIES = 100;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
特殊实现
有三种:
EnumMap
WeakHashMap
IdentityHashMap
EnumMap
内部是作为数组实现的,它为枚举键的使用提供了高性能的Map
实现。它将Map
接口的丰富性、安全性与数组的访问速度结合起来。因此,如果你想将枚举映射到值,请优先考虑使用EnumMap
。
WeakHashMap
只存储键的弱引用(不增加引用计数,类似python中弱引用),这样当某个键在WeakHashMap
外部不再被引用时,其键值对就会被垃圾回收。WeakHashMap
是利用弱引用功能的最简单方式,可用于实现类似注册表这种数据结构,当任何线程都无法访问其键时,条目的实用性就会消失。
IdentityHashMap
基于身份的Map实现,比较抽象,暂不讨论。
并发实现
一种:
ConcurrentHashMap
java.util.concurrent
包中的ConcurrentMap
接口,有原子性的putIfAbsent
, remove
, replace
方法,ConcurrentHashMap
实现了这个接口。
ConcurrentHashMap
是由哈希表支持的高并发,高性能实现。执行检索时它不会阻塞,并且允许客户端选择更新的并发级别。它用于替代Hashtable
,除了实现了ConcurrentMap
接口,它支持Hashtable
中所有的旧方法。但是不建议继续调用这些过时的方法。
Queue实现
分为通用实现和并发实现
通用实现
两种:
LinkedList
PriorityQueue
LinkedList
实现了Queue
接口,提供先入先出的队列操作。
PriorityQueue
是基于堆这种数据结构的优先级队列。它根据构造时指定的顺序(可以是自然排序,或Comparator
对象),对元素进行排序。
队列的检索操作:poll
, remove
, peek
, 和element
,访问队列头部的元素,也就是排序的最小值。如果多个元素都是最小值,那么头部是其中某个元素。
PriorityQueue
及它的迭代器实现了Collection
及Iterator
接口中所有可选可选方法。但是迭代器不保证以特定的顺序遍历PriorityQueue
中的元素。如果需要有序迭代,可以考虑使用Arrays.sort(pq.toArray())
。
并发实现
java.util.concurrent
包含许多同步的(线程安全的)队列接口和类。BlockingQueue
接口实现了检索元素时,等待队列非空,存储元素时,等待空间可用,下面的这些类实现了该接口:
LinkedBlockingQueue
链表实现的FIFO队列ArrayBlockingQueue
数组实现的FIFO队列PriorityBlockingQueue
堆实现的优先级队列DelayQueue
堆实现的基于时间的调度队列SynchronousQueue
使用BlockingQueue
接口的简单机制(不明所以)
Deque实现
分为通用实现和并发实现
通用实现
两种:
LinkedList
ArrayDeque
Deque
接口支持从两端插入、移除及检索元素,它的基本方法是:addFirst
, addLast
, removeFirst
, removeLast
, getFirst
, getLast
。
LinkedList
更灵活,实现了列表的所有可选操作,并且允许null
元素。LinkedList
最适合的操作是迭代时移除元素。但它消耗更多内存。
ArrayDeque
是可调整大小的数组实现,如果考虑效率的话,ArrayDeque
的插入和移除操作效率更高。
可以通过两种方式便利ArrayDeque
:
foreach
ArrayDeque<String> aDeque = new ArrayDeque<String>();
. . .
for (String str : aDeque) {
System.out.println(str);
}
Iterator
ArrayDeque<String> aDeque = new ArrayDeque<String>();
. . .
for (Iterator<String> iter = aDeque.iterator(); iter.hasNext(); ) {
System.out.println(iter.next());
}
并发实现
一种:
LinkedBlockingDeque
如果双向队列为空,方法takeFirst
, takeLast
会等待,直到元素可用。
包装实现
包装实现将任务委派给具体的集合,但是会添加一些额外的功能。按设计模式来说,它算是装饰器模式。包装实现是匿名的,它们提供静态工厂方法,而不是公众类。你可以在Collections
类中找到这些实现。
同步包装
六个核心集合接口:Collection
, Set
, List
, Map
, SortedSet
, SortedMap
,都有静态工厂方法:
public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);
每个静态工厂方法都返回一个同步的(线程安全的)集合。为了确保顺序访问,对原集合的所有访问都必须通过这个返回的集合进行。确保这一点的最容易的方式就是,不要保留原来的集合引用,比如,我们可以用下面这个方式创建一个同步集合:
List<Type> list = Collections.synchronizedList(new ArrayList<Type>());
在面对并发访问时, 用户必须手动同步正在迭代的同步集合。因为迭代通过多次调用集合完成,这必须被整合为一个原子性操作。下面是迭代一个同步的包装集合的惯用方式:
Collection<Type> c = Collections.synchronizedCollection(myCollection);
synchronized(c) {
for (Type e : c)
foo(e);
}
注意,这里使用了synchronized
关键字来手动同步,迭代必须在synchronized
代码块内进行,否则会导致不可预料的行为。迭代集合的视图与此类似,也需要手动同步,比如:
Map<KeyType, ValType> m = Collections.synchronizedMap(new HashMap<KeyType, ValType>());
...
Set<KeyType> s = m.keySet();
...
// Synchronizing on m, not s!
synchronized(m) {
while (KeyType k : s) // 使用while进行迭代
foo(k);
}
注意,手动同步是在同步的Map上进行,而不是它的视图。
不可修改包装
不可修改包装会拦截对集合的修改操作,并抛出UnsupportedOperationException
异常。它们的两种主要作用是:
- 使集合一旦创建便不可修改
- 允许特定的客户端以只读模式访问你的数据结构。你仍然可以保持对原集合的引用,但是暴露包装的引用。通过这种方式,客户端只能看不能改,而你拥有完全的访问权限。
和同步包装类似,六个核心的集合接口都有静态工厂方法:
public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
public static <T> Set<T> unmodifiableSet(Set<? extends T> s);
public static <T> List<T> unmodifiableList(List<? extends T> list);
public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);
public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<? extends T> s);
public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);
接口检查包装
Collections.checked
接口包装用于泛型集合。它们返回指定集合的动态类型安全的视图。如果客户端尝试添加错误类型的元素,视图会抛出ClassCastException
异常。java的泛型机制提供了编译时的类型检查,但是这一机制还是可能会被打破。而动态类型安全视图可以彻底干掉这种可能。
简便实现
如果你不需要通用实现的全部能力,那么简便实现更方便,更高效。
数组的列表视图
Arrays.asList
方法返回其数组参数的列表视图。对列表的修改会写回数组,反之也一样。列表的大小固定,等于数组的长度。对列表调用add
和remove
方法,会抛出UnsupportedOperationException
异常。
列表视图是基于数组的接口与基于集合的接口之间的桥梁。它允许你传一个数组给一个期望集合的方法。另外,它的还有一个用处,如果你需要一个定长的列表,那么它比起通用实现更高效。下面是它的惯用法:
List<String> list = Arrays.asList(new String[size]);
不可变重复元素列表
偶尔你需要一个不可变的列表,它包含同样的元素的多次重复。Collections.nCopies
方法就返回这样一个列表。它有两种用途,一是用来初始化一个刚创建的列表。比如,你想要一个包含一百个1
元素的列表,你可以这样做:
List<Integer> list = new ArrayList<>(Collections.nCopies(100, 1));
第二种用法是扩充一个列表。假如你想将上面的列表再添加100个2
进去,你可以这样做:
list.addAll(Collections.nCopies(100, 2));
不可变单元素集合
有时候你可能需要一个不可变的Set
,且它只包含单个指定的元素。Collections.singleton
方法就返回这样一个Set。它的一个用法是从集合中移除指定元素的所有出现,比如还是上面的列表,将刚添加进去的2全部移除
list.removeAll(Collections.singleton(2));
另一个惯用法是从一个Map中移除某些value值一样的所有元素。比如你有个Map,它记录每个人与其职业的对应关系,现在你想移除所有的律师,你可以这样做:
job.values().removeAll(Collections.singleton(LAWYER));
还有一个用法是给接收集合的方法提供单个值。
空Set, List, Map常量
emptySet
, emptyList
, emptyMap
方法返回对应类型的空集合。