引言
我们知道集合可以进行for-each遍历,为什么集合支持这种遍历呢?我们自定义的类,应该怎么做才能支持这种遍历?
Iterable介绍
Iterable,即迭代器的意思。其作用是为集合类提供for-each循环遍历的支持,只要让一个类实现这个接口,该类的对象就可以成为for-each循环遍历的目标。换句话说,想让一个Java对象支持for-each遍历,只要让它的类实现Iterable接口即可。而这具体又是如何做到的呢?我们来看下它的源码。
Iterable源码分析
一、接口定义
public interface Iterable<T>
二、方法定义
Iterator<T> iterator();
返回建立在类型为T的元素之上的一个Iterator对象
此方法是Iterable接口的主要方法。我们知道,想让一个类实现Iterable接口,其对象就可以使用for-each遍历。而具体如何做到就是通过此方法。比如说,我们可以通过 Iterator iterator = strings.iterator(); 的方式取得集合的Iterator对象,然后就可以利用Iterator对象对集合元素进行遍历了。
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
将集合中的每个元素作为参数传递给action执行特定的操作,直到所有元素都被处理过或者某个元素在被处理的过程中抛出异常。
执行顺序:除非实现类另有所指,否则各个元素被执行action操作时,会按照元素被迭代时的次序执行。
异常抛出:元素被执行action操作时,如果抛出异常,异常将会被中继到方法的调用方。
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
创建一个作用在迭代器的元素之上的Spliterator。
Iterator介绍
从上面的讨论可以知道,集合类for-each遍历的方式主要是通过调用Iterable的iterator()方法返回一个Iterator对象来实现的。Iterator是for-each遍历的主体。
Iterator即迭代器,是一个由集合框架提供、用于遍历一个集合的接口,并且访问集合的元素时是有序访问的。我们可以这样理解,集合的迭代器包含集合的所有元素,它们在遍历时按照一定的迭代次序进行。在迭代器中有一个光标指向最先迭代的元素,当执行next()方法时,将该元素从迭代器中删掉,然后光标往前移动指向剩下的元素中最早迭代的元素。
Iterator与枚举有些类似,不过它们有两点不同:
1)迭代器允许调用者在迭代期间从迭代器所指向的集合中删除元素
2)迭代器中的方法名称与枚举相比得到了改进(迭代器可以说是用来代替Enumeration的)
Iterator源码分析
一、接口定义
public interface Iterator<E>
二、方法定义
boolean hasNext();
判断一个对象集合是否还有下一个元素。
当迭代器中还有元素时,返回true。即,当如果再调用next()方法还可以返回一个元素,而不是抛出异常时,返回true。
E next();
获取迭代器中的下一个元素
当迭代器中没有元素时,调用此方法会抛出NoSuchElementException异常。
default void remove() {
throw new UnsupportedOperationException("remove");
}
删除最后一个元素
可以看到方法里面直接抛出异常,也就是默认是不支持删除操作的。为什么要这样做,是因为在很多情况下执行删除操作其结果不可预测,比如,当执行删除操作时,数据集合正好被修改中。
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
此方法主要是将迭代器中剩下的还没遍历到的每个元素都发给action来执行特定操作
ListIterator介绍
ListIterator是继承自Iterator接口的一个子接口,它是为需要双向遍历数据时准备的,ArrayList、LinkedList的源码中都用到了这个接口。ListIterator允许双向遍历,所谓双向遍历就相当于有两个指针,分别指向迭代器中按照正向迭代顺序最先遍历到的元素以及最晚遍历到的元素。同时,迭代期间可以修改(删除、更新)列表元素。
ListIterator源码分析
一、接口定义
public interface ListIterator<E> extends Iterator<E>
二、方法定义
ListIterator既然是继承Iterator接口,所以自然就有Iterator的所有方法。我们只需要关注它在Iterator的基础上新增加的方法即可。不过,要注意到ListIterator重写了Iterator的remove()方法。
void remove();
也就是说,ListIterator的remove()方法不再是默认抛出一个异常,也就是说,实现ListIterator的集合默认可以删除元素。
此方法可以删除next()或者previous()返回的元素(可选操作)
下面再看看ListIterator新增的方法
boolean hasPrevious();
判断逆向遍历时,前面是否还有元素未遍历
E previous();
获取前一个元素
int nextIndex();
获取正向遍历时下一个元素的位置
如果到达列表的末端,就返回列表的大小
int previousIndex();
获取逆向遍历时前一个元素的位置
如果到达列表大前端,就返回-1
void add(E e);
添加一个元素
将元素添加在next()返回的元素的前面,或者previous()返回的元素的后面(可选操作)
void set(E e);
替换next()或者previous()返回的最后一个元素(可选操作),此方法只能在没有调用remove()、add()方法时调用
三种遍历集合的方式
我们知道遍历集合有三种方式:通过迭代器Iterator、for-each循环遍历、传统的for循环遍历
三种方式有什么不同呢?我们先看下各自的使用
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
public class IteratorTest {
public static void main(String[] args) {
//创建一个存储整数元素的链表
List<Integer> list = new LinkedList<Integer>();
//添加元素到链表中
list.add(2);
list.add(3);
list.add(4);
//1、使用迭代器遍历
Iterator<Integer> itr = list.iterator();
for (; itr.hasNext(); ) {
System.out.print(itr.next() +" ");
}
System.out.println("\n============================");
//System.out.println(list); //[2, 3, 4] 只是迭代器中元素删除了而已,集合中没有删除
//2、使用for-each遍历
for (Integer item : list) {
System.out.print(item +" ");
}
System.out.println("\n============================");
//3、使用for遍历
for (int i=0; i<list.size(); i++) {
System.out.print(list.get(i) +" ");
}
}
}
运行结果如下图所示
可见,都可以达到遍历集合的目的
使用反编译工具得到上面编译得到的class文件的代码
LinkedList linkedlist = new LinkedList();
linkedlist.add(Integer.valueOf(2));
linkedlist.add(Integer.valueOf(3));
linkedlist.add(Integer.valueOf(4));
for(Iterator iterator = linkedlist.iterator(); iterator.hasNext(); System.out.print((new StringBuilder()).append(iterator.next()).append(" ").toString()));
System.out.println("\n============================");
Integer integer;
for(Iterator iterator1 = linkedlist.iterator(); iterator1.hasNext(); System.out.print((new StringBuilder()).append(integer).append(" ").toString()))
integer = (Integer)iterator1.next();
System.out.println("\n============================");
for(int i = 0; i < linkedlist.size(); i++)
System.out.print((new StringBuilder()).append(linkedlist.get(i)).append(" ").toString());
从反编译的结果可以看出,使用iterator和使用for-each遍历的底层是一样的,也就是说for-each遍历的底层实现是借助Iterator实现的(其实,不只是for-each循环,Java中还有很多特性也是通过接口来实现的)。因此,这两种方式的时间效率是一样的。不过,使用传统的for循环方式则不同了。迭代时for循环方式是通过元素位置访问元素,如果集合是可以随机访问,比如ArrayList,则访问一个元素的时间是O(1)。而如果集合无法随机访问,比如LinkedList访问一个元素的时间是O(n),则此时使用for循环方式将会慢得多。