源码分析-java-AbstractList-Itr和ListItr的实现

时间:2022-12-28 09:04:28

AbstractList

API文档

AbstractList实现了List接口,又因为List继承自Collection,Collection继承自Iterable。因此List接口包含很多的方法。

AbstractList实现了List接口的最小实现。
他是针对随机访问储存数据的方式的,如果需要使用顺序访问储存数据方式的List还有一个AbstractSequentialList它是继承自AbstractList的子类,顺序访问时应该优先使用它。

对于不可修改的list,只需要覆盖get和size就可以

对于修改的list,还需要覆盖set,如果要求list大小可变还需要拓展add和remove方法

跟所有的Collection类一样,通常子类都需要实现无参和带参数的两个构造器

AbstractList是一个字节实现了iterator的方法。这和其他的抽象Collection类不同。并且实现了迭代器get、set、add和remove方法。

源码分析

AbstractList

protected AbstractList() {
}

首先构造器必然是protected。
如果是public有可能会被外界调用,虽然这个类是一个抽象类不可被调用构造器,但是从实现的目的上说AbstractList是为了编程人员实现一个List做一些铺垫工作,而不是直接用于构造对象的,因此不应该是protected方法。

当然也不可能是private,首先来说private修饰构造器过后这个类就没有办法被继承了,从这个角度说抽象类的构造器是不可以用private来修饰的。
private 通常来说用来做单例模式使用,具体搜索单例模式。

这样的情况下,希望子类可以实例化,protected是最好也是唯一的选择。
这里还需要说明的是java里面的构造器即使是protected的方法其子类却可以将其变成public方法。或者这种的说法不严谨,更严谨点的说法应该是基类的构造器和子类的构造并没有什么太大的关系。除非子类调用了super(),否则两者基本上是互相独立的。

我目前的认识是这样,如果有问题以后再修改

Itr

接下来是重头戏,也就是AbstractList实现的迭代器,但是在说迭代器之前需要说明一下:AbstractList的迭代器其实定义了两个迭代器,一个是实现了Iterator接口的迭代器Itr,另一个是实现了ListIterator并且继承自Itr的迭代器ListItr。
而ListItr实际上是继承自Itr的,这么一说就比较清晰了。迭代器的接口Itr实际上是公共的基本接口,就像Collection,它本身也有自己的继承体系,但实际官方只给出了两个子的继承子接口,一个是ListIterator一个是PrimitiveIterator。这里就见到一个。

另外有一点说明,我记得之前在看java核心技术的时候记得里面详细说明过java迭代器的实现方式,应该说的就是这里。如果希望看到更权威的说法可以参考下java核心技术。个人认为java核心技术对这里的描述还是比较彻底而清晰的。

我们先介绍基本的Itr实现:
Itr是实际上是一个内部类。主要来说有三个有意思的部分来说,从这三个部分也基本描述了迭代器的使用方式:

  1. 如何判断当前迭代器的位置,及如何移动?
  2. 如何对迭代的元素进行删除,及删除后的影响?
  3. 如何防止多个迭代器之间的相互影响?

1. 如何判断当前迭代器的位置,及如何移动?
java的迭代器和C++中的指针类型的迭代器有很大的不同。主要区别在于两大点。
一是java的迭代器是无法将随机访问的,也就是说不可以根据输入的数字来跳转到指定的位置,迭代器到达任何一个位置都需要经过next或者可能存在的previous方法实现,只能一个一个的移动。
二是迭代器并不是指向一个元素的,而是指向元素之间的。迭代器初始化之后再0号元素之前,迭代到尾之后是在最后一个元素之后,期间任何一点都是指向两个元素之间的位置。

因此在每次调用迭代器之前通常都需要进行查询即使用hasNext方法确定是否已经迭代到尾,如果迭代到尾就返回false。如果已经迭代到尾还强行的运行next则会抛出异常。

2. 如何对迭代的元素进行删除,及删除后的影响?
由于java的迭代器没有办法进行随机访问,因此如果需要删除元素也只能删除刚刚跳过的元素。这样也导致每次remove之前必定有一个next。如果连续进行两次remove则会抛出异常。因为并不知道删除的元素是哪一个。

3. 如何防止多个迭代器之间的相互影响?
在使用迭代器的时候,如果一个迭代器正在迭代器另一个迭代器修改了元素这样可能会导致很严重的问题。java采用一个计数器来保证不会有两个迭代器同时修改这个对象,具体的部分等到实现来说明。通常来说如果迭代器在进行访问时,另一个迭代器结构性的修改了对象,则正在访问的迭代器会抛出异常。但是如果是多个迭代器只进行读操作就可以。当然也可以自定义的修改成多个读迭代器和一个写迭代器。这样可以提高访问速率。但是这个功能在AbstractList类中并没有实现,而是只提供了基本的迭代器功能。


以上说明完了迭代器的使用方法,这里就开始讨论迭代器是如何实现的:

 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;

public boolean hasNext() {
return cursor != size();
}

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();
}
}

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();
}
}
protected transient int modCount = 0;

首先概述一下。这个类是private的内部类。因为是内部类因此拥有和内部变量相同的访问权限,对于AbstractList类的所有私有成员都有访问权限,更不用说protected和public,因此可以直接访问modCount而不需要加其他范围声明。而又因为是private所有所有变量都不需要加private等修饰词,因为无法从外部访问不需要用范围限定词。

1. 如何判断当前迭代器的位置,及如何移动?
首先对于每一个迭代器,保存了三个变量。一个是cursor,一个是lastRet,一个是expectedModCount。
cursor对于每次调用next方法就增加一次。并且将上一个cursor的值传给lastRet(用于可能的删除操作),将刚刚跳过的第i个元素作为返回值。这里得到刚刚跳过的元素的方法是使用了AbstractList类的get方法。

对于如何判断迭代到尾的hasNext方法,采用的是比较cursor的值和size的大小。如果两者相等则说明迭代到尾了。

2. 如何对迭代的元素进行删除,及删除后的影响?
上问说道了三个变量中lastRet,保存了每次跳过的元素的坐标,如果需要删除则只要调用remove就行。这里需要考虑的是为什么使用AbstractList.this.remove(lastRet);这样的句式。
这样的句式当然很好理解是使用的Abstract的remove方法。但是在使用而get的时候没有这样用。而且对于get方法并没有用这么复杂的句式。显然不可能是因为Itr类有自己的同名方法,因为方法的签名不一样。

一种可能的解释是:实际上来说AbstractList.this.remove才是标准的语法,但是为何这里使用但是get并没有使用,我才想可能是仅仅是因为这里不想给引起误解吧。但是在get和modCount 这些外部类的引用的时候不会引起误解,因此采用更为简洁的写法。但是内部类本身是一个比较trick的问题。很多问题我个人经验也不足,如果以后想到其他更为合理的解释再修改。

第二个需要说明的是程序在每次删除lastRet位的之前进行了判断,如果lastRet如果为-1,则抛出一个异常。
lastRet在两种情况下回变成-1,一个是删除完成后,一个是初始化的时候,两种情况也可以概括为一种情况,在调用remove之前没有执行next。

第三个小问题是还需要判断删除的迭代器是否是在cursor之前,如果是的话还需要将cursor减一。其实这个也很有意思。实际上这里应该是单向的迭代,为何还需要进行判断而不是直接减一。可能的解释是这里还有一个继承了Itr的ListItr,而它包含了向前迭代的可能,因此如何换成一个不包含向前迭代的实现就不需要判断了。因此这里的写法实际上是依赖于实现的。当然你可以选择在写ListItr方法的时候重写remove方法。不过后文可以看到这里的实现非常漂亮没有覆盖覆盖任何方法。

3. 如何防止多个迭代器之间的相互影响?
要实现多个迭代器之间的互不影响的访问,或者说如果有迭代器对对象进行了结构性的修改其他迭代器可以立即检测出问题并抛出异常(也就是所谓的fail-fast),这里采用的是这样的方式:
首先对每个需要迭代的对象(这里就是ArrayList对象)保存一个modCount的数字。这个数字在每次迭代器进行结构性修改的时候加1。换句话说,这个变量表示的是对象的修改次数,因此需要放在Itr类外面,AbstractList类里面。此外这个变量是transient的,也就说序列化的时候是不需要储存的。
其次对每个迭代器保存一个expectedModCount ,来记录这个迭代器对对象进行结构性修改的次数。这样每次迭代器进结构性修改的时候都将expectedModCount 和modCount进行对比,如果两种相等则说明没有其他迭代器修改了对象,可以进行。如果不相等则说明有迭代进行了修改,立刻抛出异常。

需要说明的是这里实现的迭代器只能进行多个读访问,如果适当修改可以实现多个读和一个写共存的情况,但是这里没有给出实现,如果以后看到了会补充进来。


ListItr

Itr看完了其实ListItr就没有什么特别需要说明的了。只是继承了继承了Itr,并且增加了几个方法个人认为并没有特别需要说明的。基本方法和Itr的方法类似。

但是有一个很有意思的问题是。为何在AbstractList中同时实现了Itr和ListItr。因为很显然Itr实现的是Iterator接口,按道理说应该在更早的Collection集合中实现。我自己考虑了两种可能的原因

第一个是实际上在整个java实现中抽象类里面,这里指直接继承自Collection类的几个抽象类中。AbstractList是唯一一个实现了Itr的类。其他的抽象类并没有这么复杂。因此如果在Collection类中实现了,那就无法使用private的内部类,这样可能会破坏封装性。但是如果不使用private就没办法使用ListItr继承Collection中的继承类。这样会很麻烦。

第二个问题是实际上Itr的实现还是依赖具体实现的。比如说如果在不可寻址的地方无法使用get或者remove删除指定的cursor,因此其他地方仅仅给出接口就行不需要具体实现,具体实现需要结合实际情况来看。

当然以上作为个人猜测。