Java核心技术 卷1-总结-11
- Java 集合框架
- 将集合的接口与实现分离
- Collection接口
- 迭代器
- 泛型实用方法
- 集合框架中的接口
Java 集合框架
将集合的接口与实现分离
Java集合类库将接口(interface)与实现(implementation)分离。
例如队列(queue):队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照“先进先出”的规则检索对象时就可以使用队列。队列接口的最简形式类似下面这样:
public interface Queue<E> {// a simplifiedform of the interface in the standard library
void add(E element);
E remove();
int size();
}
这个接口并没有说明队列是如何实现的。队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表。
每一个实现都可以通过一个实现了Queue接口的类表示。
public class CircularArrayQueue<E> implements Queue<E> { // not an actual library class
private int head;
private int tail;
CircularArrayQueue(int capacity) {...}
public void add(E element) {...}
public E remove() {...}
public int size() {...}
private E[] elements;
}
public class LinkedListQueue<E> implements Queue<E> { // not an actual library class
private Link head;
private Link tail;
LinkedListQueue() {...}
public void add(E element) {...}
public E remove() {...}
public int size() {...}
CircularArrayQueue和LinkedListQueue类。只是以这些类作为示例,实际上Java中并没有这两个类。如果需要一个循环数组队列,就可以使用ArrayDeque类。如果需要一个链表队列,就直接使用LinkedList 类,这个类实现了Queue接口。
当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了哪种实现。因此,只有在构建集合对象时,使用具体的类才有意义。可以使用接口类型存放集合的引用。
Queue<Customer> expressLane = new CircularArrayQueue<>(100);
expressLane.add(new Customer("Harry"));
利用这种方式,一旦改变了想法,可以轻松地使用另外一种不同的实现。只需要对程序调用构造器的地方=做出修改,如果觉得LinkedListQueue是个更好的选择,就可以将代码修改为:
Queue<Customer> expressLane = new LinkedListQueue<);
expressLane.add(new Customer("Harry"));
循环数组要比链表更高效,因此多数人优先选择循环数组。然而,通常这样做也需要付出一定的代价。循环数组是一个有界集合,即容量有限。如果程序中要收集的对象数量没有上限,就最好使用链表来实现。
Collection接口
在Java类库中,集合类的基本接口是Collection接口。这个接口有两个基本方法:
public interface Collection<E> {
boolean add(E element);
Iterator<E> iterator();
}
add方法用于向集合中添加元素。如果添加元素确实改变了集合就返回true,如果集合没有发生变化就返回false。例如,如果试图向集中添加一个对象,而这个对象在集中已经存在,这个添加请求就没有生效,因为集合中不允许有重复的对象。
iterator方法用于返回一个实现了Iterator接口的对象。可以使用这个迭代器对象依次访问集合中的元素。
迭代器
Iterator接口包含4个方法:
public interface Iterator<E> {
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<?super E> action);
}
通过反复调用 next
方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next
方法将抛出一个NoSuchElementException
。因此,需要在调用 next
之前调用 hasNext
方法。如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true 时反复地调用next方法。 例如:
Collection<String> c = ...;
Iterator<String>iter = c.iterator();
while (iter.hasNext)) {
String element = iter.next();
do something with element
}
用“for each”循环可以更加简练地表示同样的循环操作:
for (String element : c) {
do something with element
}
编译器将"for each"循环翻译为带有迭代器的循环。"for each"循环可以与任何实现了Iterable
接口的对象一起工作,这个接口只包含一个抽象方法:
public interface Iterable<E> {
Iterator<E> iterator();
}
Collection
接口扩展了Iterable
接口。因此,对于标准类库中的任何集合都可以使用"for each”循环。
元素被访问的顺序取决于集合类型。如果对ArrayList进行迭代,迭代器将从索引0开始,每迭代一次,索引值加1。然而,如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现。 虽然可以确定在迭代过程中能够遍历到集合中的所有元素,但却无法预知元素被访问的次序。
Iterator接口的remove方法将会删除上次调用next方法时返回的元素。 在大多数情况下,在决定删除某个元素之前应该先看一下这个元素是很具有实际意义的。然而,如果想要删除指定位置上的元素,仍然需要越过这个元素。例如,下面是如何删除字符串集合中第一个元素的方法:
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
泛型实用方法
由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。例如,下面是一个检测任意集合是否包含指定元素的泛型方法:
public static <E> boolean contains(Collection<E> c, Object obj) {
for (E element : c) {
if (element.equals(obj)) {
return true;
}
}
return false;
}
这些实用方法中的某些方法非常有用,这样,类库的使用者就不必自己重新构建这些方法了。contains就是这样一个实用方法。
Collection接口声明了很多有用的方法,所有的实现类都必须提供这些方法, 下面列举了其中的一部分:
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)
如果实现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 element : this) { // calls iterator()
if (element.equals(obj)) {
return = true;
}
}
return false;
}
如果子类有更加有效的方式实现contains
方法,也可以由子类提供,没有什么限制。
另外,还有一个很有用的方法:
default boolean removeIf(Predicate<? super E> filter)
这个方法用于删除满足某个条件的元素。
集合框架中的接口
集合有两个基本接口:Collection
和Map
。可以用以下方法在集合中插人元素:
boolean add(E element)
由于映射包含键/值对,所以要用put方法来插入:
V put(K key, V value)
要从集合读取元素,可以用迭代器访问元素。从映射中读取值则要使用get
方法:
V get(K key)
List是一个有序集合(ordered collection)。元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问。 后一种方法称为随机访问(random access),因为这样可以按任意顺序访问元素。与之不同,使用迭代器访问时,必须顺序地访问元素。
List
接口定义了多个用于随机访问的方法:
void add(int index, E element)
void remove(int index)
E get(int index)
E set(int index, E element)
Set接口等同于Collection接口,不过其方法的行为有更严谨的定义。集(set)的add方法不允许增加重复的元素。 要适当地定义集的 equals 方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序。hashCode方法的定义要保证包含相同元素的两个集会得到相同的散列码。