1.问题引入
今天好哥们突然给我发来一条QQ消息,问迭代器能不能修改集合,用过迭代器的小伙伴应该都知道这一点:迭代器是用来统一对于List和Set集合的遍历方式的,至于能不能修改集合中的元素,讲真的,我没去思考过。不过当时为了快点回复哥们(其实是为了遮掩自己也不知道的惨状),我就建议他去看下Iterator的接口定义,随后悄咪咪的又学习了一波迭代器,于是便有了这篇博文。
2.迭代器回顾
2.1 迭代器是什么
- 可以先看下图,总览一下整个集合框架。
-
我们知道不同集合的底层实现是不一样的,比如ArrayList的底层是顺序表,LinkedList的底层是链表,HashSet的底层是hash表,它们的底层实现从根本上决定了它们自身的特性,其中就包括遍历的方式。具有索引的集合(比如List集合)可以采用for循环来遍历,但是由于Set集合不包含索引,所以Set集合就不能使用for循环来遍历。对不同集合遍历的前提是必须了解各个集合的底层实现,这使得集合与其遍历方式存在很强的耦合关系。
-
迭代器的存在就是为了解决上述强耦合的关系,它总是用同一种逻辑来遍历集合,使得用户自身不需要了解集合的内部结构,所有的内部状态都由Iterator来维护。也就是说,用户不用直接和集合进行打交道,而是控制Iterator向它发送向前向后的指令,就可以遍历集合。
-
我们看一下迭代器接口,可以看到迭代器中定义了四个方法:hasNext(),next(),remove(),forEachRemaining()。
2.2 集合框架是如何实现迭代器接口的
Iterator接口只是在顶层设计了规范,那么这些规范是如何实现的呢?这里以ArrayList为例子,看看它是如何实现迭代器接口的。
-
ArrayList的继承和实现的体系结构
-
从上图中我们并没有看到terator接口,但是看到了Iterable接口,那么这个Iterable接口又和Iterator接口有什么联系呢?到这里我的猜想是Iterable接口内部封装了Iterator接口,之所以这样猜想,是因为没有更加合理的地方来封装Iterator借口了。
-
如下图所示,我看了下Iterable的源码,发现其内部的确封装了Iterable接口。OK,我们离答案又近了一步,接下继续抽丝剥茧。
-
接下来的问题就是,哪个类又实现了Iterable接口?下面查看Collection接口、AbstractCollection接口、AbstractList接口,看看能不能找到Iterator的踪迹。
-
Collection接口继承了Iterable接口,没有对Iterator接口的实现或继承
-
List接口继承了Collection接口,同样没有对Iterator接口的实现或继承
-
AbstractCollection是一个抽象类,它继承了Collection接口,在它的一些方法里我看到了Iterator接口的方法hasNext()和next(),这说明我们离答案又进了一步。
-
分析到这里我遇到了一个问题:AbstractCollection里只是定义了一个返回值为Iterator的抽象方法,为什么AbstractCollection里面的方法就可以调用Iterator接口的方法了?
我的理解是这里的抽象方法虽然没有实现,但是在抽象类中依然可以调用这个抽象方法,把视角放在最终的实现类一切就都合理了。那么这个抽象方法在最终的实现类中是如何实现的呢?继续往下看。 -
再看一眼继承和实现的结构,我们下一步应该探究一下AbstractList了,那就一起来看一看吧。
-
AbstractList类继承了AbstractCollection抽象类,实现了List接口,它内部定义了5个成员类,具体啥作用后面再说。
AbstractList类实现了返回值为Iterator的抽象方法iterator(),它return了一个Itr对象,那么这个Itr对象又是什么呢,我们跳转到Itr处看它的具体定义
可以看到Itr实现了Iterator接口,到这里终于是看到Iterator接口本尊了,答案也就快水落石出了
Ltr的源码如下(截图的话篇幅不够,直接上源码,中文注释是自己家的):
private class Itr implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
int cursor = 0;
/**
* Index of element returned by most recent call to next or
* previous. Reset to -1 if this element is deleted by a call
* to remove.
*/
int lastRet = -1;
/**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount;
//实现hasNext()方法
public boolean hasNext() {
return cursor != size();
}
//实现next()方法
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
//实现remove()方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
- 到这里我们已经基本上解决了集合框架是如何实现Iterator接口的问题,但还有一个问题没有解决,就是到现在为止,我们知道了关键在于下图这个方法,实际上我们只需要用到Iterator,但是为什么要用Iterable接口?
先不急着说为什么,可以先看一下LinkedList中是如何实现的
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public E previous() {
checkForComodification();
try {
int i = cursor - 1;
E previous = get(i);
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.set(lastRet, e);
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
AbstractList.this.add(i, e);
lastRet = -1;
cursor = i + 1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
再来看一下HashSet是如何实现的
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
removeNode(p.hash, p.key, null, false, false);
expectedModCount = modCount;
}
}
通过上面的两个例子可以知道不同的集合对迭代器的实现也有不同的方法,Iterable接口对Iterator进行了封装,利用接口的多态将是实现的多样性隐藏起来,实际操作时只需调用一个接口就行,这实质上就是设计模式种的工厂模式的体现。
- 到了这里,对迭代器的底层源码也有了一个大概的掌握,后期有机会再研究研究具体的细节。
3. 迭代器应用实践
//一、List集合
List<String> list = new ArrayList<>();
list.add("henrly");
list.add("nancy");
list.add("lucy");
list.add("jeacy");
//遍历List集合
//1.使用for循环
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
//2.使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
//3.使用增强for循环
for (String l : list) {
System.out.println(l);
}
//二、Set集合
Set<Integer> set = new TreeSet<>();
set.add(111);
set.add(222);
set.add(333);
set.add(444);
//遍历Set集合
//Set集合无索引,所以无法使用for循环遍历
//1.使用迭代器
Iterator<Integer> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
//2.使用增强for循环
for (Integer i : set) {
System.out.println(i);
}
4. 能不能使用迭代器修改集合中的元素
- Iterator是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出java.util.ConcurrentModificationException异常。所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。
- 但你可以使用 Iterator 本身的方法 remove() 来删除对象,Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。
总结
通过对源码的解读,我对迭代器有了深入的了解。Java的源码不是凭空而出的,是这个行业的前辈们的设计并实现的,看了这些大佬们的代码,感觉自己现在的技术是真的菜,还有很大的提升空间,以后一定要多读源码,多动手。