Java 中那些绕不开的内置接口 -- Iterator 和 Iterable
在平时开发中,让我们直接手动进行 Java 对象的序列化场景非常少,更多是 框架、JVM 在帮我们做这个事儿,我们也介绍了 Java 序列化与 JSON 这样通用序列化格式的区别,以及各自的优势,今天我们继续介绍两个从名字和功能上乍一看都很相似的基础接口 -- Iterator 和 Iterable。
Java 的 Iterable 接口用来表示可迭代对象的集合,这意味着,实现 了 Iterable 接口的类可以迭代其元素。Java 里还有一个 Iterator 接口表示迭代器,迭代其实我们也可以简单地理解为遍历,是一个标准化遍历各类 Collection 容器里面的所有对象的接口。Java 的 Iterator 接口相当于是从语言本身支持迭代器这种设计模式,在文中介绍 Iterator 接口前,我们会先介绍一下迭代器设计模式的思想,以及使用这种设计模式能获得的收益。
Iterator 和 Iterable 这两个接口在命名上相似,新手学习时容易混淆,所以在这里把两者展开,梳理明白。本文大纲如下:
为避免混淆,Collection 和 集合指的是同类,而另外一个可以翻译成集合的Set 数据结构文中会直接用Set表示。
1
Iterator 接口
Iterator 接口的全限定名是 java.util.Iterator ,我们前面学习的 Java 集合框架里的各种容器也是 Iterator 接口的实现。java.util.Iterator和它的实现类是 Java 语言内置提供的对迭代器模式的支持,这里我们先花点篇幅了解一下,到底什么是迭代器模式。
迭代器模式
迭代器模式用于遍历集合中的对象,很多语言里都内置了这种设计模式,迭代器模式的思想是将集合对象的遍历操作从集合类中拆分出来,放到迭代器实现类中,让两者的职责更单一。
迭代器模式属于 GoF 总结的 23 种设计模式中的行为型模式。在这种模式中,集合结构会提供一个迭代器。通过这个迭代器可以顺序遍历集合中的每个元素而不需要暴露其具体的实现。其结构,可以用下面这个 UML 类图表示:
Iterator 接口:这个接口会定义一些基础的操作函数,如hasNext()或getNext()等。通过名称就可以看出,这些方法可以帮助我们执行遍历集合、重启迭代等操作。
Collection 接口:这个接口代表了要被遍历的集合。在这个接口里定义了一个createIterator方法,该方法会返回一个Iterator的实例。
Concrete Iterator:Iterator接口的具体实现类。
Concrete Collection:Collection接口的具体实现类。
上面这个迭代器模式的结构中的各个角色映射到 Java 内置为我们提供的迭代器模式实现中来的话,iterator接口就是一会儿要说到的java.util.iterator接口,它定义了模式中迭代器的行为规范;collection接口就是我们前面学过了集合框架的顶层接口java.util.collection; Concrete Iterator 和Concrete Collection即两个顶层接口的实现类则对应于前面几节学过的集合框架中的List, Set这些实现类,当然初次之外,我们在 Java 里也可以根据自己的需求,自己实现java.util.iterator接口,来提供迭代器的自定义实现类。
了解完迭代器模式后,接下来我们看看java.util.iterator接口。
Java提供的迭代器接口
要使用 Iterator 遍历元素,必须从要迭代的 Collection 对象中获取一个 Iterator 实例。获得的 Iterator 会跟踪底层 Collection 中的元素,以确保能正确遍历所有元素。如果在迭代器 Iterator 在迭代的过程中,代码对其指向的底层 Collection 进行了修改,Iterator 通常会检测到,并在下次尝试从 Iterator 获取下一个元素时抛出异常。
Iterator 的定义如下
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
Iterator 接口相当简单,只定义了四个方法,其中两个还提供了默认的实现。
方法名 描述
hasNext() 如果迭代器中还有元素,则返回 true,否则返回 false。
next() 从迭代器返回下一个元素
remove() 从迭代器正在迭代的集合中移除从 next() 返回的最新元素。
forEachRemaining() 迭代迭代器中的所有剩余元素,并调用 Lambda 表达式,将每个剩余元素作为参数传递给 lambda 表达式。
下面的内容会介绍每个方法如何使用。
获取迭代器
Java 集合接口 java.util.Collection 中包含一个称为 iterator() 的方法。通过调用 iterator(),可以从给定的 Collection 中获取迭代器。 Java 集合框架中实现了 Collection 接口的数据结构,比如列表、集合(Set)、队列、双端队列等等,它们都实现了iterator() 方法。
下面是从各种集合类型获取迭代器的几个示例:
List<String> list = new ArrayList<>();
list.add("one");
list.add("two");
list.add("three");
Iterator<String> iterator = list.iterator();
Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
set.add("three");
Iterator<String> iterator2 = set.iterator();
迭代迭代器
可以使用 while 循环迭代 Iterator 中的对象。下面是一个使用 while 循环迭代 Iterator 元素的示例:
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
Object nextObject = iterator.next();
}
上面的例子中有两个方法需要注意。第一个方法是 hasNext() 方法,如果 Iterator 包含更多元素,则返回 true。换句话说,如果 Iterator 还没有遍历完其指向的 Collection 中的所有元素 - hasNext() 方法将返回 true。如果 Iterator 已经迭代了底层集合中的所有元素 - hasNext() 方法返回 false。要注意的第二个方法是 next() 方法。next() 方法返回迭代器正在迭代的 Collection 的下一个元素
迭代顺序
Iterator 中包含的元素的遍历顺序取决于提供 Iterator 的对象。例如,从 List 获得的迭代器将按照元素在 List 内部存储的相同顺序遍历该 List 的元素。而从 Set 获得的 Iterator 不保证任何 Set 中元素迭代的顺序。
迭代期间修改底层集合
某些集合不允许您在通过 Iterator 迭代时修改集合。在这种情况下,将在下次调用 Iterator next() 方法时抛出异ConcurrentModificationException。以下示例在执行时程序会导致 ConcurrentModificationException:
List<String> list = new ArrayList<>();
list.add("123");
list.add("456");
list.add("789");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String value = iterator.next();
if(value.equals("456")){
list.add("999");
}
}
如果在通过 Iterator 迭代集合时修改了集合,则会抛出
ConcurrentModificationException 因为 Iterator 与底层集合不再同步。
迭代过程中删除元素
上面说了在迭代过程中修改迭代器指向的底层集合会导致抛出异常,但是 Iterator 接口也有一个 remove() 方法,它允许我们从底层集合中删除 next() 刚刚返回的元素。调用 remove() 不会导致抛出
ConcurrentModificationException。
List<String> list = new ArrayList<>();
list.add("123");
list.add("456");
list.add("789");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String value = iterator.next();
if(value.equals("456")){
iterator.remove();
}
}
forEachRemaining
Iterator 的 forEachRemaining() 方法可以在内部迭代 Iterator 中剩余的所有元素,并为每个元素调用作为参数传递给 forEachRemaining() 的 Lambda 表达式。以下是使用 forEachRemaining() 方法的示例:
List<String> list = new ArrayList<>();
list.add("Jane");
list.add("Heidi");
list.add("Hannah");
Iterator<String> iterator = list.iterator();
iterator.forEachRemaining((element) -> {
System.out.println(element);
});
双向迭代器
Java 还包含一个名为 ListIterator 的接口,它扩展了 Iterator 接口,表示双向一个迭代器 - 意味着我们可以在迭代中向前和向后导航元素的迭代器。下面的例程展示了如何使用双向迭代器向前后和向后迭代。
List<String> list = new ArrayList<>();
list.add("Jane");
list.add("Heidi");
list.add("Hannah");
ListIterator<String> listIterator = list.listIterator();
while(listIterator.hasNext()) {
System.out.println(listIterator.next());
}
while(listIterator.hasPrevious()) {
System.out.println(listIterator.previous());
}
示例首先通过所有元素向前迭代 ListIterator,然后再次通过所有元素向后迭代回到第一个元素。
Iterator 的自定义实现
如果你有一个特殊的、自定义集合类型,你可以通过自己实现 Iterator 接口来创建一个迭代器,它可以遍历你的自定义集合的元素。
在本节中,我将展示 Iterator 接口的一个超级简单的自定义实现。例子实现的比较粗糙,没有考虑在迭代过程中检测内部元素是否被更改之类的问题,但是足以让你了解 Iterator 的实现应该长什么样。
import java.util.Iterator;
import java.util.List;
public class ListIterator <T> implements Iterator<T> {
private List<T> source = null;
private int index = 0;
public ListIterator(List<T> source){
this.source = source;
}
@Override
public boolean hasNext() {
return this.index < this.source.size();
}
@Override
public T next() {
return this.source.get(this.index++);
}
remove() 和 foreachRemaining() 方法在 Iterator 接口里提供了默认实现,所以我们这个例子只提供了 hasNext() 和 next() 两个方法的实现。这个实现类的使用方法如下:
import java.util.ArrayList;
import java.util.List;
public class ListIteratorExample {
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("one");
list.add("two");
list.add("three");
ListIterator<String> iterator = new ListIterator<>(list);
while(iterator.hasNext()) {
System.out.println( iterator.next() );
}
}
}
2
Iterable 接口
Iterable 接口的全称是 java.lang.Iterable ,它表示可迭代对象的集合,这意味着,实现 Iterable 接口的类可以迭代其元素。
Iterable 接口定义了三个方法,其中两个提供了默认实现,只有 iterator() 方法是要求实现类必须实现的,该方法返回一个上个章节已经介绍过的 Iterator 迭代器。
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
java.util.Collection 接口扩展了 Iterable 接口,所以集合框架里的 Collection 类族也都是 Iterable 的实现类--即他们的对象都可以迭代内部元素。
在 Java 里有三种方式可以迭代 Iterable 实现类的对象:通过 for-each 循环、获取 Iterable 实现类对象的迭代器(Iterator) 或者是通过调用 Iterable 的 forEach() 方法。
使用for-each循环迭代Iterable对象
下面的示例展示了如何通过 Java 的 for-each 循环迭代 列表的元素。由于Java 的 List 接口扩展了Collection 接口,而Collection 接口又扩展了Iterable 接口,因此List 对象被 for-each 循环迭代。
List<String> list = new ArrayList><();
list.add("one");
list.add("two");
list.add("three");
for( String element : list ){
System.out.println( element.toString() );
}
通过迭代器迭代Iterable对象
迭代 Iterable 对象元素的第二种方法是,通过调用Collection 提供的iterator() 方法从中获取指向对象元素的迭代器,然后使用迭代器迭代元素。
List<String> list = new ArrayList><();
list.add("one");
list.add("two");
list.add("three");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String element = iterator.next();
System.out.println( element );
}
使用forEach方法迭代 Iterable 对象
迭代 Iterable 对象元素的第三种方法是通过其 forEach() 方法。forEach() 方法使用 Lambda 表达式作为参数。对 Iterable 对象中的每个元素,都会调用一次此 lambda 表达式。
List<String> list = new ArrayList><();
list.add("one");
list.add("two");
list.add("three");
list.forEach( (element) -> {
System.out.println( element );
});
编写自己的 Iterable 实现
需要自己 Iterable 的需求很少,一般 集合框架里提供的数据结构就能满足我们的开发需求了。不过为了演示,下面给出 Iterable 接口的一个超简单的自定义实现。
public class Persons implements Iterable {
private List<Person> persons;
public Persons(List<Person> persons) {
this.persons = persons;
}
public Iterator<Person> iterator() {
return this.persons.iterator();
}
}
3
总结
本篇文章给大家梳理了 Java 内置提供的 Iterator 和 Iterable接口,两者都跟集合对象的迭代有关,相信大家在看 Java 提供的一些内置对象、工具类方法的参数定义里见过这两个从名字上看长有点类似的接口类型。相信看过这篇文章,会减少你不少--“这两块货都是啥?”的疑惑。
在介绍Iterator 接口时我们提到了,它是 Java 语言为我们提供的迭代器这种设计模式的在语言层面的支持,也对迭代器设计模式做了一些解释,希望你能喜欢本篇内容
作者:码出宇宙
欢迎关注微信公众号 :码出宇宙
扫描添加好友邀你进技术交流群,加我时注明【姓名+公司(学校)+职位】