在类库中,Java语言包含一些普通数据结构的实现,这部分通常被称作Collection API,表ADT是在Collection API中实现的数据结构之一。
Collection接口
Collection API位于java.util包中,集合(collection)的概念在Collection接口中得到抽象,它存储一些类型相同的对象。以下代码显示该接口主要的部分。
public interface Collection<AnyType> extends Iterable<AnyType>{
int size();
boolean isEmpty();
void Clear();
boolean contains(AnyType x);
boolean add(AnyType x);
java.util.Iterator<AnyType> iterator();
}
1. size返回集合中的项数;
2. isEmpty当且仅当集合的大小为0时返回true;
3. 如果x在集合中,则contain返回true,该接口并不规定集合如何决定x是否属于该集合,而是要由实现该Collection接口的具体的类来决定;
4. add和remove从集合中添加和删除x,如果操作成功则返回true,如果因某个(非异常)原因失败则返回false,如删除项不在集合中,则remove可能失败,而如果特定集合不允许重复,当企图插入重复项时,add操作就会失败;
5. Collection接口扩展了Iterable接口。该接口在java.lang包里:
public interface Iterable<T> {}
实现Iterable接口的类可以拥有增强的for循环,该循环施于这些类之上以观察它们所有的项。
public static <AnyType> void print(Collection<AnyType>coll){
for(AnyType item:coll)
System.out.println(item);
}
这段代码可以用来打印任意集合中的所有项,当coll具有类型AnyType[]时相应的实现也一样,它们逐个字符都是一样的。
Iterator接口
实现Iterable接口的集合必须提供一个称为iterator的方法,该方法返回一个Iterator类型的对象。
该Iterator是一个在java.util包中定义的接口
public interface Iterator<AnyType>{
boolean hasNext();
AnyType next();
void remove();
}
Iterator接口的思路是,通过iterator方法,每个集合均可创建并返回给客户一个实现Iterator接口的对象,并将当前位置的概念在对象内部存储下来。
每次对next调用都给出集合的(尚未见到的)下一项。如第一次调用next给出第一次项,第二次调用给出第二项等等。hasNext用来告诉是否存在下一项。当编译器见到一个正在用于Iterable的对象的增强的for循环时,它用对iterator方法的那些调用代替增强的for循环以得到一个Iterator对象,然后调用next和hasNext。因此,上面的print例程有编译器重写,代码如下
public static <AnyType> void print(Collection<AnyType> coll){
Ierator<AnyType> itr = coll.iterator();
while(itr.hasNext()){
AnyType item = itr.next();
System.out.print(item);
}
}
这是通过编译器使用一个迭代器改写的Iterable类型上的增强的for循环。由于Iterator接口中的现有方法有限,因此,很难使用Iterator做简单变量Collection以外的任何工作。Iterator集合还包含一个方法,叫做remove。该方法可以删除由next最新返回的项(此后,我们不再调用remove,直到对next再一次调用以后)。Collection接口也有remove方法,但使用Iterator的remove方法可能有更多的有点。
Iterator的remove方法的有点在于,Collection的remove方法必须首先找出要被删除的项。如果直到所要删除的项的准确位置,删除它的开销可能要小得多。
直接使用Iterator(而不是通过一个增强的for循环间接使用)时,当对正在被迭代的集合进行结构上的改变(即对集合使用add
、remove、clear方法等),该迭代器就不再合法。因为迭代器可能准备给出某一项作为下一项(nextitem)而该项此后或者被删除,或者一个新的项正好插入该项的前面。这意味着,只有需要立即使用一个迭代器的时候,我们才应该获取迭代器。然而,如果迭代器调用了自己的remove方法,那么该迭代器就仍然合法。这是我们有时候更愿意使用迭代器的remove的另一个原因。
List接口、ArrayList和LinkList类
List接口在java.util包中,它继承了Collection接口,
public interface List<E> extends Collection<E> {
...
}
因此它包含Collection接口里所有的方法,还另外有一些主要方法
List ADT有两种流行的实现方式。ArrayList类提供了List ADT的一种可增长数组的实现,
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
...
}
使用ArrayList的优点是对get和set的调用花费常数时间,缺点在于新项的插入和现有项的删除代价昂贵,除非变动是在ArrayList的末端进行。
LinkList提供了List ADT的双链表实现。其优点是新项插入和现有项的删除均开销很小,这里假设变动项的位置是已知的。在表的前端进行添加和删除都是常数时间的操作,LinkList提供了方法addFirst和removeFirst、addLast和removeLast、以及getFisrt和getLast等有效地添加、删除和访问表两端的项。使用LinkList的缺点是不容易做索引,所以对get的调用时昂贵的,除非调用非常接近表的端点(如果对get的调用是对接近表后部的项进行,那么索引的进行可以从表的后部开始)。
下面我们考察对一个List进行操作的某些方法。首先,设我们通过末端添加一项来构造一个List。
public static void makeList1(List<Integer>lst,int N){
lst.clear();
for(int i=0;i<N;i++)
lst.add();
}
这里ArrayList或LinkList作为参数被传递时,makeList的运行时间都是O(N),因为add的每次调用都是在表的末端进行从而均花费常数时间(可以忽略对ArrayList偶尔进行的扩展)。如果我们通过表的前端添加一些项来构造一个List:
public static void makeList2(List<Integer>lst,int N){
lst.clear();
for(int i=0;i<N;i++)
lst.add(0,i);
}
此时LinkList的运行时间是O(N),但是ArrayList的运行时间是O(N*N),因为对ArrayList的前端进行添加是一个O(N)操作。下面的例程是计算List中的数的和:
public static int sum(List<Integer>lst){
int total=0;
for(int i=0;i<N;i++)
total += lst.get(i);
return total;
}
这里ArrayList的运行时间是O(N),但对于LInkLIst来说,其运行时间是O(N*N),因为在LinkList中,对get方法的调用为O(N)操作。当使用一个增强的for循环时,则对于任的List的运行时间都是O(N),因为迭代器能有效地从一项到下一项推进。
对搜索而言,ArrayList和LinkList都是低效的,对于Collection的contains和remove方法(它们都是AnyType)的调用均花费线性时间。
在ArrayList中有一个容量的概念,用来表示数组的大小。需要时,ArrayList将自动增加其容量保证它至少具有表的大小。如果该大小的早起估计存在,则ensureCapacity可以设置容量为一个足够大的量避免数组容量以后的扩展。还有trimTosize可以在所有的ArrayList添加操作完成以后使用以避免浪费空间。