JDK源码剖析之Iterator
Posted 58招聘技术团队
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK源码剖析之Iterator相关的知识,希望对你有一定的参考价值。
对于经常使用集合的同学来说Iterator接口并不陌生,我们经常用它来遍历对象 。本文将从源码角度对其做简单介绍(PS:限于篇幅,将重点以ArrayList为例进行说明)
在jdk中Iterator接口定义了标准化访问集合中对象的方法,用于对Collection中元素进行迭代,避免了向使用者暴露集合内部结构。这种方法已被抽象为迭代器模式:提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。通过迭代器模式可以实现容器类与外部类的功能分离,简化容器接口,方便了调用者以通用的方法去遍历容器内元素。下面我们先来介绍下迭代器的接口定义。
Iterator接口定义了一个返回Iterator对象的方法
Collection接口实现了Iterable接口,故所有实现了Collection接口的容器类(集合)都可以通过对象名.iterator()的方法获取该collection的iterator(迭代器)对象
在Map中同样提供了通过Collection获取迭代器的方法,以便对键值对的值域进行操作。
例如:
Collection<Object>values=hashMap.values();
Iterator<Object> itr=values.iterator();
Iterator接口作为Iterable接口的返回值,定义了如下三个方法:
E next(): 返回游标右边的元素并将游标移至下一个位置
void remove():删除游标左边的元素
ListIterator是list集合对Iterator接口的增强版实现。除了Iterator的三个基本方法外,还增加了双向移动遍历,以及在迭代过程中添加修改元素的方法。在初始化ListIterator时,还可以提供一个index参数,即可实现从任何位置对元素的迭代。
在ArrayList中iterator方法返回了一个Itr对象,是一个实现了Iterator接口的内部类
private class Itr implements Iterator<E>{
int cursor = 0;
int lastRet = -1;
int expectedModCount= modCount;
}
在Itr内部定义了三个int型的变量:cursor、lastRet、expectedModCount。其中cursor表示下一个元素的索引位置,初始值为0,即第一个元素。lastRet表示上一个元素的索引位置,所以lastRet的值永远比cursor值少1.在内部类实例化时,会将modCount的值赋给expetedModCount,modCount用来表示list被结构性修改的次数。所谓结构性修改是指,改变了list的size,或者在迭代过程中产生了不正确的结果。
hasNext()方法通过判断游标值是否等于集合大小来判断是否还有下一个迭代的元素
next()方法则通过游标值获取数组对应值,即迭代返回的下一个值,并将游标移至下一位。方法开始时,首先调用checkForComodification方法,检查集合在迭代过程中是否被修改。然后判断游标值是否大于等于数组大小,是的话抛出异常。接下来获取当前数组,再次判断游标是否大于等于数组大小,是的话抛出同样抛出ConcurrentModificationException异常。
remove()方法调用ArrayList的remove()方法删除lastRet位置元素,并修改expectedModCount。首先会检查lastRet的值。而从上面的next()方法结束时,都会将lastRet值赋值为前一元素下标,只有在执行完add和remove方法后会将值赋为-1,所以在增删集合元素时,对同一个元素多次执行remove或add操作,会引发IllegalStateExcepion。
在ArrayList中的内部类ListItr,实现了比ListIterator更加丰富的迭代功能。
previous()方法返回游标左边位置对应数组元素,并修改游标值
set()方法将lastRet位置对应的数组值修改为新的元素
add()方法用于添加元素,通过将expectedModCount同步,避免抛出ConcurrentModifException异常。
ConcurrentModificationException作为集合遍历时的常见异常,经常出现在iterator调用remove()或hasNext()方法时用到的checkForComodification ()方法里
该方法用于判断集合的修改次数是否合法,即判断遍历过程中是否被修改过。若modCount不等于expecModCount,则抛出ConcurrentModificationException
下面以ArrayList为例,说明其产生的原因。
从上面的案例可以看到,在迭代过程中调用ArrayList.remove()方法删除元素时,程序会抛出ConcurrentModificationException。而通过迭代器的remove()方法则不会抛出异常,并正常将元素删除。
通过查看Arraylist的remove()方法,可以看到在方法中修改了modCount,modCount用于记录集合被修改的次数,而迭代器中的expectedModCount仍保持不变,当集合的修改次数和迭代器的修改次数不一致时,抛出ConcurrentModificationException。而通过迭代器在迭代过程中修改集合时,会将两者保持一致,避免了异常的抛出。因此在对集合遍历时,只能通过迭代器对集合进行修改。
除了单线程操作外,当多个线程同时对集合进行结构性改变时,例如一个线程通过Iterator遍历集合中元素,另一个线程在某个时候修改了集合的结构,也有可能抛出异常。这种错误检查机制被称为fail-fast快速失败。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。快速失败机制保证了错误在迭代之前及时抛出,避免了更大程度的异常。想要避免fail-fast,有两种解决方案:
1)在所有可能改变modCount的方法上加锁,或使用Collections.synchronizedList加同步锁
2)使用CopyOnWriteArrayList代替ArrayList。CopyOnWrite容器为写时复制容器,以其add方法为例,当添加元素时,不直接往容器里加,而是将当前容器复制,往新的容器中添加,完成后将引用指向新的容器。从而实现对容器多并发的读取。
在CopyOnWriteArrayList的iterator的hasNext()方法中,可以看到其并未调用checkForComodification方法,自然也就不会引发快速失败机制
LinkedList底层数据结构是基于双向循环链表的,通过节点存储当前节点信息以及前一个和后一个节点的引用。在LinkedList的Iterator同样是由一个内部类ListItr继承了ListIterator,由于LinkedList结构的不同,内部类实现也有所区别,包括四个属性值
HashMap是基于哈希表的 Map 接口的实现,HashMap的底层主要是基于数组加链表来实现的。在HashIterator中保存了当前访问和Entry和下一个Entry。在初始化时,先找到一个不为空的Entry作为next()返回的值。
hasNext()方法通过判断next指向的Entry是否为空判断是否还有未访问的元素。nextEntry方法返回下一个不为空的Entry。若当前链表已访问完成,则去寻找下个数组中不为iy空的Entry
在HashIterator中并没有实现next方法,而是由继承类来实现。
HashTable与HashMap类似,都是基于链表+数组实现的基于键值对的集合,不同的是HashTable不允许插入null作为键,并且HashTable是线程安全的,而HashMap不是线程安全的。在HashTable中同样可以通过iterator方法实现元素的迭代,不同的是HashTable中还实现了Enumeration方法,通过该接口中的hasMoreElements()和nextElement()方法来实现对下一个元素的访问,其他方法与HashMap类似。
HashSet是基于 HashMap 实现的,其底层采用 HashMap 来保存所有元素,当添加元素时,将该元素作为Key插入HashMap中(value值和Key相同),保证了集合的不重复。在调用HashIterator方法时,调用hashmap的keySet.iterator()方法返回hashmap的key,即该set的value。
LinkedHashSet是继承于HashSet的子类,其Iterator方法与HashSet类似。
通过这段时间对JDK源码Iterator的阅读,了解其在各个容器类中的实现,也对各个集合类有了更加深入的理解。今后也会继续深入阅读源码,掌握其中精妙的地方并运用到实际中。这个文档也是基于我的一点理解写的,有不周全的地方欢迎指正,共同学习。
以上是关于JDK源码剖析之Iterator的主要内容,如果未能解决你的问题,请参考以下文章
java集合框架源码剖析系列java源码剖析之TreeMap
Java集合源码剖析——基于JDK1.8中HashSetLinkedHashSet的实现原理
STL源码剖析——iterators与trait编程#4 iterator源码