铁文整理
13.1 集合接口
Java最初版本只为最常用的数据结构提供了很少的一组类:Vector、Stack、Hashtable、BitSet与Enumeration接口,其中的Enumeration接口提供了一种用于访问任意容器中各个元素的抽象机制。这是一种很明智的选择,但要想建立一个全面的集合类库还需要大量的时间和高超的技能。
随着Java SE 1.2的问世,设计人员感到是推出—组功能完善的数据结构的时机了。面对一大堆相互矛盾的设计策略,他们希望让类库规模小且易于学习而不希望像C++的标准模版库那样复杂,但却又希望能够得到STL率先推出的“泛型算法”所具有的优点。他们希望将传统的类融入新的框架中。与所有的集合类库设计者一样,他们必须做出一些艰难的选择。于是,在整个设计过程中,他们做出了一些独具特色的设计决定。本节将介绍Java集合框架的基本设计,展示使用它们的方法,并解释一些颇具争议的特性背后的考虑。
13.1.1 将集合的接口与实现分离
与现代的数据结构类库的常见情况一样,Java集合类库也将接口与实现分离。首先,看一下人们熟悉的数据结构——队列(queue)是如何分离的。
队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以査找队列中元素的个数。当需要收集对象,并按照“先进先出”的规则检索对象时就应该使用队列(见图13-1)。
一个队列接口的最小形式可能类似下面这样:
interface Queue<E> // a simplified form of the interface in the standard library
{
void add(E dement);
E remove();
int size();
}
这个接口并没有说明队列是如何实现的。队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表(见图13-2)。
注释:从Java SE 5.0开始,集合类是带有类型参数的泛型类。有关泛型类的更多信息请参看第12章。
每一个实现都可以通过一个实现了Queue接口的类表示。
class CircularArrayQueue<E> implements Queue<E> // not an actual library class
{
CircularArrayQueue(int capacity) { }
public void add(E element) { }
public E remove() { }
public int size() { }
private E[] elements;
private int head;
private int tail;
}
class LinkedListQueue<E> implements Queue<E> // not an actual library class
{
LinkedListQueue() { }
public void add(E element) { }
public E remove() { }
public int size() { }
private Link head;
private Link tail;
}
注释:实际上,Java类库没有名为CircularArrayQueue和LinkedListQueue的类。这里,只是以这些类作为示例,解释—下集合接口与实现在概念上的不同。如果需要一个循环数组队列,就可以使用Java SE 6中引入的ArrayDeque类。如果需要一个链表队列,就直接使用LinkedList类,这个类实现了Queue接口。
当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了哪种实现。因此,只有在构建集合对象时,使用具体的类才有意义。可以使用接口类型存放集合的引用。
Queue<Customer> expressLane = new CircularArrayQueue<Customer>(100);
expressLane.add(new Customer("Harry"));
利用这种方式,一旦改变才想法,可以轻松地使用另外—种不同的实现。只需要对程序的一个地方做出修改,即调用构造器的地方。如果最终觉得LinkedListQueue是个更好的选择,就将代码修改为:
Queue<Customer> expressLane = new LinkedListQueue<Customer>();
expressLane.add(new Customer("Harry"));
为什么选择这种实现,而不选择那种实现呢?接口本身并不能说明哪种实现的效率究竟如何。循环数组要比链表更高效,因此多数人优先选择循环数组。然而,通常这样做也需要付出一定的代价。
循环数组是一个有界集合,即容量有限。如果程序中要收集的对象数量没有上限,就最好使用链表来实现。
在研究API文档时,会发现另外一组名字以Abstract开头的类,例如,AbstractQueue。这些类是为类库实现者而设计的。如果想要实现自己的队列类(也许不太可能),会发现扩展AbstractQueue类要比实现Queue接口中的所有方法轻松得多。
13.1.2 Java类库中的集合接口和迭代器接口
在Java中,集合类的基本接口是Collection接口。
这个接口有两个基本方法:
public interface Collection<E> {
boolean add(E element);
Iterator<E> iterator();
}
除了这两个方法之外,还有几个方法,将在稍后介绍。
add方法用于向集合中添加元素。如果添加元素确实改变了集合就返回true,如果集合没有发生变化就返回false。例如,如果试图向集中添加一个对象,而这个对象在集合中已经存在,这个添加请求就没有实效,因为集中不允许有重复的对象。
iterator方法用于返回一个实现了Iterator接口的对象。可以使用这个迭代器对象依次访问集合中的元素。
1. 迭代器
Iterator接口包含3个方法:
public interface Iterator<E> {
E next();
boolean hasNext();
void remove();
}
通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next方法将抛出一个NoSuchElementException。因此,需要在调用next之前调用hasNext方法。如果迭代器对象还有多个供访问的元素,这个方法就返回true。如果想要査看集合中的所有元素,就请求一个迭代器,并在hasNext返回true时反复地调用next方法。例如:
Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while (iter.hasNext()) {
String element = iter.next();
doSomethingWith(element);
}
从Java SE 5.0起,这个循环可以采用一种更优雅的缩写方式。用“for each”循环可以更加简练地表示同样的循环操作:
for (String element : c) {
doSomethingWith(element);
}
编译器简单地将“for each”循环翻译为带有迭代器的循环。
“for each”循环可以与任何实现了Iterable接口的对象一起工作,这个接口只包含一个方法:
public interface Iterable<E> {
Iterator<E> iterator();
}
Collection接口扩展了Iterable接口。因此,对于标准类库中的任何集合都可以使用“for each”循环。
元素被访问的顺序取决于集合类型。如果对ArrayList进行迭代,迭代器将从索引0开始,每迭代一次,索引值加1。然而,如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现。虽然可以确定在迭代过程中能够遍历到集合中的所有元素,但却无法预知元素被访问的次序。这对于计算总和或统计符合某个条件的元素个数这类与顺序无关的操作来说.并不是什么问题。
注释:编程老手会注意到:Iterator接口的next和hasNext方法与Enumeration接口的nextElements和hasMoreElements方法的作用一样。Java集合类库的设计者可以选择使用Enumeration接口。但是,他们不喜欢这个接口累赘的方法名,于是引入了具有较短方法名的新接口。
Java集合类库中的迭代器与其他类库中的迭代器在概念上有着重要的区别。在传统的集合类库中,例如,C++的标准模版库,迭代器是根据数组索引建模的。如果给定这样一个迭代器,就可以査看指定位置上的元素,就像知道数组索引就可以査看数组元素a[i]一样。不需要查找元素,就可以将迭代器向前移动一个位置。这与不需要执行査找操作就可以通过i++将数组索引向前移动一样。但是,Java迭代器并不是这样操作的。查找操作与位置变更是紧密相连的。査找一个元素的唯一方法是调用next,而在执行査找操作的同时,迭代器的位置随之向前移动。
因此,应该将Java迭代器认为是位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用(见图13-3)。
注释:这里还有一个有用的类推。可以将Iterator.next与InputStream.read看作为等效的。从数据流中读取一个字节,就会自动地“消耗掉”这个字节。下一次调用read将会消耗并返回输入的下一个字节。用同样的方式,反复地调用next就可以读取集合中所有元素。
2. 删除元素
Iterator接口的remove方法将会删除上次调用此以方法时返回的元素,在大多数情况下,在决定刪除某个元素之前应该先看一下这个元素是很具有实际意义的。然而,如果想要删除指定位置上的元素,仍然需要越过这个元素。下面是如何删除字符串集合中第一个元素的方法:
Iterator<String> it = c.iterator();
it.next(); // skip over the first element
it.remove(); // now remove it
更重要的是,对next方法和remove方法的调用具有互相依赖性,如果调用remove之前没有调用next将是不合法的。如果这样做,将会抛出一个IllegalStateException异常。
如果想删除两个相邻的元素,不能直接地这样调用:
it.remove();
it.remove(); // ERROR
相反地,必须先调用next越过将要刪除的元素。
it.remove();
it.next();
it.remove(); // OK
3. 泛型实用方法
由于Collection都是泛型接口,可以编写操作任何集合类型的实用方法,例如,下面是一个检测任意集合是否包含指定元素的泛型方法:
public static <E> boolean contains(Collection<E> c, Object obj) {
for (E elenent : c)
if (elenent.equals(obj))
return true;
return false;
}
Java类库的设计者认为:这些实用方法中的某些方法非常有用,应该将它们提供给用户使用。这样,类库的使用者就不必自己重新构建这些方法了。contains就是这样一个实用方法。
事实上,Collection接口声明了很多有用的方法,所有的实现类都必须提供这些方法。下面列举了其中的一部分:……
在这些方法中,有许多方法的功能非常明确,不需要过多的解释。在本节尾部的API注释中可以找到有关它们的完整文档说明。
当然,如果实现Collection接口的每一个类都要提供如此多的例行方法将是一件很烦人的事情。为了能够让实现者更容易地实现这个接口,Java类库提供了一个类AbstractCollection。它将基础方法size和iterator抽象化了,但是在此提供了例行方法。例如:
public abstract class AbstractCollection<E> implements Collection<E> {
public abstract Iterator<E> iterator();
public boolean contains(Object obj) {
for (E elemeit : c)
// calls iterator()
if (element.equals(ob))
return true;
return false;
}
}
此时,一个具体的集合类可以扩展AbstractCollection类了,现在要由具体的集合类提供iterator方法,而contains方法已由AbstractCollection超类提供了。然而,如果子类有更加有效的方式实现contains方法,也可以由子类提供,就这点而言,没有什么限制。
对于类框架来说,这是一个很好的设计。集合类的用户可以使用泛型接口中一组更加丰富的方法,而实际的数据结构实现者并没有需要实现所有例行方法的负担。
API:java.util.Collection<E> 1.2
-
Iterator<E> iterator():返回一个用于访问集合中每个元素的迭代器。
-
int size():返回当前存储在集合中的元素个数。
-
boolean isEmpty():如果集合中没有元素,返回true。
-
boolean contains(Object obj):如果集合中包含了一个与obj相等的对象,返回true。
-
booTean containsAll(Collection<?> other):如果这个集合包含other集合中的所有元素,返回true。
-
boolean add(Object element):将一个元素添加到集合中。如果由于这个调用改变了集合,返回true。
-
boolean addAll(Collection<? extends E> other):将other集合中的所有元素添加到这个集合。如果由于这个调用改变了集合,返回true。
-
boolean remove(Object obj):从这个集合中删除等于obj的对象。如果有匹配的对象被删除,返回true。
-
boolean removeAll(Collectlon<?> other):从这个集合中删除other集合中存在的所有元素。如果由于这个调用改变了集合,返回true。
-
void clear():从这个集合中删除所有的元素。
-
boolean retainAll(Collection<?> other):从这个集合中删除所有与other集合中的元素不同的元素。如果由于这个调用改变了集合,返回true。
-
Object[] toArray():返回这个集合的对象数组。
-
<T> T[] toArray(T[] arrayToFill):返回这个集合的对象数组。如果arrayToFill足够大,就将集合中的元素填入这个数组中,剩余空间填补null;否则,分配一个新数组,其成员类型与arrayToFill的成员类型相同,其长度等于集合的大小,并添入集合元素。
API:java.util.Iterator<E> 1.2
-
boolean hasNext():如果存在可坊问的元素,返回加true。
-
E next():返回将要访问的下一个对象。如果已经到达了集合的尾部,将抛出一个NoSuchElementException。
-
void remove():删除上次访问的对象。这个方法必须紧跟在访问一个元素之后执行。如果上次访问之后,集合已经发生了变化,这个方法将抛出一个IllegalStateException。