本章概要
- 迭代器 Iterators
- ListIterator
- 链表 LinkedList
- 堆栈 Stack
迭代器Iterators
在任何集合中,都必须有某种方式可以插入元素并再次获取它们。毕竟,保存事物是集合最基本的工作。对于 List , add()
是插入元素的一种方式, get()
是获取元素的一种方式。
如果从更高层次的角度考虑,会发现这里有个缺点:要使用集合,必须对集合的确切类型编程。这一开始可能看起来不是很糟糕,但是考虑下面的情况:如果原本是对 List 编码的,但是后来发现如果能够将相同的代码应用于 Set 会更方便,此时应该怎么做?或者假设想从一开始就编写一段通用代码,它不知道或不关心它正在使用什么类型的集合,因此它可以用于不同类型的集合,那么如何才能不重写代码就可以应用于不同类型的集合?
迭代器(也是一种设计模式)的概念实现了这种抽象。迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外,迭代器通常被称为_轻量级对象_(lightweight object):创建它的代价小。因此,经常可以看到一些对迭代器有些奇怪的约束。例如,Java 的 Iterator 只能单向移动。这个 Iterator 只能用来:
- 使用
iterator()
方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素。 - 使用
next()
方法获得序列中的下一个元素。 - 使用
hasNext()
方法检查序列中是否还有元素。 - 使用
remove()
方法将迭代器最近返回的那个元素删除。
为了观察它的工作方式,这里再次使用类型信息章节中的 Pet 工具:
SimpleIteration.java
import java.util.Iterator;
import java.util.List;
public class SimpleIteration {
public static void main(String[] args) {
List<Pet> pets = new PetCreator().list(12);
Iterator<Pet> it = pets.iterator();
while (it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
// A simpler approach, when possible:
for (Pet p : pets) {
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
// An Iterator can also remove elements:
it = pets.iterator();
for (int i = 0; i < 6; i++) {
it.next();
it.remove();
}
System.out.println(pets);
}
}
其它相关类:
Cat.java
public class Cat extends Pet {
public Cat(String name) { super(name); }
public Cat() { super(); }
}
Creator.java
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public abstract class Creator implements Supplier<Pet> {
private Random rand = new Random(47);
// The different types of Pet to create:
public abstract List<Class<? extends Pet>> types();
@Override
public Pet get() { // Create one random Pet
int n = rand.nextInt(types().size());
try {
return types().get(n)
.getConstructor().newInstance();
} catch (InstantiationException |
NoSuchMethodException |
InvocationTargetException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public Stream<Pet> stream() {
return Stream.generate(this);
}
public Pet[] array(int size) {
return stream().limit(size).toArray(Pet[]::new);
}
public List<Pet> list(int size) {
return stream().limit(size).collect(Collectors.toCollection(ArrayList::new));
}
}
Cymric.java
public class Cymric extends Manx {
public Cymric(String name) { super(name); }
public Cymric() { super(); }
}
Dog.java
public class Dog extends Pet {
public Dog(String name) { super(name); }
public Dog() { super(); }
}
EgyptianMau.java
public class EgyptianMau extends Cat {
public EgyptianMau(String name) { super(name); }
public EgyptianMau() { super(); }
}
Hamster.java
public class Hamster extends Rodent {
public Hamster(String name) { super(name); }
public Hamster() { super(); }
}
Individual.java
import java.util.Objects;
public class Individual implements Comparable<Individual> {
private static long counter = 0;
private final long id = counter++;
private String name;
public Individual(String name) {
this.name = name;
}
// 'name' is optional:
public Individual() {
}
@Override
public String toString() {
return getClass().getSimpleName() +
(name == null ? "" : " " + name);
}
public long id() {
return id;
}
@Override
public boolean equals(Object o) {
return o instanceof Individual &&
Objects.equals(id, ((Individual) o).id);
}
@Override
public int hashCode() {
return Objects.hash(name, id);
}
@Override
public int compareTo(Individual arg) {
// Compare by class name first:
String first = getClass().getSimpleName();
String argFirst = arg.getClass().getSimpleName();
int firstCompare = first.compareTo(argFirst);
if (firstCompare != 0) {
return firstCompare;
}
if (name != null && arg.name != null) {
int secondCompare = name.compareTo(arg.name);
if (secondCompare != 0) {
return secondCompare;
}
}
return (arg.id < id ? -1 : (arg.id == id ? 0 : 1));
}
}
Manx.java
public class Manx extends Cat {
public Manx(String name) { super(name); }
public Manx() { super(); }
}
Mouse.java
public class Mouse extends Rodent {
public Mouse(String name) { super(name); }
public Mouse() { super(); }
}
Mutt.java
public class Mutt extends Dog {
public Mutt(String name) { super(name); }
public Mutt() { super(); }
}
Pet.java
public class Pet extends Individual {
public Pet(String name) {
super(name);
}
public Pet() {
super();
}
}
PetCreator.java
import java.util.*;
public class PetCreator extends Creator {
// No try block needed.
public static final
List<Class<? extends Pet>> ALL_TYPES = Collections.unmodifiableList(Arrays.asList(
Pet.class, Dog.class, Cat.class, Rodent.class,
Mutt.class, Pug.class, EgyptianMau.class,
Manx.class, Cymric.class, Rat.class,
Mouse.class, Hamster.class));
// Types for random creation:
private static final
List<Class<? extends Pet>> TYPES =
ALL_TYPES.subList(
ALL_TYPES.indexOf(Mutt.class),
ALL_TYPES.size());
@Override
public List<Class<? extends Pet>> types() {
return TYPES;
}
public static void main(String[] args) {
System.out.println(TYPES);
List<Pet> pets = new PetCreator().list(7);
System.out.println(pets);
}
}
/* Output:
[class reflection.pets.Mutt, class reflection.pets.Pug,
class reflection.pets.EgyptianMau, class
reflection.pets.Manx, class reflection.pets.Cymric, class
reflection.pets.Rat, class reflection.pets.Mouse, class
reflection.pets.Hamster]
[Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug]
*/
Pug.java
public class Pug extends Dog {
public Pug(String name) { super(name); }
public Pug() { super(); }
}
Rat.java
public class Rat extends Rodent {
public Rat(String name) { super(name); }
public Rat() { super(); }
}
Rodent.java
public class Rodent extends Pet {
public Rodent(String name) { super(name); }
public Rodent() { super(); }
}
打印内容如下:
有了 Iterator ,就不必再为集合中元素的数量操心了。这是由 hasNext()
和 next()
关心的事情。
如果只是想向前遍历 List ,并不打算修改 List 对象本身,那么使用 for-in 语法更加简洁。
Iterator 还可以删除由 next()
生成的最后一个元素,这意味着在调用 remove()
之前必须先调用 next()
。
在集合中的每个对象上执行操作,这种思想十分强大,并且贯穿于本书。
现在考虑创建一个 display()
方法,它不必知晓集合的确切类型:
public class CrossCollectionIteration {
public static void display(Iterator<Pet> it) {
while (it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Pet> pets = new PetCreator().list(8);
LinkedList<Pet> petsLL = new LinkedList<>(pets);
HashSet<Pet> petsHS = new HashSet<>(pets);
TreeSet<Pet> petsTS = new TreeSet<>(pets);
display(pets.iterator());
display(petsLL.iterator());
display(petsHS.iterator());
display(petsTS.iterator());
}
}
display()
方法不包含任何有关它所遍历的序列的类型信息。这也展示了 Iterator 的真正威力:能够将遍历序列的操作与该序列的底层结构分离。出于这个原因,我们有时会说:迭代器统一了对集合的访问方式。
我们可以使用 Iterable 接口生成上一个示例的更简洁版本,该接口描述了“可以产生 Iterator 的任何东西”:
import java.util.*;
public class CrossCollectionIteration2 {
public static void display(Iterable<Pet> ip) {
Iterator<Pet> it = ip.iterator();
while (it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Pet> pets = new PetCreator().list(8);
LinkedList<Pet> petsLL = new LinkedList<>(pets);
HashSet<Pet> petsHS = new HashSet<>(pets);
TreeSet<Pet> petsTS = new TreeSet<>(pets);
display(pets);
display(petsLL);
display(petsHS);
display(petsTS);
}
}
这里所有的类都是 Iterable ,所以现在对 display()
的调用显然更简单。
ListIterator
ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且可以使用 set()
方法替换它访问过的最近一个元素。可以通过调用 listIterator()
方法来生成指向 List 开头处的 ListIterator ,还可以通过调用 listIterator(n)
创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator 。 下面的示例演示了所有这些能力:
import java.util.List;
import java.util.ListIterator;
public class ListIteration {
public static void main(String[] args) {
List<Pet> pets = new PetCreator().list(8);
ListIterator<Pet> it = pets.listIterator();
while(it.hasNext()) {
System.out.print(it.next() +
", " + it.nextIndex() +
", " + it.previousIndex() + "; ");
}
System.out.println();
// Backwards:
while(it.hasPrevious()) {
System.out.print(it.previous().id() + " ");
}
System.out.println();
System.out.println(pets);
it = pets.listIterator(3);
while(it.hasNext()) {
it.next();
it.set(new PetCreator().get());
}
System.out.println(pets);
}
}
new PetCreator().get()
方法用来从位置 3 开始替换 List 中的所有 Pet 对象。
链表LinkedList
LinkedList 也像 ArrayList 一样实现了基本的 List 接口,但它在 List 中间执行插入和删除操作时比 ArrayList 更高效。然而,它在随机访问操作效率方面却要逊色一些。
LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque) 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。例如:
getFirst()
和element()
是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 List 为空,则抛出 NoSuchElementException 异常。peek()
方法与这两个方法只是稍有差异,它在列表为空时返回 null 。removeFirst()
和remove()
也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常。poll()
稍有差异,它在列表为空时返回 null 。addFirst()
在列表的开头插入一个元素。offer()
与add()
和addLast()
相同。 它们都在列表的尾部(末尾)添加一个元素。removeLast()
删除并返回列表的最后一个元素。
下面的示例展示了这些功能之间基本的相似性和差异性。它并不是重复执行 ListFeatures.java 中所示的行为:
public class LinkedListFeatures {
public static void main(String[] args) {
LinkedList<Pet> pets = new LinkedList<>(new PetCreator().list(5));
System.out.println(pets);
// Identical:
System.out.println("pets.getFirst(): " + pets.getFirst());
System.out.println("pets.element(): " + pets.element());
// Only differs in empty-list behavior:
System.out.println("pets.peek(): " + pets.peek());
// Identical; remove and return the first element:
System.out.println("pets.remove(): " + pets.remove());
System.out.println("pets.removeFirst(): " + pets.removeFirst());
// Only differs in empty-list behavior:
System.out.println("pets.poll(): " + pets.poll());
System.out.println(pets);
pets.addFirst(new Rat());
System.out.println("After addFirst(): " + pets);
pets.offer(new PetCreator().get());
System.out.println("After offer(): " + pets);
pets.add(new PetCreator().get());
System.out.println("After add(): " + pets);
pets.addLast(new Hamster());
System.out.println("After addLast(): " + pets);
System.out.println("pets.removeLast(): " + pets.removeLast());
}
}
new PetCreator().list()
的结果被传递给 LinkedList 的构造器,以便使用它来填充 LinkedList 。如果查看 Queue 接口就会发现,它在 LinkedList 的基础上添加了 element()
, offer()
, peek()
, poll()
和 remove()
方法,以使其可以成为一个 Queue 的实现。 Queue 的完整示例将在本章稍后给出。
堆栈Stack
堆栈是“后进先出”(LIFO)集合。它有时被称为_叠加栈_(pushdown stack),因为最后“压入”(push)栈的元素,第一个被“弹出”(pop)栈。经常用来类比栈的事物是带有弹簧支架的自助餐厅托盘。最后装入的托盘总是最先拿出来使用的。
Java 1.0 中附带了一个 Stack 类,结果设计得很糟糕(为了向后兼容,我们被迫一直忍受 Java 中的旧设计错误)。Java 6 添加了 ArrayDeque ,其中包含直接实现堆栈功能的方法:
import java.util.*;
public class StackTest {
public static void main(String[] args) {
Deque<String> stack = new ArrayDeque<>();
for (String s : "My dog has fleas".split(" ")) {
stack.push(s);
}
while (!stack.isEmpty()) {
System.out.print(stack.pop() + " ");
}
}
}
即使它是作为一个堆栈在使用,我们仍然必须将其声明为 Deque 。有时一个名为 Stack 的类更能把事情讲清楚:
import java.util.ArrayDeque;
import java.util.Deque;
public class Stack<T> {
private Deque<T> storage = new ArrayDeque<>();
public void push(T v) {
storage.push(v);
}
public T peek() {
return storage.peek();
}
public T pop() {
return storage.pop();
}
public boolean isEmpty() {
return storage.isEmpty();
}
@Override
public String toString() {
return storage.toString();
}
}
这里引入了使用泛型的类定义的最简单的可能示例。类名称后面的 告诉编译器这是一个参数化类型,而其中的类型参数 T 会在使用类时被实际类型替换。基本上,这个类是在声明“我们在定义一个可以持有 T 类型对象的 Stack 。” Stack 是使用 ArrayDeque 实现的,而 ArrayDeque 也被告知它将持有 T 类型对象。注意, push()
接受类型为 T 的对象,而 peek()
和 pop()
返回类型为 T 的对象。 peek()
方法将返回栈顶元素,但并不将其从栈顶删除,而 pop()
删除并返回顶部元素。
如果只需要栈的行为,那么使用继承是不合适的,因为这将产生一个具有 ArrayDeque 的其它所有方法的类。使用组合,可以选择要公开的方法以及如何命名它们。
下面将使用 StackTest.java 中的相同代码来演示这个新的 Stack 类:
public class StackTest2 {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String s : "My dog has fleas".split(" ")) {
stack.push(s);
}
while (!stack.isEmpty()) {
System.out.print(stack.pop() + " ");
}
}
}
如果想在自己的代码中使用这个 Stack 类,当在创建其实例时,就需要完整指定包名,或者更改这个类的名称;否则,就有可能会与 java.util 包中的 Stack 发生冲突。例如,如果我们在上面的例子中导入 java.util.*,那么就必须使用包名来防止冲突:
public class StackCollision {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String s : "My dog has fleas".split(" ")) {
stack.push(s);
}
while (!stack.isEmpty()) {
System.out.print(stack.pop() + " ");
}
System.out.println();
java.util.Stack<String> stack2 =
new java.util.Stack<>();
for (String s : "My dog has fleas".split(" ")) {
stack2.push(s);
}
while (!stack2.empty()) {
System.out.print(stack2.pop() + " ");
}
}
}
尽管已经有了 java.util.Stack ,但是 ArrayDeque 可以产生更好的 Stack ,因此更可取。
还可以使用显式导入来控制对“首选” Stack 实现的选择:
import onjava.Stack;
现在,任何对 Stack 的引用都将选择 onjava 版本,而在选择 java.util.Stack 时,必须使用全限定名称(full qualification)。