13.1 集合接口
13.1.1 将集合的接口与实现分离
与现代的数据结构类库的常见情况一样,Java集合类库也将接口(interface)与实现(impementation)分离。首先,看一下人们熟悉的数据结构——队列(queue)是如何分离的。
队列接口指出可以在队列的尾部添加元素。在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照“先进先出”的规则检查对象时就应该使用队列。
队列通常有两种实现形式:一种是使用循环数组,另一种是使用链表。
13.1.2Java类库中的集合接口和迭代器
在Java类库中,集合类的基本接口是Collection接口。这个接口有两个基本方法:
● boolean add(E element)
● Iterator<E> iterator();
add方法用于向集合添加元素。如果添加元素确实改变了集合就返回true,否则就返回false。
iterator方法用于返回一个实现了Iterator接口的对象。可以使用这个迭代器对象依次访问集合中的元素。
1.迭代器
Iterator接口包含三个方法:
boolean hasNext();
E next();
void remove();
通过反复调用next方法,可以逐个访问集合中的每个元素。但是如果到达了集合的末尾,next方法将抛出一个NoSuchElementException。因此,需要在调用next方法之前调用哈斯Next方法。如果迭代器对象还有多个供访问的元素,这个方法就返回true。如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext方法返回true的时候反复调用next方法。在Java SE 5.0以后迭代器可以使用for each 循环进行代替。编译器将for each 循环翻译为带有迭代器的循环。
Collection接口扩展了Iterator接口。因此,对于标准类库中的任何集合都可以使用for each 循环。
元素访问的顺序取决于集合类型。如果对于ArrayList进行迭代,迭代器将从索引0开始,每一次迭代,索引值加1。然而,如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现。虽然可以确定在迭代过程中能够遍历到集合中的所有元素,但却无法预知元素被访问的次序。这对于计算总和或同级复核某个条件的元素个数与顺序无关的操作来说,并不是什么问题。
注释:编程老手会注意到:Iterator接口的next和hasNext方法与Enumeration接口的nextElement和hasMoreElements方法的作用一样。Java标准类库的设计者可以选择使用Enumeration接口。但是,他们不喜欢这个接口累赘的方法名,于是引入了具有较短方法名的新接口。
注释:这里还有一个有用的类推。可以将Iterator.next与InputStream.rea看作为等效的。从数据流中读取一个字节,就会自动地“消耗掉“这个字节。下一次调用read将会消耗并返回输入的下一个字节。用同样的方式,反复地调用next方法就可以读取集合中所有元素。
2.删除元素
Iterator接口的remove方法将会删除上次调用next方法时返回的元素。在大多数情况下,在决定删除某个元素之前应该看一下这个元素很具有实际意义的。然而,如果想要删除指定位置上的元素,仍要越过这个元素。例如,下面是如何删除字符串集合中啊第一个元素的方法:
Iterator<String> it =list.iterator();
it.next();
it.remove();
3.泛型实用方法
由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。例如,下面是一个检测任意集合是否包含指定元素的泛型方法:
public static <E> boolean contains(Collection<E> c,Object obj) {
for (E element : c)
if (element.equals(obj))
return true;
return false;
}
Collection接口中常用方法:
方法摘要 |
||
boolean |
add(E e) |
|
boolean |
addAll(Collection<? extends E> c) |
|
void |
clear() |
|
boolean |
contains(Object o) |
|
boolean |
containsAll(Collection<?> c) |
|
boolean |
equals(Object o) |
|
int |
hashCode() |
|
boolean |
isEmpty() |
|
Iterator<E> |
iterator() |
|
boolean |
remove(Object o) |
|
boolean |
removeAll(Collection<?> c) |
|
boolean |
retainAll(Collection<?> c) |
|
int |
size() |
|
Object[] |
toArray() |
|
|
toArray(T[] a) |
如果实现Collection接口的每一个类都要提供如此多的例行方法将是一件很烦人的事情。为了能够让实现者更容易地实现这个接口,Java类库还提供了一个类AbstractCollection,它将基础方法size和Iterator抽象化了,但是在此提供了例行方法。此时,一个具体的集合类可以扩展此抽象类了。现在要由具体的集合类提供Iterator方法,而contains方法已由AbstractCollection超类提供了。然而,如果子类有更加有效的方式实现contains方法,也可以由子类提供。
Collection在使用时注意:
13.2 具体的集合
下表中,除了以Map结尾的类之外,其它类都实现了Collection接口。而以Map结尾的类实现了Map接口。
集合类型 |
描述 |
ArrayList |
一种可以动态增长和缩减的索引序列 |
LinkedList |
一种可以在任何位置进行高效地插入和删除操作的有序序列 |
ArrayDeque |
一种用循环数组实现的双端队列 |
HashSet |
一种没有重复元素的无序集合 |
TreeSet |
一种有序集 |
EnumSet |
一种包含枚举类型值的集 |
LinkedHashSet |
一种可以记住元素插入次序的集 |
PriorityQueue |
一种允许高效删除最小元素的集合 |
HashMap |
一种存储键/值关联的数据结构 |
TreeMap |
一种键值有序排列的映射表 |
EnumMap |
一种键值属于枚举类型的映射表 |
LinkedHashMap |
一种可以记住键/值项添加次序的映射表 |
WeakHashMap |
一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap |
一种用==而不是equals比较键值的映射表 |
13.2.1 链表
数组和数组列表都有一个重大的缺陷。这就是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。在数组中间的位置插入一个元素也是如此。
尽管在连续的存储位置上存放对虾加工引用,但链表却将每个对象存放在独立的结点中。每个结点还存放着序列中下一个结点的引用。在Java程序设计语言中,所有链表实际上都是双向链接的——即每个结点还存放着指向前驱结点的引用。
从链表中删除一个元素是一个很轻松的操作,即需要对被删除元素附近的结点更新一下即可。
但是,链表与泛型集合之间有一个重要的区别。链表是一个有序集合,每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表的中间。由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责。
只有对自然有序的集合使用迭代器添加元素才有实际意义。例如,Set类型,其中的元素完全无序。因此,在Iterator接口中就没有add方法。相反地,集合类库提供了子接口ListIterator,其中包含add方法:
public interface ListIterator<E> extends Iterator<E> {
void add(E e);
… …}
与Collection.add不同,这个方法不返回Boolean类型的值,它假定添加操作总会改变链表。此外,ListIterator接口有两个方法,可以用来反向遍历链表。
boolean hasPrevious();
E previous();
当用一个刚刚由Iterator方法返回,并且指向链表表头的迭代器调用add操作时,新添加的元素将编程列表的新表头。当迭代器越过链表的最后一个元素时(即hasNext返回false),添加的元素将编程列表的新表尾。add方法依赖于迭代器的位置,而remove方法依赖于迭代器的状态。
最后需要说明,set方法用一个新元素去取代调用next或previous方法返回的上一个元素。
可以想象,如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合资深的方法修改了,就会抛出一个Concurrent ModificationException。
next方法的实现:
public E next() {
checkForComodification();//检查集合是否改变
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
列表迭代器接口还有一个方法,可以告之当前位置的索引。实际上,从概念上讲,由于Java迭代器指向两个元素之间的位置,所以可以同时产生两个索引:nextIndex方法返回下一次调用next方法时返回元素的整数索引;previousIndex方法返回下一次调用previous方法时返回元素的整数作引。当然,这个索引只比nextIndex返回的索引值小1。这两个方法的效率相当高,这是因为迭代器保持着当前位置的计数值。最后需要说一下,如果有一个整数索引n,list.listIterator(n)将返回一个迭代器,这个迭代器指向索引为n的元素前面的位置。也就是说,调用next与调用list.get(n)会产生同一个元素,只是获得这个迭代器的效率比较低。
如果链表中只有很少的几个元素,就完全没有必要为get方法和set方法开销而烦恼。但是,为什么有限使用链表呢?使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素所付出的代价。如果列表中只有少数几个元素,就完全可以使用ArrayList。
链表不使用与随机访问:
for(intI = 0 ; I < list.size(); i++){
list.ge(i);
}
每次查找一个元素都要从列表的头部重新开始搜索。LinkedList对象根本不做任何缓存位置信息的操作。
13.2.2 数组列表
List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。有两种访问元素的协议:一种是使用迭代器,另一种是用get和set方法随机地访问每个元素。后者不适用于链表,但对数组却很有用。集合类库提供了一种大家属性的ArrayList类,这个类也实现了List接口。ArrayList封装了一个动态再分配的对象数组。
注释:对于一个经验丰富的Java程序员来说,在需要动态数组时,可能会使用Vector类。为什么要用ArrayList取代Vector呢?原因很简单:Vector类的所有方法都是同步的。可以由两个线程安全地访问同一个Vector对象。但是,如果由一个线程访问Vector,代码要在同步上耗费大量的时间。这种情况还是很常见的。而ArrayList方法不是同步的,因此,检疫在不需要同步时使用ArrayList,而不要使用Vector。
13.2.3 散列集
链表和数组可以按照人们的意愿排列元素的次序。但是,如果想要查看某个指定的元素,却又忘了它的位置,就需要访问所有元素,知道找到它为止。如果集合中包含的元素很多,将会消耗很多时间。如果不在意元素的顺序,可以有几种能够快速查找元素的数据结构。其缺点是无法控制元素出现的次序。他们将按照有利于操作目的的原则组织数据。
13.2.4 树集
TreeSet类与散列集十分相似,不过,它比散列集有所改进。树集是一个有序集合。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。将一个元素添加到树中要比添加到散列表中慢,但是,与将元素添加到数组或链表的正确位置上相比还是要块很多的。
13.2.6 队列与双端队列
队列可以让人么有效地在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素不支持在队列中间添加元素。
13.2.8 映射表
映射表用来存放键/值对。
如果提供了键,就能够查找到值。Java类库为映射表提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。
键必须是唯一的。不能对同一个键存放两个值。如果对同一个键两次调用put方法,第二个值就会取代第一个值。实际上put方法将返回用这个键参数存储的上一个值。remove方法用于从映射表中删除给定键对应的元素。size方法用于返回映射表中的元素数。
集合与数组之间的转换
数组à集合:Arrays.asList
集合à数组:toArray