从`ArrayList`中了解Java的迭代器
目录
什么是迭代器
迭代器的设计意义
ArrayList
对迭代器的实现增强for循环和迭代器
参考链接
什么是迭代器
Java中的迭代器——Iterator
是一个位于java.util
包下的接口,这个接口里声明了三个重要的方法hasNext()
、next()
、remove()
。下面看看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());
}
}
值得提一下的是remove()
方法,remove()
方法在接口中使用了default
关键字修饰,这是JDK1.8的一个新特性——接口的方法可以拥有方法体(即默认实现)。当你使用一个类去实现Iterator
的时候,如果你不去重写它的remove()
方法的话,再调用remove()
就会去调用接口里的默认实现,此时程序会抛出UnsupportedOperationException
异常。
设计迭代器的意义
首先,迭代器这个东西是针对集合设计的。我们知道,List家族,Set家族,Map家族他们的底层设计都是不一样的,那么在遍历它们的时候,方式也自然不会一样,既然大家都有遍历的需求,那么就可以把这个需求统一一下,设计一个接口来把这些方法抽象出来,于是Iterator
这个接口就应运而出。迭代器的设计初衷是:提供一种方法对一个容器对象中的各个元素进行访问,而又不暴露该对象容器的内部细节。
ArrayList
对迭代器的实现
很多集合都实现了Iterator
接口,实现了属于自己的迭代器,在本篇文章中我们以ArrayList
中的迭代器为例讲述ArrayList
是如何实现自己的迭代器的。
基本使用
想一想,你平时在使用迭代器去遍历ArrayList
的时候是怎么做的,是不是像下面这样
// 首先有个ArrayList
ArrayList<Object> list = new ArrayList<>();
// 然后获取到迭代器
Iterator<Object> iterator = list.iterator();
// 再开始遍历ArrayList,使用hasNext()判断ArrayList中是否有下一个元素
while (iterator.hasNext()){
// 如果ArrayList中有下一个元素,就使用next()方法获取下一个元素
Object e = iterator.next();
if ("条件".equals(e)){
// 如果元素e满足某种条件就使用remove()删除该元素
iterator.remove();
}
}
Iterable
接口
看看下面获取迭代器的这段代码
// 获取迭代器
Iterator<Object> iterator = list.iterator();
可以看到 list.iterator()
给我们返回了一个迭代器对象,那么这个iterator()
方法是个什么样的方法呢,通过查看源码可以发现,iterator()
方法是来自于接口Iterable
的一个方法,这个Iterable
的声明是这样的
public interface Iterable<T> {
Iterator<T> iterator();
// 其他方法...
}
其中,iterator()
方法返回的就是一个Iterator
迭代器。实现了Iterable
接口的类我们称它为可迭代的类,该类的对象我们称之为可迭代对象。
Itr
类及其和接口Iterator
的关系
我们接着再看看iterator()
方法的源码
public Iterator<E> iterator() {
return new Itr();
}
你会发现发现iterator()
方法只是给我们new了一个Itr
类的对象,然后就把这个对象的引用返回给我们了。那我们就顺藤摸瓜看看Itr()
这个构造方法,看看这个Itr
类到底是个啥玩意。
通过查看Itr
类的源码我们可以发现Itr
类是ArrayList
中的一个私有的内部类,并且这个类实现了前面说的Iterator
接口,下面看下Itr
类的具体源码
private class Itr implements Iterator<E> {
int cursor; // 下一个要返回的元素的索引
int lastRet = -1; // 返回的最后一个元素的索引;如果没有这个值就是-1
int expectedModCount = modCount; // 期望ArrayList的结构被改变(新增,删除元素都会改变ArrayList的结构)的次数,初始值等于modCount
// 构造方法
Itr() {}
// hasNext方法的具体实现
public boolean hasNext() {
return cursor != size;
}
// next方法的具体实现
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
// remove方法的具体实现
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
// ...
}
// 检查在使用迭代器遍历ArrayList的同时是否有其他的操作改动了ArrayList的结构
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
下面来逐一解释Itr
里的成员变量和方法
Itr
类中的成员变量
cursor
int cursor; // 下一个要返回的元素的索引
由于cursor
是一个int
类型的成员变量,所以他的初始值是0,也就是说,一开始,下一个要返回的元素的索引是0,实际上就是ArrayList
的第一个元素。
lastRet
int lastRet = -1; // 返回的最后一个元素的索引;如果没有这个值就是-1
lastRet
这个变量存的是迭代器上一个返回的元素的索引,一开始这个值是-1,也就是说还没有使用next()
方法返回ArrayList
的下一个元素时,这个值就是-1,当我们调用next方法返回ArrayList
的下一个元素后,可以从next()
方法的源码里看到cursor
会+1,然后lastRet
被赋值为cursor
加一之前的值,所以lastRet
里存的就是上一个返回的元素的索引。
expectedModCount
// 期望ArrayList的结构被改变(新增,删除元素都会改变ArrayList的结构)的次数,初始值等于modCount
int expectedModCount = modCount;
expectedModCount
从字面意思上来看,表示期望被改变的次数,然后它的默认值是modCount
。这个modCount
是何许人也?我们点进去看看,原来它是ArrayList
的父类AbstractList
的成员变量,它长这样
protected transient int modCount = 0;
modCount
它记录了整个List的结构被改变的次数,我们常用的add()方法,remove()方法都会改动List的结构,相应的,这些方法被调用一次后,modCount
就会+1,用以记录List的结构被改变的次数。
Itr
类中的方法
hasNext()
public boolean hasNext() {
return cursor != size;
}
hasNext()
的实现其实很简单,就是判断下一个元素的索引是否和ArrayList
的大小相等。不相等的话,就说明ArrayList
中还有下一个元素相等的话,就说明遍历已经走到尽头了,ArrayList
中没有下一个元素了。
checkForComodification()
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
checkForComodification()
方法的作用就是检查迭代器对ArrayList
进行遍历的过程中是否有其他的操作对ArrayList
本身的结构进行了修改,要知道这种修改操作是不被允许的,如果你这么做了,会导致迭代器在遍历的时候获取或者删除的元素并不是你想要的那个元素,甚至会发生索引越界的现象。那么设计者是如何避免这种不当的情况呢?这就要用到前面说到的expectedModCount
和modCount
这两个变量了,还记得吗,在new一个Itr
对象的时候,expectedModCount
的初始值就是modCount
的值,前面说了,modCount
记录了ArrayList
本身结构被改变的次数,那么只要在我们使用迭代器遍历ArrayList
的时候,这个值没有发生变化,这说明这期间没有其他的操作来改变ArrayList
的结构(比如删除、新增元素),迭代器的这次遍历是安全的。但是与之相反的,在迭代器遍历ArrayList
期间,modCount
的值发生了变化(有其他的操作改变了ArrayList
的结构),modCount
的值不等于expectedModCount
的值了,这就说明迭代器的这次遍历是不安全,这次遍历应该被停止掉。于是,当modCount != expectedModCount
的时候,程序抛出了一个ConcurrentModificationException
异常,终止掉了迭代器的这次遍历。
next()
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
梳理一下next()
方法的执行过程:
进方法之后,调用
checkForComodification()
方法检查下本次遍历是否是安全的,如果是安全的,执行第2步,否则checkForComodification()
方法里会抛出ConcurrentModificationException
异常,阻止这次遍历的进行声明一个局部变量
i
,i
的值就是cursor
的值判断
i
(cursor
)的值是不是大于或者等于ArrayList
的大小,如果是的,就表明迭代器已经遍历完了整个ArrayList
了,按道理来说,不应该再调用next()
方法获取下一个值了(因为已经没有下一个值了),于是这种情况下,程序就抛出了一个NoSuchElementException
异常。从这里也可以看到,对于一个已经被迭代器遍历完的集合,如果你还试图去获取下一个元素,程序会抛出NoSuchElementException
异常。声明了一个Object[]类型的成员变量
elementData
,这个数组实际上指向了ArrayList
内部用来存储元素的数组,就是下面这个transient Object[] elementData; // 非私有以简化嵌套类访问
判断
cursor
的值是否大于elementData
的大小,如果是,抛出ConcurrentModificationException
异常cursor
的值+1将
lastRet
的值设置为i
的值返回
ArrayList
的索引为i
的元素
remove()
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
remove()
方法的过程没啥好说的,值得注意的是:
remove()
本质上调用的是ArrayList
的remove(int index)
方法- 调用完
ArrayList
的remove(int index)
方法之后,modCount
的值会发生变化(+1),这时再将+1后的modCount
赋值给expectedModCount
以保证接下来的遍历顺利进行下去。
总结:在试用迭代器遍历集合时,如果想要删除集合中的元素,必须使用迭代器提供的remove()
方法,否则程序将抛出ConcurrentModificationException
异常。
增强for循环和迭代器
背景
增强for循环是JDK1.5以后出来的新特性,是一种高级的for循环,用来方便遍历数组和集合的。
遍历数组
下面先用增强for循环遍历下数组看下,看下示例代码
public static void testArray(){
String[] s = {"a", "b", "c", "d"};
for (String e : s) {
System.out.println(e);
}
}
输出结果和普通for循环是一样的
a
b
c
d
让我们来看下上面这段示例代码的反编译结果,看看编译器实际执行的代码是怎么样的
public static void testArray() {
String[] s = new String[]{"a", "b", "c", "d"};
String[] var1 = s;
int var2 = s.length;
for(int var3 = 0; var3 < var2; ++var3) {
String e = var1[var3];
System.out.println(e);
}
}
可以很明显的看到,如果你是用增强for循环来遍历数组的话,实际上编译器执行的代码还是使用普通for循环来遍历这个数组。
遍历可迭代对象
这里仍然以ArrayList
举例说明,我们看下使用增强for循环来遍历可迭代对象的示例代码
public static void testList(){
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
for (String e : list) {
System.out.println(e);
}
}
这段代码的输出结果同上
a
b
c
d
同样的,让我们来看看这个示例代码的反编译结果
public static void testList() {
List<String> list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
Iterator var1 = list.iterator();
while(var1.hasNext()) {
String e = (String)var1.next();
System.out.println(e);
}
}
可以很明显的看到,当增强for循环遍历的是一个可迭代的对象时,实际上是使用迭代器来遍历这个可迭代对象的。
使用总结
从上面的两个示例代码的反编译结果来看,我们不难得出下面这几个结论
- 当增强for循环遍历的是一个数组的时候,实际上还是使用的普通for循环来遍历
- 当增强for循环遍历的是一个可迭代的对象的时候,实际上使用的是迭代器来遍历这个对象
- 在增强for循环遍历可迭代对象的时候,不可以执行删除元素的操作,否则程序会抛出
java.util.ConcurrentModificationException
异常 - 增强for循环遍一个对象的时候,这个对象不可以为
null
,否则程序会抛出NullPointerException
异常
参考链接
[迭代器]https://www.cnblogs.com/zhuyeshen/p/10956822.html
[迭代器]https://www.cnblogs.com/zyuze/p/7726582.html
[增强for循环]https://blog.csdn.net/baidu_25310663/article/details/80222534
[增强for循环]https://www.cnblogs.com/miaoxingren/p/9413320.html
从`ArrayList`中了解Java的迭代器的更多相关文章
- java——ArrayList中常见方法用法
package com.xt.list; import java.util.ArrayList; import java.util.Iterator; import java.util.List; p ...
- 设计模式 - 迭代器模式详解及其在ArrayList中的应用
基本介绍 迭代器模式(Iterator Pattern)是 Java 中使用最多的一种模式,它可以顺序的访问容器中的元素,但不需要知道容器的内部细节 模式结构 Iterator(抽象迭代器):定义遍历 ...
- Java ArrayList中对象的排序 (Comparable VS Comparator)
我们通常使用Collections.sort()方法来对一个简单的数据列表排序.但是当ArrayList是由自定义对象组成的,就需要使用comparable或者comparator接口了.在使用这两者 ...
- 如何使用 Java 删除 ArrayList 中的重复元素
如何使用 Java 删除 ArrayList 中的重复元素 (How to Remove Duplicates from ArrayList in Java) Given an ArrayList w ...
- Java删除ArrayList中的重复元素
Java删除ArrayList中的重复元素的2种方法 ArrayList是Java中最常用的集合类型之一.它允许灵活添加多个null元素,重复的元素,并保持元素的插入顺序.在编码时我们经常会遇到那种必 ...
- ArrayList中重复元素处理方法.[Java]
1.使用HashSet删除ArrayList中重复的元素 private static void sortByHashSet() { ArrayList<String> listWithD ...
- Java中list集合ArrayList 中contains包含的使用
Java中list集合ArrayList 中contains包含的使用 https://blog.csdn.net/qq_38556611/article/details/78774690
- Java之——删除ArrayList中的反复元素的2种方法
转载请注明出处:http://blog.csdn.net/l1028386804/article/details/47414935 ArrayList是Java中最经常使用的集合类型之中的一个.它同意 ...
- 面试官:如何在Integer类型的ArrayList中同时添加String、Character、Boolean等类型的数据? | Java反射高级应用
原文链接:原文来自公众号:C you again,欢迎关注! 1.问题描述 "如何在Integer类型的ArrayList中同时添加String.Character.Boolean等 ...
随机推荐
- Springboot:员工管理之首页(十(2))
访问首页可以通过两种方式: 1:编写controller 2:自定义扩展视图解析器(推荐使用) 1:编写Controller com\springboot\controller\IndexContro ...
- 最全的 API 接口集合
对于程序员来说,为自己的程序选择一些合适的API并不是那么简单,有时候还会把你搞得够呛,今天猿妹要和大家分享一个开源项目,这个项目汇集了各种开发的api,涵盖了音乐.新闻.书籍.日历等,无论你是从事W ...
- vue显示富文本
来源:https://segmentfault.com/q/1010000013952512 用 v-html 属性解决
- Linux安全实验缓冲区溢出
缓冲区溢出实验: 缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况.这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片段.这一漏洞的出现是由于数据缓冲器和返回地址的暂时关 ...
- thymeleaf 模板语法
模板语法 如何在 script 标签体内部使用 th 获取后端数据 添加如下属性 <script type="text/javascript" th:inline=" ...
- Android:RelativeLayout 内容居中
Android RelativeLayout 内容居中解决办法: 使用Linearlayout本来利用父控件的gravity属性是很好解决的.但是对应RelativeLayout虽然有gravity属 ...
- java中interrupt,interrupted和isInterrupted的区别
文章目录 isInterrupted interrupted interrupt java中interrupt,interrupted和isInterrupted的区别 前面的文章我们讲到了调用int ...
- office 365 激活
将以下代码复制到记事本 @echo off title Activate Microsoft Office ALL versions &echo - Microsoft Office Prof ...
- 【Linux常见命令】alias命令
alias命令用于查看和设置指令的别名. 用户可利用alias,自定指令的别名. 若仅输入alias,则可列出目前所有的别名设置. alias的效力仅及于该次登入的操作.若要每次登入是即自动设好别名, ...
- Netty(三):IdleStateHandler源码解析
IdleStateHandler是Netty为我们提供的检测连接有效性的处理器,一共有读空闲,写空闲,读/写空闲三种监测机制. 将其添加到我们的ChannelPipline中,便可以用来检测空闲. 先 ...