集合接口
Java的早期版本只发布了一些简单的集合类接口:Vector, Stack, Hashtable, BitSet和Enumerate接口,提供几种元素的访问方法。这是一个聪明的决定——编写合适的容器类显然需要很长时间的思考。
在Java 1.2发布时,设计者感觉发布稍微成熟的数据接口的时间到了。他们面临一些相反的选择,想要设计一种简单易学的接口。他们想要C++中STL类的泛型算法,但是又不想要STL中的复杂度。在这一章,我们考虑Java的一些设计,并解释为什么要这样设计。
将接口和实现分离
作为现代数据结构库的通用做法,Java集合库将接口和实现分离。我们使用队列数据接口解释。
队列接口指定你可以在队列的结尾添加元素,在开头删除元素,你也可以知道当前队列中有多少数据。当你的数据需要“先进先出”逻辑时,你需要使用队列。
最简单的队列接口可以写为
interface Queue<E> // 标准库中队列接口的简化实现
{
void add(E element);
E remove();
int size();
}
这个接口没有告诉你任何关于队列实现的信息,目前主流实现包括两种,循环数列和链表。他们的实现可以分别表示为
class CircularArrayQueue<E> implements Queue<E> // 不是标准库的实现
{
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> // 不是标准库实现
{
LinkedListQueue() {...}
public void add(E element) {...}
public E remove() {...}
public int size() {...}
private Link head;
private Link tail;
}
Java标准库中没有上面两个类的实现,如果你想使用循环数组实现的队列,使用ArrayDeque,如果你要使用链表的实现方法,直接使用LinkedList。
当你使用队列时,如果队列已经实现了,那你不需要考虑他的实现。只有在创建队列时,你才需要考虑具体实现。通常,你使用接口类型保存集合类引用。
Queue<Customer> expressLane = new CircularArrayQueue<Customer>(100);
expressLane.add(new Customer("Harry"));
这样,你就可以随时改变你的实现,而不用大规模修改代码。接口并没有说明队列实现的效率,通常,循环数组队列的效率高一点,但是,他的大小很难改变。如果你无法决定队列中元素个数的上限,你最好使用链表队列。
当你读API文档时,你会发现还有一些类以Abstract开头,比如AbstractQueue。这些类指向库实现。如果你要实现自己的类,你会发现集成自AbstractQueue类更简单,而不是继承自Queue接口。
集合接口和迭代器接口
Java中集合类的基本接口是Collection 接口。这个接口有两个基本方法。
public interface Collection<E>
{
boolean add(E element);
Iterator<E> iterator();
}
还有一些其他的方法,我们稍后讨论。
add方法想集合中添加一个元素。如果该操作真的改变了集合,那么返回true,否则返回false。比如,如果你向Set类中添加一个元素,而这个元素已经存在,那么就会返回false。
iterator方法返回该集合的迭代器。你可以使用这个迭代器一个一个地访问元素。
迭代器
迭代器(Iterator)接口有三个方法。
public interface Iterator<E>
{
E next();
boolean hasNext();
void remove();
}
重复调用next函数,你可以一个接一个地访问集合类中的元素。但是,如果迭代器中没有元素,那么next方法返回NoSuchElementException。所以,在调用next方法之前,你需要先调用hasNext,如果集合中还有元素,他会返回true。如果你要访问集合中所有元素,你需要在hasNext返回true时,调用next。
Collection<String> c = ...;
Iterator<String> ite = c.iterator();
while(ite.hasNext()){
String element = iter.next();
dosomethingelse;
}
Java SE5.0 之后,你可以使用for循环逐个读取数据。
for(String element: c){
dosomethingwithelement;
}
编译器会这段语句转换为迭代器的循环。
上面的for循环可以用于所有实现了Iterator接口的类,接口只有一个方法
public interface Iterator<E>
{
Iterator<E> iterator();
}
Collection接口继承自Iterator接口。所以,你可以对所有Collection类使用for循环。
遍历的顺序取决于集合类型。如果你迭代ArrayList,那么会从下标从0开始到最后一个元素。但是,如果你迭代HashSet,那么你没有办法知道迭代顺序,但你可以确定,你会遍历到每个元素。遍历顺序在大多数情况下都不是问题。
移除元素
Iterator接口的remove方法会移除上一次调用next得到的元素。在多数情况下,这是合理的,在你删除元素之前,你需要查看一下它。比如,下面的代码移除第一个元素。
Iterator<String> ite = c.iterator();
it.next();
it.remove();
如果你在不允许的地方调用remove,那么就会抛出IllegalStateException。
如果你想移除两个相邻的元素,那么下面的代码是错误的。
it.remove();
it.remove(); //错误
你必须使用下面的代码。
it.remove();
it.next();
it.remove();
泛型工具方法
由于Collection和Iterator接口都是泛型的,因此你可以写一些这两个接口的工具类。比如,下面的方法可以判断一个集合中是否包含一个给定的元素。
public static <E> boolean contains(Collection<E> c,Object obj){
for(E element: c){
if(element.equals(obj))
return true;
}
return false;
}
Java设计者认为有些方法非常常用,因此应该包含在标准库中,在标准库中,也包含contains函数。
事实上,还有一些其他的接口。
int size();
boolean isEmpty();
boolean contains(Object obj);
boolean containsAll(Collection<?> c);
boolean equals(Object other);
boolean addAll(Collection<? extends E> from);
boolean remove(Object obj);
boolean removeAll(Collection<?> c);
void clear();
boolean retainAll(Collection<?> c);
Object[] toArray();
<T> T[] toArray(T[] arrayToFill);
多数方法的功能跟名字一样。具体用法可查看API文档。