容器部分归纳为三篇来写,分别从基本用法,深入研究,以及在算法中的应用。本章主要介绍深入研究,具体实现部分。
上图是集合类库的完备图,包括抽象类和遗留构件。
Collection
List
List是有序的,跟插入的顺序对应。基本的List很容易使用:大多数时候只是调用add()添加对象,使用get()一次取出一个元素,以及调用iterator()获取用于该序列的Iterator。
Set和存储顺序
Set是自己维护内部顺序的,Set在执行诸如Integer和String这样的java预定义的类型时,这些类型被设计为可以在容器内部使用。当你创建自己的类型时要意识到Set需要一种方式来维护存储顺序,而存储顺序如何维护,则是在Set的不同实现之间有变化。
- Set(Interface) 存入Set的每个元素都必须是唯一的,因为Set不保存重复的元素。加入的元素必须定义equals()方法以确保对象的唯一性。Set和Collection有完全一样的接口。Set接口不保证维护元素的次序。
- HashSet*(默认) 为快速查找而设计的Set。存入HashSet的元素必须定义HashCode()。
- TreeSet 保持次序的Set,底层为树结构。使用它可以从Set中提取有序的序列。元素必须实现Comparable接口。
- LinkedHashSet 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入顺序)。于是在使用迭代器遍历Set时,结果会按元素的插入顺序显示。元素也必须定义hashCode()方法。
从上面对比可以看出,我们必须为散列存储和树形存储创建一个hashCode()方法,为了有个良好的编程方法,我们在覆盖equals方法时也覆盖hashCode方法,只有在至于HashSet时HashCode方法才起作用。以下代码是具体实现。
class SetType{
int i;
public SetType(int n){i = n;}
@Override
public boolean equals(Object obj) {
return obj instanceof SetType && (i == ((SetType)obj).i);
}
public String toString(){return Integer.toString(i);}
}
class HashType extends SetType{
public HashType(int n){super(n);}
public int hashCode(){return i;}
}
class TreeType extends SetType
implements Comparable<TreeType>{
public TreeType(int n){super(n);}
@Override
public int compareTo(TreeType o) {
return (o.i < i? -1 :(o.i == i?0:1));
}
}
public class MockSet {
static <T> Set<T> fill(Set<T>set, Class<T> type){
try {
for(int i=0; i<10;i++){
set.add(type.getConstructor(int.class).newInstance(i));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return set;
}
static <T> void test(Set<T>set, Class<T> type){
fill(set, type);
fill(set, type);
fill(set, type);
System.out.println(set);
}
public static void main(String[] args) {
test(new HashSet<HashType>(), HashType.class);
test(new LinkedHashSet<HashType>(), HashType.class);
test(new TreeSet<TreeType>(), TreeType.class);
//以下测试没有重新遵守各契约所产生的后果
test(new HashSet<SetType>(), SetType.class);
test(new HashSet<TreeType>(), TreeType.class);
test(new LinkedHashSet<SetType>(), SetType.class);
test(new LinkedHashSet<TreeType>(), TreeType.class);
try {
test(new TreeSet<SetType>(), SetType.class);
} catch (Exception e) {
System.out.println(e.getMessage());
}
try {
test(new TreeSet<HashType>(), HashType.class);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
output:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
[9, 5, 3, 1, 8, 7, 1, 3, 5, 0, 2, 0, 7, 4, 6, 8, 3, 7, 5, 4, 2, 9, 6, 0, 9, 1, 8, 6, 2, 4]
[0, 2, 5, 1, 2, 4, 7, 4, 5, 6, 3, 1, 4, 1, 6, 3, 8, 9, 0, 9, 6, 3, 5, 0, 8, 7, 9, 7, 8, 2]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
java.lang.ClassCastException: container.SetType cannot be cast to java.lang.Comparable
java.lang.ClassCastException: container.HashType cannot be cast to java.lang.Comparable
从输出可以看出,HashSet以某种神秘的顺序保存所有元素,LinkedHashSet按照元素的插入顺序保存元素,而TreeSet按照排序顺序维护元素(按照compareTo()的实现方式)其实如果equals()对于某个特定比较产生true,那么compareTo()应该返回0;
SortedSet
SortedSet中的元素可以保证处于排序状态,这时代的它可以通过在SortedSet接口中的下列方式提供附加的功能:Comparator comparator()返回Set使用的Comparator;或者返回null,表示以自然顺序。所以SortedSet的意思是“按照对象的比较函数对元素排序”,而不是“元素的插入次序”。插入顺序可以用LinkedHashSet来保存。
队列
除了并发应用中,Queue在java中仅有两个实现是LinkedList和PriorityQueue,它们的差异在于排序行为而不是性能。
优先级队列
该列表排序也是通过实现Comparable而进行控制的。从一端插入,从另一端删除。
双向队列
双向队列就像一个队列,但可以在任何一端添加或者移除元素。在LinkedList中包含支持双向队列的方法,但在java标准库中没有任何显示的用于双向队列的接口。
Map
Map的基本思想是维护键值对关联。
下面是map具体实现,
public class Mockmap<K, V> {
private Object[][] pairs;
private int index;
public Mockmap(int length){
pairs = new Object[length][2];
}
public void put(K key, V value){
if(index >= pairs.length){
throw new ArrayIndexOutOfBoundsException();}
pairs[index++] = new Object[]{key, value};
}
@SuppressWarnings("unchecked")
public V get(K key){
for(int i =0 ;i<index; i++){
if(key.equals(pairs[i][0])){
return (V) pairs[i][1];
}
}
return null;
}
public String toString(){
StringBuilder result = new StringBuilder();
for (int i = 0; i < index; i++) {
result.append(pairs[i][0].toString());
result.append(":");
result.append(pairs[i][1].toString());
if (i<index -1) {
result.append("\n");
}
}
return result.toString();
}
public static void main(String[] args) {
Mockmap<String, String> map = new Mockmap<String, String>(3);
map.put("a", "nihao");
map.put("b", "hello");
map.put("c", "你好");
try {
map.put("d", "haha");
} catch (Exception e) {
// TODO Auto-generated catch block
System.out.println("out of length");
}
System.out.println(map);
System.out.println(map.get("a"));
}
}
output:
out of length
a:nihao
b:hello
c:你好
nihao
- HashMap*(默认) Map基于散列表实现(它取代了HashTable)。插入和查询“键值对”的开销是固定,可以通过构造器设置容量和负载因子,以调整容器的性能。
- LinkedHashMap 类似于HashMap,但是迭代遍历时,取得“键值对”的顺序是插入顺序,或者是最少使用(LRU)的次序,只比HashMap慢一点;而在迭代访问时反而更快,因为它使用链表维护内部次序。
- TreeMap 基于红黑树的实现。查看“键”或“键值对”时,它会被排序(次序由Comparable或Comparator实现)。TreeMap的特点在于,所得到的结果是经过排序的。TreeMap是唯一带有subMap()方法的Map,它可以返回一个子树。
- WeakHashMap 弱键映射,允许释放映射所指向的对象,这是为解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”可以被垃圾收集器回收。
- ConcurrentHashMap 一种线程安全的Map,它不涉及同步加锁,在并发中常用。
对Map中使用键的要求与对Set中的元素的要求一样。任何键都必须具有一个equals()方法,如果键被用于散列Map那么必须还具有恰当的hashCode()方法,如果键被用于TreeMap,必须实现Comparable。
SortedMap
使用SortedMap可以确保键处于排序状态,这使得它具有额外的功能,这些功能由SortedMap接口中的方法提供。
LinkedHashMap
为了提高速度,LinkeHashMap散列化所有的元素,但是在遍历键值对时,却又以元素插入顺序返回键值对。此外,可以在构造器中设定LinkedHashMap,使之采用基于访问的最少使用算法,于是需要定期清理元素以节省空间的程序来说,此功能使得程序很容易实现。
散列与散列码
在使用自己的类作为HashMap的键,必须重载hashCode()和equals(),默认的Object.equals()只是比较对象的地址。Object.hashCode()默认是使用对象的地址计算散列码。你必须同时重载hashCode()和equals(),HashMap使用equals()判断当前的键是否与表中存在的键相同。
- 正确的equals()方法必须满足5个条件:
自反性、对称性、传递性、一致性、对任何不是null的x,x.equals()一定返回false。 - 使用散列的目的在于:想要使用一个对象来查找另一个对象。不过使用TreeMap或自己实现的Map也可以达到此目的,但自己实现的Map不会很快,问题在于对键的查询没有按照特定顺序保存,所以只能使用最简单的线性查询,而线性查询是最慢的查询方式。散列的价值在于速度,使得查询以快速进行。散列更进一步,它将键保存在某处,以便能够很快的找到,存储一组元素最快的数据结构是数组,所以用它来保存键的信息而不是键本身,通过键对象生成一个数字将其作为数组的下标,这个数字就是散列码,可以由类覆盖的hashCode()方法生成。所以查询一个值得过程首先是计算散列码,然后使用散列码,然后使用散列码查询数组。注意,设计hashCode()时最重要的因素就是:无论何时对同一个对象调用hashCode都应该生成同样的值。
选择接口的不同实现
容器之间的区别通常归结为什么在背后“支持”它们,所使用的接口是有什么样的数据机构实现的。ArrayList和LinkedList都实现了List接口,所以无论选择哪一个基本的List都是相同的。然而ArrayList底层由数组支持;而LInkedList底层是双向链表实现,看具体想做什么操作选择效率最高的。再比如,Set可被实现为TreeSet、HashSet、LinkedHashSet。每一种都有不同的行为:HashSet最常用,查询速度最快,LinkedHashSet保持元素插入次序,TreeSet基于TreeMap,生成一个总是处于排序状态的Set。
对List的选择
ArrayList和LinkedList都实现了List接口,然而ArrayList底层由数组支持,无论列表的大小如何,这些访问都很快速;而LInkedList底层是双向链表实现,对于访问时间对于较大的列表将明显增加。如果需要执行大量的随机访问,链表不会是一个好的选择。ArrayList再插入时,必须创建空间并将它的所有引用向前移动,这会随着ArrayList的尺寸增加而产生高昂的代价。LinkedList只需连接新的元素而不必修改列表剩余元素。
应避免使用Vector,它只是存在于支持遗留代码的类库中。
将ArrayList作为默认首选,只要你需要使用额外的功能或者当程序的性能因为经常从表插入删除而变差时才选LinkedList。如果使用的是固定数量的元素,那么既可以选择使用背后有数组支撑的List,也可以选择真正的数组。
对Set的选择
HashSet的性能基本总是比TreeSet好,特别在添加查询元素时。TreeSet存在的唯一原因它可以维持元素的排序状态,所以当需要一个排好序的Set时,应该使用TreeSet。因为内部结构支持排序,并且因为迭代是我们更有可能的操作,用TreeSet迭代比用HashSet要快。注意,对于插入操作,LinkedHashSet比HashSet的代价更高,这是维护链表带来的额外代价。
对于Map的选择
所有的Map实现的插入操作都会随着Map尺寸的变大而明显变慢,查找的代价通常比插入要小的多。TreeMap通常比HashMap要慢,TreeMap是一种创建有序列表的方式。输的行为是:总是保证有序,并且不必进行特殊的排序。一旦填充了一个TreeMap就可以调用keySet()方法获取键的Set视图。HashMap本身就是被设计为可以快速查找键。当使用Map时,第一选择应是HashMap,只有在要求Map始终保持有序才需要使用TreeMap。LinkedHashMap比HashMap插入的代价更高,这是维护链表带来的额外代价。HashMap可以调整性能参数来提高其性能。