Java迭代器Iterator和Iterable有什么区别?

embedded/2024/9/23 1:13:58/

在 Java 中,我们对 List 进行遍历的时候,主要有这么三种方式。

第一种:for 循环。

for (int i = 0; i < list.size(); i++) {System.out.print(list.get(i) + ",");
}

第二种:迭代器。

Iterator it = list.iterator();
while (it.hasNext()) {System.out.print(it.next() + ",");
}

第三种:for-each。

for (String str : list) {System.out.print(str + ",");
}

第一种我们略过,第二种用的是 Iterator,第三种看起来是 for-each,其实背后也是 Iterator,看一下反编译后的代码(如下所示)就明白了。

Iterator var3 = list.iterator();while(var3.hasNext()) {String str = (String)var3.next();System.out.print(str + ",");
}

for-each 只不过是个语法糖,让我们开发者在遍历 List 的时候可以写更少的代码,更简洁明了。

Iterator 是个接口,JDK 1.2 的时候就有了,用来改进 Enumeration 接口:

  • 允许删除元素(增加了 remove 方法)
  • 优化了方法名(Enumeration 中是 hasMoreElements 和 nextElement,不简洁)

来看一下 Iterator 的源码:

public interface Iterator<E> {// 判断集合中是否存在下一个对象boolean hasNext();// 返回集合中的下一个对象,并将访问指针移动一位E next();// 删除集合中调用next()方法返回的对象default void remove() {throw new UnsupportedOperationException("remove");}
}

JDK 1.8 时,Iterable 接口中新增了 forEach 方法。该方法接受一个 Consumer 对象作为参数,用于对集合中的每个元素执行指定的操作。该方法的实现方式是使用 for-each 循环遍历集合中的元素,对于每个元素,调用 Consumer 对象的 accept 方法执行指定的操作。

default void forEach(Consumer<? super T> action) {Objects.requireNonNull(action);for (T t : this) {action.accept(t);}
}

该方法实现时首先会对 action 参数进行非空检查,如果为 null 则抛出 NullPointerException 异常。然后使用 for-each 循环遍历集合中的元素,并对每个元素调用 action.accept(t) 方法执行指定的操作。由于 Iterable 接口是 Java 集合框架中所有集合类型的基本接口,因此该方法可以被所有实现了 Iterable 接口的集合类型使用。

它对 Iterable 的每个元素执行给定操作,具体指定的操作需要自己写Consumer接口通过accept方法回调出来。

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.forEach(integer -> System.out.println(integer));

写得更浅显易懂点,就是:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.forEach(new Consumer<Integer>() {@Overridepublic void accept(Integer integer) {System.out.println(integer);}
});

如果我们仔细观察ArrayList 或者 LinkedList 的“户口本”就会发现,并没有直接找到 Iterator 的影子。

反而找到了 Iterable!

public interface Iterable<T> {Iterator<T> iterator();
}

也就是说,List 的关系图谱中并没有直接使用 Iterator,而是使用 Iterable 做了过渡。

回头再来看一下第二种遍历 List 的方式。

Iterator it = list.iterator();
while (it.hasNext()) {
}

发现刚好呼应上了。拿 ArrayList 来说吧,它重写了 Iterable 接口的 iterator 方法:

public Iterator<E> iterator() {return new Itr();
}

返回的对象 Itr 是个内部类,实现了 Iterator 接口,并且按照自己的方式重写了 hasNext、next、remove 等方法。

/*** ArrayList 迭代器的实现,内部类。*/
private class Itr implements Iterator<E> {/*** 游标位置,即下一个元素的索引。*/int cursor;/*** 上一个元素的索引。*/int lastRet = -1;/*** 预期的结构性修改次数。*/int expectedModCount = modCount;/*** 判断是否还有下一个元素。** @return 如果还有下一个元素,则返回 true,否则返回 false。*/public boolean hasNext() {return cursor != size;}/*** 获取下一个元素。** @return 列表中的下一个元素。* @throws NoSuchElementException 如果没有下一个元素,则抛出 NoSuchElementException 异常。*/@SuppressWarnings("unchecked")public E next() {// 获取 ArrayList 对象的内部数组Object[] elementData = ArrayList.this.elementData;// 记录当前迭代器的位置int i = cursor;if (i >= size) {throw new NoSuchElementException();}// 将游标位置加 1,为下一次迭代做准备cursor = i + 1;// 记录上一个元素的索引return (E) elementData[lastRet = i];}/*** 删除最后一个返回的元素。* 迭代器只能删除最后一次调用 next 方法返回的元素。** @throws ConcurrentModificationException 如果在最后一次调用 next 方法之后列表结构被修改,则抛出 ConcurrentModificationException 异常。* @throws IllegalStateException         如果在调用 next 方法之前没有调用 remove 方法,或者在同一次迭代中多次调用 remove 方法,则抛出 IllegalStateException 异常。*/public void remove() {// 检查在最后一次调用 next 方法之后是否进行了结构性修改if (expectedModCount != modCount) {throw new ConcurrentModificationException();}// 如果上一次调用 next 方法之前没有调用 remove 方法,则抛出 IllegalStateException 异常if (lastRet < 0) {throw new IllegalStateException();}try {// 调用 ArrayList 对象的 remove(int index) 方法删除上一个元素ArrayList.this.remove(lastRet);// 将游标位置设置为上一个元素的位置cursor = lastRet;// 将上一个元素的索引设置为 -1,表示没有上一个元素lastRet = -1;// 更新预期的结构性修改次数expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}
}

那可能有些小伙伴会问:为什么不直接将 Iterator 中的核心方法 hasNext、next 放到 Iterable 接口中呢?直接像下面这样使用不是更方便?

Iterable it = list.iterator();
while (it.hasNext()) {
}

从英文单词的后缀语法上来看,(Iterable)able 表示这个 List 是支持迭代的,而 (Iterator)tor 表示这个 List 是如何迭代的。

支持迭代与具体怎么迭代显然不能混在一起,否则就乱的一笔。还是各司其职的好。

想一下,如果把 Iterator 和 Iterable 合并,for-each 这种遍历 List 的方式是不是就不好办了?

原则上,只要一个 List 实现了 Iterable 接口,那么它就可以使用 for-each 这种方式来遍历,那具体该怎么遍历,还是要看它自己是怎么实现 Iterator 接口的。

Map 就没办法直接使用 for-each,因为 Map 没有实现 Iterable 接口,只有通过 map.entrySet()map.keySet()map.values() 这种返回一个 Collection 的方式才能 使用 for-each。

如果我们仔细研究 LinkedList 的源码就会发现,LinkedList 并没有直接重写 Iterable 接口的 iterator 方法,而是由它的父类 AbstractSequentialList 来完成。

public Iterator<E> iterator() {return listIterator();
}

LinkedList 重写了 listIterator 方法:

public ListIterator<E> listIterator(int index) {checkPositionIndex(index);return new ListItr(index);
}

这里我们发现了一个新的迭代器 ListIterator,它继承了 Iterator 接口,在遍历List 时可以从任意下标开始遍历,而且支持双向遍历。

public interface ListIterator<E> extends Iterator<E> {boolean hasNext();E next();boolean hasPrevious();E previous();
}

我们知道,集合(Collection)不仅有 List,还有 Set,那 Iterator 不仅支持 List,还支持 Set,但 ListIterator 就只支持 List。

那可能有些小伙伴会问:为什么不直接让 List 实现 Iterator 接口,而是要用内部类来实现呢?

这是因为有些 List 可能会有多种遍历方式,比如说 LinkedList,除了支持正序的遍历方式,还支持逆序的遍历方式——DescendingIterator:

/*** ArrayList 逆向迭代器的实现,内部类。*/
private class DescendingIterator implements Iterator<E> {/*** 使用 ListItr 对象进行逆向遍历。*/private final ListItr itr = new ListItr(size());/*** 判断是否还有下一个元素。** @return 如果还有下一个元素,则返回 true,否则返回 false。*/public boolean hasNext() {return itr.hasPrevious();}/*** 获取下一个元素。** @return 列表中的下一个元素。* @throws NoSuchElementException 如果没有下一个元素,则抛出 NoSuchElementException 异常。*/public E next() {return itr.previous();}/*** 删除最后一个返回的元素。* 迭代器只能删除最后一次调用 next 方法返回的元素。** @throws UnsupportedOperationException 如果列表不支持删除操作,则抛出 UnsupportedOperationException 异常。* @throws IllegalStateException         如果在调用 next 方法之前没有调用 remove 方法,或者在同一次迭代中多次调用 remove 方法,则抛出 IllegalStateException 异常。*/public void remove() {itr.remove();}
}

可以看得到,DescendingIterator 刚好利用了 ListIterator 向前遍历的方式。可以通过以下的方式来使用:

Iterator it = list.descendingIterator();
while (it.hasNext()) {
}

好了,关于Iterator与Iterable我们就先聊这么多,总结两点:

  • 学会深入思考,一点点抽丝剥茧,多想想为什么这样实现,很多问题没有自己想象中的那么复杂。
  • 遇到疑惑不放弃,这是提升自己最好的机会,遇到某个疑难的点,解决的过程中会挖掘出很多相关的东西。

http://www.ppmy.cn/embedded/115331.html

相关文章

Kali root密码忘记的解决方法

Kali root密码忘记的解决方法 uname -a: Linux xkm 5.10.0-21-amd64 #1 SMP Debian 5.10.162-1 (2023-01-21) x86_64 GNU/Linux背景 许久未用的虚拟机&#xff0c;密码忘了&#xff0c;按照有的搜索结果操作竟然不行&#xff08;那篇是乱写的&#xff09;。 按照这篇博客&a…

19 基于51单片机的倒计时音乐播放系统设计

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 五个按键&#xff0c;分别为启动按键&#xff0c;则LCD1602显示倒计时&#xff0c;音乐播放 设置按键&#xff0c;可以设置倒计时的分秒&#xff0c;然后加减按键&#xff0c;还有最后一个暂停音乐…

Mysql学习

目录 1、常用命令&#xff1a; 2、SQL语言分类 2.1 DML语言 2.2 DDL语言 2.3 DCL语言 2.4 SQL语言注意事项 3、数据处理之查询 3.1 基本的 SELECT 语句 3.2 过滤数据 3.2.1 比较运算符 a、将表中第lien2 列中不等于90的列过滤掉 b、选择表中salary 列中小于3000的…

Give azure openai an encyclopedia of information

题意&#xff1a;给 Azure OpenAI 提供一部百科全书式的信息 问题背景&#xff1a; I am currently dabbling in the Azure OpenAI service. I want to take the default model and knowledge base and now add on to it my own unique information. So, for example, for mak…

GPU使用

0. 写这篇文章的背景 最近还是在使用GPU、连接远程服务器上出现了一点问题,发现在这方面的知识还是学得很模糊。(最让人感到困惑的是之前GPU的使用都没有问题) 总结一下最近的问题: 1.每一次连接远程服务器(选择的Ubuntu22.04),使用服务器的文件夹还好(关键是现在用…

uniapp js修改数组某个下标以外的所有值

推荐学习文档 golang应用级os框架&#xff0c;欢迎stargolang应用级os框架使用案例&#xff0c;欢迎star案例&#xff1a;基于golang开发的一款超有个性的旅游计划app经历golang实战大纲golang优秀开发常用开源库汇总想学习更多golang知识&#xff0c;这里有免费的golang学习笔…

《家庭无线网络覆盖项目》

家庭无线网络覆盖报项目 目录 家庭无线网络覆盖项目 家庭无线网络覆盖项目 一、项目概述 二、设备清单及报价 三、安装调试费用 四、总报价 五、服务承诺 家庭无线网络覆盖项目 客户姓名:[客户姓名] 联系方式:[电话号码] 家庭地址:[详细地址] 一、项目概述 为客户…

GlusterFS 分布式文件系统

一、GlusterFS 概述 1.1 什么是GlusterFS GlusterFS 是一个开源的分布式文件系统&#xff0c;它可以将多个存储服务器结合在一起&#xff0c;创建一个大的存储池&#xff0c;供客户端使用。它不需要单独的元数据服务器&#xff0c;这样可以提高系统的性能和可靠性。由于没有…