ArrayList-ListItr源码逐条解析

一家之言 姑妄言之 絮絮叨叨 不足为训

笔者废话:

   这篇文章是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()方法返回当前下一个未遍历元素的索引indexpreviousIndex()返回当前最后一次已遍历出的元素的索引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()方法返回这个相同的元素。形似这样:

12345

   当我们需要遍历上述的元素时,如果调用了两次next()方法,这个时候会返回给我们数字2。而当前游标cursor则如下指向:

12345
cursor^

   而当我们调用一次previous()方法,这个时候会返回最后一次遍历的元素2,
这个毋庸置疑,但是这个时候我们上方代码的i,也就是cursor - 1指向了哪里呢?指向了如下的位置:

12345
i^
cursor^

   那么这个时候,如果我们还想利用ListIterator接口所谓的返回相同元素的特性,你觉得我们在调用next()方法的时候这个游标cursor会指向哪里呢?那么只有下面这样,才会在调用next()方法的时候返回previous()返回的那个元素。也就是所谓“相同的元素”:

12345
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指向了索引2lastRet指向了索引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,才可以执行此调用。

   第六步,同步操作值modCountexceptModCount,这个就不须过多的解释了,因为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到此全部解析完毕(ಥ_ಥ)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值