一家之言 姑妄言之 絮絮叨叨 不足为训
笔者废话:
这篇文章是ArrayList源码逐条解析外述篇。为什么来个外述篇呢?因为:
1. 这个类作为ArrayList
的迭代方式是非常重要的;
2. 我是实在不想在“ArrayList的遍历功能解析”中解析这个类了,本身它是非常重要的,如果不单拿出来讲而是放在ArrayList源码逐条解析这个文章里解析其实会给人造成误解认为其不重要;
3. 公司里的领导告诉我源码分析写的过于长可能影响观感。
所以,我这里把这个类单拿出来进行解析(>ω<)。
ArrayList-ListItr类注释翻译:
一个优化版的AbstractList.ListItr
ArrayList-ListItr类信息:
private class ListItr extends Itr implements ListIterator<E>
我们可以清楚的看到,ListItr
是一个继承了Itr
类,并且实现了Iterator
接口的类。起码我们从这里能看出这个ListItr
是一个具有遍历集合属性的Itr
类子类,同时也是Iterator
接口实现类。
那么关于Itr
类,也就是咱们本类的父类解析,可以参考ArrayList-Itr源码逐条解析这篇文章。而关于Iterator
接口的解析我们已经在Iterator源码逐条解析里面介绍过了。想要详细了解的话,可以查看这篇Iterator源码逐条解析文章。
ArrayList-ListItr构造函数信息:
ListItr(int index) {
super();
cursor = index;
}
我们从这个构造函数来看,发现这个是一个有参构造器。其中会传入一个索引值index
。不过,这里我们会看到第一行会调用父类的构造函数,也就是Itr
类。而下方的这个游标cursor
也是父类的属性。
另外,我们还会发现,这个ListItr
类的构造函数有且只有一个有参构造器,所以,我们在创建ListItr
类时会主动向内传入一个索引index
。不过放心,这一步并不用我们去创建,这一步已经被封装进ArrayList
类中的listIterator(int index)
方法和listIterator()
方法中了。
ArrayList-ListItr成员变量信息:
这里的成员变量信息都继承自了父类Itr
类的成员变量,所以如果有疑问,那么你可以复习一下ArrayList-Itr源码逐条解析这篇文章里面所介绍的。
ArrayList-ListItr的方法解析:
好,我们现在开始进入正题了,别忘了,这个ListItr
类不光继承了Itr
类,而且还是对接口ListIterator
的具体实现。具体的情形可先阅读ListIterator源码逐条解析倒数第二段。
不过,这里还需要说明的是,因为本类继承了Itr
类,所以其中的hasNext()
方法、next()
方法、remove()
方法在这里都没有进行覆写。而这里出现的则是覆写了ListIterator
接口中的方法。
public boolean hasPrevious() {
return cursor != 0;
}
这里覆写了ListIterator
接口中的hasPrevious()
。我们知道这个方法是在询问当前遍历的容器中是否含有上一个元素,那么我们看具体的实现是如何呢?
cursor != 0
,对,就是这样。它在判断我们的cursor
游标是否不等于当前数组内第一个元素的索引值0
。因为如果等于了0
不就代表我们这个游标指向已经指向了第一个元素了吗?那么它还会有前一个元素吗?肯定不会的。
**千万记住,这里的hasPrevious()
方法调用完后,游标指向cursor
是不会移动的
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
这里我们一同介绍上述两个方法。它们覆写了ListIterator
接口中的nextIndex()
和previousIndex()
。这两个方法的作用分别是返回当前下一个未遍历元素的索引index
和返回当前下一个未遍历元素的上一个元素的索引index
。
通俗意义上来说,从代码中我们发现nextIndex()
方法返回当前游标所处的索引,而previousIndex()
方法则返回当前最后一次已遍历出的元素的索引。
我们来举个例子:
/**
* 放心运行
*/
public static void main(String[] args) {
LinkedList<Integer> arrayList = new LinkedList<Integer>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
ListIterator listIterator = arrayList.listIterator();
System.out.println("当前遍历出的元素: " + listIterator.next());
System.out.println("下一个元素的索引: " + listIterator.nextIndex());
System.out.println("上一个元素的索引: " + listIterator.previousIndex());
}
/*
* 输出结果:
* 当前遍历出的元素: 1
* 下一个元素的索引: 1
* 上一个元素的索引: 0
*/
我们从这个例子可以看出,当我们遍历出第一个元素的时(1),这个时候,游标cursor
右移,指向了索引1
。也就是说cursor = 1
。而这个索引1
指向的元素是数字2。
这个时候我们调用nextIndex()
方法,返回当前游标cursor
就会返回索引1
。然而,这个索引1
所代表的元素2我们还没有进行遍历。
那么我们调用previousIndex()
方法,返回当前游标cursor - 1
就会返回索引0
。而这个索引0
所代表的则是我们刚刚遍历出的元素1。
这也印证了我们之前说的,nextIndex()
方法返回当前下一个未遍历元素的索引index
,previousIndex()
返回当前最后一次已遍历出的元素的索引index
。
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
这里是迭代的重要步骤,这个previous()
才是真正可以取出元素的方法。它与next()
方法的含义是相反的。next()
方法是遍历下一个,而我们的previous()
方法则是遍历上一个。我们经过了之前的hasPrevious()
方法判断是否含有上一个元素之后,就可以紧接着调用该方法。
我们来看这个方法的具体实现。
第一步,先利用checkForComodification()
方法对修改值modCount
进行检查,判断当前数组是否发生修改。
第二步,声明一个局部变量i
,并将当前游标值的前一位cursor -1
赋予它(其实这里就是游标本身的左侧一位,我们接下来描述的时候就用“游标”这个概念)。
笔者废话:
我们可以试想一下,next()
是将cursor
赋予i
是因为我们需要遍历的是下一个元素,也就是进行移位后的cursor
所指向的那个元素。而previous()
需要遍历的是游标cursor
指向的上一个元素,所以这里用cursor -1
赋予i
是逻辑通畅的。不过这里也体现了阅读源码的重要性,这个previous()
方法其实返回就是你最后一次已经遍历出来的那个元素。
第三步,判断游标是否小于当前数组第一个元素的索引0
,如果符合判断条件,则抛出NoSuchElementException
异常。这点还是易于理解的,之前我们说过,当这个游标cursor
的值等于了当前数组第一个元素的索引0
的话就已经代表我们遍历到了数组的第一个元素,何况还要cursor - 1
。这样就会遍历第一个元素的左侧那个元素。有吗?肯定没有的。因为这里数组脚标越界了,所以这个时候抛出异常就不足为奇了。
第四步,是将当前ArrayList
的数组赋予一个新的类型为Object
的数组,其实就是把当前的数组复制了一份;
第五步,到了这里,你看,又一次把游标进行判断,判断什么呢?判断我们的游标是否大于或等于了数组的容量length
,如果符合判断条件,则抛出ConcurrentModificationException
异常。看,我们这里又有一个熟悉的异常,那么为什么游标大于或等于了数组的容量length
就会抛出这个异常呢?其实,它还是在做检测,检测集合是否被修改过。
笔者废话:
到这里你仔细想想,我们假设元素个数size
就是数组长度length
,也就是说这个数组已经被填满了。但是这个时候,你通过了第三步的数组脚标越界的判断,但是没有通过第五步的容量判断,也就是发生了这种场景:length <= i > 0
。这种情况可能吗?数组容量小于了当前的索引位置?这种情况有可能,就是你删除了元素,然后你的数组缩容了。说的直白一些当你remove()
完毕后你又调用了类似于trimToSize()
的缩容方法。这可不就是修改了集合元素嘛~所以,这个时候抛出ConcurrentModificationException
异常是一种非常正确的行为。
第六步,这个时候,该判断的也都判断了,我们可以正常的取出元素返回给调用者了。不过这里我们需要开始最重要的一步了,挪动我们的游标卡尺。形如代码所说的那样,当前游标变为i
即可。也就是这个游标指向向左移动一格。
笔者废话:
这里可能有些绕。也就是游标左移cursor - 1
我能明白,但是这个当前游标cursor
也左移是为了什么?其实这里ListIterator源码逐条解析已经解释的很清楚了。next()
方法和previous()
方法上的注释说的很清楚了,如果这两个方法交替使用,将返回同一个元素。如何达到这种目的呢?那就是当我们进行了previous()
方法返回了最后一次遍历的那个元素后,只有当前的cursor
也进行一次左移,才会在下一次next()
方法返回这个相同的元素。形似这样:
1 | 2 | 3 | 4 | 5 |
---|
当我们需要遍历上述的元素时,如果调用了两次next()
方法,这个时候会返回给我们数字2。而当前游标cursor
则如下指向:
1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|
cursor | ^ |
而当我们调用一次previous()
方法,这个时候会返回最后一次遍历的元素2,
这个毋庸置疑,但是这个时候我们上方代码的i
,也就是cursor - 1
指向了哪里呢?指向了如下的位置:
1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|
i | ^ | ||||
cursor | ^ |
那么这个时候,如果我们还想利用ListIterator
接口所谓的返回相同元素的特性,你觉得我们在调用next()
方法的时候这个游标cursor
会指向哪里呢?那么只有下面这样,才会在调用next()
方法的时候返回previous()
返回的那个元素。也就是所谓“相同的元素”:
1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|
i | ^ | ||||
cursor | ^ |
到这里你看,是不是需要执行cursor = i
这个操作很有必要呢?答案是肯定的。
第七步,返回所遍历的元素,返回哪个呢?当然返回我们之前想要遍历的那个游标所在处左侧的元素,也就是那个i
,也就是返回数组当前i
索引处的元素。
但是,这里又碰到了一个问题,那就是在next()
方法内,返回操作中的步骤是lastRet = i
。而这里的previous()
方法内的返回操作中也是lastRet = i
。细心的人会觉得这里的lastRet
难道不是代表游标cursor
左侧的那个元素吗?返回i
处的元素是没有问题的,但是你这里的lastRet
和上面分析得出的cursor
不就重叠了吗?
能想到这里我真的非常高兴!!!我也不卖关子了,直接说结论:这里的lastRet = i
操作,是为了remove()
和set(E e)
这个方法的。当然,我们这样指出来可能是不对的,但是通过源码,这里的两个方法都依靠着lastRet
这个值进行处理,所以我这里提出的观点可能不是很对,但是当你面试的时候说出了这个要更好一些~
那么我们仔细想一下remove()
方法删除的是哪个位置的元素?是lastRet
这个位置的元素。千万不要忘了,ListIterator
接口中对remove()
方法的定义:
从列表中删除next或previous返回的最后一个元素
所以,你应该就明白了,这个lastRet = i
操作是必须的,因为当我想要删除由previous()
方法返回来的这个元素的时候,我的lastRet
必须指向这个元素(*•̀ᴗ•́*)و ̑̑
另外,还有这个set(E e)
方法,但是因为在下面我们会解析到这个源码,所以这里就不做解释。看我的解析就可以了~
到此,previous()
方法解析完成~
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
我们接下来的这个方法也是覆写了ListIterator
接口中的set(E e)
方法。这个方法我们在ListIterator源码逐条解析中简单介绍过。其目的是为了修改调用next()
方法和previous()
方法返回的元素。我们一会儿举例,先来分析源码:
第一步,首先会判断我们的lastRet
是否小于0
。如果小于的话就抛出IllegalStateException
异常。这步比较好理解,你都小于0
了我修改谁去啊?不过正经的说,根据这个方法的本意是为了修改调用next()
方法和previous()
方法返回的元素。那么针对于next()
方法是修改游标cursor
的前一项,即lastRet
指向的那一项。而针对于previous()
方法则是修改当前cursor
指向的那一项,其实还是lastRet
指向的那一项(这一点参考previous()
方法解析)。
第二步,利用checkForComodification()
方法对修改值modCount
进行检查,判断当前数组是否发生修改。
第三步,利用ArrayList
类本身的set(int index, E element)
方法对我们的元素进行修改。你看,这里传入的第一个索引值就是我们的lastRet
,也就是说修改的就是这个lastRet
指向的那一项。当然,这里如果出现了异常,则抛出ConcurrentModificationException
异常。
我们来举两个例子:
情形一:next()
方法的调用:
/**
* 正确实例,放心运行
*/
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
ListIterator listIterator = arrayList.listIterator();
listIterator.next();
listIterator.next();
listIterator.set(10);
System.out.println(arrayList);
}
/*
* 输出结果:
* [1, 10, 3, 4, 5]
*/
情形二:previous()
方法的调用:
/**
* 正确实例,放心运行
*/
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
ListIterator listIterator = arrayList.listIterator();
listIterator.next();
listIterator.next();
listIterator.previous();
listIterator.set(10);
System.out.println(arrayList);
}
/*
* 输出结果:
* [1, 10, 3, 4, 5]
*/
我们发现这两个示例的输出结果都一样,其实这个正常的。在情形一中,我们调用了两次next()
方法之后,游标cursor
指向了索引2
,lastRet
指向了索引1
,所以这里在第二个元素进行修改是无误的。那么在情形二中,我们调用了两次next()
方法之后又调用了一次previous()
方法,这个时候虽说游标cursor
指向了索引1
,但是lastRet
指向的也是索引1
。所以这里在第二个元素进行修改也是无误的。
这样的话,我们就对这个set(E e)
方法理解的就更深了。
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
那么最后这一个方法也是覆写了ListIterator
接口中的add(E e)
方法。之前我们在ListIterator源码逐条解析中说过这个方法,它大致描述为插入更加合适。其目的是在调用了next()
方法或previous()
方法返回的元素之后插入指定元素。
我们还是先来看源码,然后再举例。
第一步,依旧是利用checkForComodification()
方法对修改值modCount
进行检查,判断当前数组是否发生修改。
第二步,获取当前的游标cursor
并赋予新声明的变量i
。
第三步,这里是极为重要的一步,就是插入动作,你也可以叫添加。它调用了ArrayList
本类的add(int index, E element)
方法来进行插入操作。第二步中的i
就是插入的位置索引。通俗的说就是,在游标cursor
处进行插入操作。
第四步,将当前的游标cursor
进行加1操作。这步操作的目的是为了不影响游标cursor
所代表的含义,即,指向某一个特定元素。你在这个位置新添加了一个元素,那么我当初游标cursor
如果还在这个位置的话,是不是就指向了这个新元素?但是其本身是要指向原先你本身cursor
指向的那个旧元素,怎么办呢?你就只能进行移位操作,也就是加1了。
第五步,我们把这个上一项元素指向lastRet
初始化为-1
。这种操作无非就是不让你进行set(E e)
操作,抑或是不让你进行remove()
操作。为什么呢?因为其实现的方法注释是规定了如此:
remove()规定:
只有在最后一次调用next或previous之后没有调用add时,才可以执行此操作。
set(E e)规定
只有在最后一次调用next或previous之后既不调用remove也不调用add,才可以执行此调用。
第六步,同步操作值modCount
到exceptModCount
,这个就不须过多的解释了,因为add(i, e)
操作可能会使得当前ArrayList
的操作值modCount
加1,所以这里为了避免ConcurrentModificationException
异常你需要把这个修改了的值更新到我们ListItr
中的期望修改值exceptModCount
内。
我们接下来来看两个示例:
情形一:next()
方法的调用:
/**
* 正确实例,放心运行
*/
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
ListIterator listIterator = arrayList.listIterator();
listIterator.next();
listIterator.next();
listIterator.add(10);
System.out.println(arrayList);
}
/*
* 输出结果:
* [1, 2, 10, 3, 4, 5]
*/
情形二:previous()
方法的调用:
/**
* 正确实例,放心运行
*/
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
ListIterator listIterator = arrayList.listIterator();
listIterator.next();
listIterator.next();
listIterator.previous();
listIterator.add(10);
System.out.println(arrayList);
}
/*
* 输出结果:
* [1, 10, 2, 3, 4, 5]
*/
上面这两个示例输出就过不太一样,其实这个正常的。
在情形一中,我们调用了两次next()
方法之后,游标cursor
指向了索引2
,这个时候你进行添加是往索引2
这个地方进行添加,也就是挤占了原本数字3的位置。所以这里将3以及以后的所有元素后移一位,然后在本身数字3这个位置变更为10即可。
那么在情形二中,我们调用了两次next()
方法之后又调用了一次previous()
方法,这个时候游标cursor
指向了索引1
,也就是挤占了原本数字2的位置。所以这里将2以及以后的所有元素后移一位,然后在本身数字2这个位置变更为10即可。
这样的话,我们就对这个add(E e)
方法理解的就更深了。
至此,我们ArrayList-ListItr
到此全部解析完毕(ಥ_ಥ)。