ArrayList分析2 : ItrListIterator以及SubList中的坑

转载请注明出处:https://www.cnblogs.com/funnyzpc/p/16409137.html

一.不论ListIterator还是SubList,均是对ArrayList维护的数组进行操作

首先我得说下ListIterator是什么,ListIteratorIterator均是迭代器接口,对应ArrayList中的实现就是ListItrItr,我们使用ListIteratorSubList的过程中很少对ArrayList的操作,如果有那就很严重了(下面会说的),对源数组进行操作这是一个事实存在的问题,尤其在SubList表现的尤为严重~

先看看ArrayListsubList方法定义:

    public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}

可以看到subList方法返回的是SubList的一个实例,好,继续看构造函数定义:

    private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
// SubList构造函数的具体定义
SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) {
// 从offset开始截取size个元素
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}

首先我们要清楚的是subList对源数组(elementData)的取用范围fromIndex <=取用范围< toIndex, 这里用取用范围其实很准确,接着看~ 因为return new SubList(this, 0, fromIndex, toIndex);对应构造函数的第一个参数parent其实也就是当前ArrayList的实例对象,这是其一,还有就是SubList的offset是默认的offset+ fromIndex,取用的范围就size限制在toIndex - fromIndex;以内,不管是ArrayList还是SubList对数组(elementData)的偏移操作,只不过一个是从0开始一个是从 offset + fromIndex;开始~,如果你还是存在怀疑,先看看SubListget`方法:

        public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}

看到没,get方法也只直接取用的原数组(elementData)->return ArrayList.this.elementData(offset + index);,很明白了吧,再看看SubListremove方法论证下当前这个小标题哈~

        public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
this.size--;
return result;
}

我在前前面说过,这个parent其实也就是当前ArrayList的一个引用,既然是引用,而不是深拷贝,那这句 parent.remove(parentOffset + index);操作的依然是原数组elementData,实操一下看:

    public static void main(String[] args) {
ArrayList arr = new ArrayList();
arr.add("a"); // 0
arr.add("b");
arr.add("c");
arr.add("d"); // 3
arr.add("e");
arr.add("f"); // 4
List sub_list = arr.subList(0, 3);
System.out.println(sub_list);// [a, b, c]
sub_list.remove(0);
System.out.println(sub_list); // [b, c]
System.out.println(arr); // [b, c, d, e, f]
}

坑吧,一般理解subList返回的是一个深度拷贝的数组,哪知SubListArrayList内部都是一家人(elementData),所以在使用subList的函数时要谨记这一点,当然咯,既然SubList也是继承自AbstractListsubList返回的数组也能继续调用subList方法,内部操作的数组也是一样,是不是很吊诡

二.ListItrprevious方法不太好用

其实这是个小问题,我是基于以下两点来判断的.

1.使用迭代器的习惯

我们实际使用迭代器的习惯是从左往右(一般数组结构),索引从小到大(index),这样的一个使用习惯:

   public static void main(String[] args) {
ArrayList arr = new ArrayList();
arr.add("a"); // 0
arr.add("b");
arr.add("c");
arr.add("d"); // 3
ListIterator listIterator = arr.listIterator();
while(listIterator.hasPrevious()){
Object item = listIterator.next();
System.out.println(item);
}
}

以上代码是常规的代码逻辑,而且previous一般在next方法使用后才可使用,这里就牵出另一个问题了,往下看

2.迭代器的默认游标是从0开始的

如果您觉得1的说法不够信服的话,那就实操下看:

      public static void main(String[] args) {
ArrayList arr = new ArrayList();
arr.add("a"); // 0
arr.add("b");
arr.add("c");
arr.add("d"); // 3
ListIterator listIterator = arr.listIterator();
while(listIterator.hasPrevious()){//这里返回的始终是false,所以while内的逻辑根本就不会被执行
Object item = listIterator.previous();
System.out.println(item); // 这里没输出
}
}

哈哈哈,看出bug所在了嘛,再看看ListItr的构造函数吧

ArrayList函数)

    public ListIterator<E> listIterator() {
// 当前方法同以上,只不过是直接从0开始索引并返回一个迭代器 ,具体代码方法内会有说明
return new ListItr(0);
}

(ListItr的构造函数)

     private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}

ListItrhasPrevious方法)

     public boolean hasPrevious() {
return cursor != 0;
}

看出症结所在了吧,其实很简单,也就是默认listIterator()的构造函数传入的游标是0(cursor = index;)导致的,好了,对于一个正常的previous方法的使用该怎么办呢

    public static void main(String[] args) {
ArrayList arr = new ArrayList();
arr.add("a"); // 0
arr.add("b");
arr.add("c");
arr.add("d"); // 3
ListIterator listIterator = arr.listIterator(arr.size());// 修改后的
while(listIterator.hasPrevious()){
Object item = listIterator.previous();
System.out.println(item);// b a
}
}

其实也就改了一句ListIterator listIterator = arr.listIterator(arr.size());,是不是超 easy,所以使用previous的时候一定要指定下index(对应ListIter的其实就是游标:cursor) ,知其症之所在方能对症下药

三.ListItr中的set、remove方法一般在nextprevious方法之后调用才可

如果看过上面的内容,估计你您能猜个八九,线上菜:

    public static void main(String[] args) {
ArrayList arr = new ArrayList();
arr.add("a");
arr.add("b");
arr.add("c");
arr.add("d");
System.out.println(arr);
ListIterator listIterator = arr.listIterator();
listIterator.set("HELLO"); // throw error
}

我还是建议您先将上面一段代码执行下看,虽然结果还是抛错。。。

好吧,瞅瞅源码看:

public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();//发生异常的位置
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

再看看lastRet定义的地方:

     private class Itr implements Iterator<E> {
// 这个其实默认就是 i=0;
int cursor; // index of next element to return :下一个将要返回的元素位置的索引,其实也就是个游标
int lastRet = -1; // index of last element returned; -1 if no such :返回的最后一个元素的索引; -1 如果没有
int expectedModCount = modCount;

顺带再回头看看构造方法:

        ListItr(int index) {
super();
cursor = index;
}

我先解释下lastRet是什么,lastRet其实是cursor(俗称游标)的参照位置,具体的说它是标识当前循环的元素的位置(cursor-1)

这时 是不是觉得直接使用ListIterset方法是条死路..., 既然lastRet必须>=0才可,找找看哪里有变动lastRet的地方:

      @SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
      @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];
}

看到没lastRet = i它解释了一切

现在来尝试解决这个问题,两种方式:

(方式一)

    public static void main(String[] args) {
ArrayList arr = new ArrayList();
arr.add("a");
arr.add("b");
arr.add("c");
arr.add("d");
System.out.println(arr);
ListIterator listIterator = arr.listIterator();
listIterator.next();
listIterator.set("HELLO");
System.out.println(arr);
}

(方式二)

    public static void main(String[] args) {
ArrayList arr = new ArrayList();
arr.add("a");
arr.add("b");
arr.add("c");
arr.add("d");
System.out.println(arr);
ListIterator listIterator = arr.listIterator(3);
listIterator.previous();
listIterator.set("HELLO");
System.out.println(arr);
}

四.ListItr中的previousnext不可同时使用,尤其在循环中

先看一段代码吧,试试看你电脑会不会炸

   public static void main(String[] args) {
ArrayList arr = new ArrayList();
arr.add("a");
arr.add("b");
arr.add("c");
arr.add("d");
ListIterator listIterator = arr.listIterator();
while (listIterator.hasNext()){
Object item = listIterator.next();
System.out.println(item);
if("c".equals(item)){
Object previous_item = listIterator.previous(); // c
if("b".equals(previous_item)){
return;
}
}
}
}

怎么样,我大概会猜出你的看法,previous_item 的值与预期的并不一样,哈哈哈,不解释了,这里简单的解决办法是:如果是在循环内,就不要尝试nextprevious可能的同时调用了 ,非循环也不建议,还是留意下源码看(此处省略n多字).

五. Itr、ListItr、SubList使用过程中不可穿插ArrayList的相关操作(remove、add等),否则抛错

废话是多余的,先给个事故现场

    public static void main(String[] args) {
ArrayList arr = new ArrayList();
arr.add("a");
arr.add("b");
arr.add("c");
arr.add("d");
ListIterator listIterator = arr.listIterator();
arr.add("HELLO");
listIterator.hasNext();
listIterator.next(); // throw error
}

为了更清楚,给出异常信息:

Exception in thread "main" java.util.ConcurrentModificationException
at com.mee.source.c1.ArrayList$Itr.checkForComodification(ArrayList.java:1271)
at com.mee.source.c1.ArrayList$Itr.next(ArrayList.java:1181)
at com.mee.source.test.ArrayList_listIterator_Test.main(ArrayList_listIterator_Test.java:208)

next方法:

 @SuppressWarnings("unchecked")
public E next() {
checkForComodification(); // 1181行,这里抛出错误!
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

checkForComodification方法:

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

这里我先卖个关子,具体原因需要您看看上一篇博客 ArrayList分析1-循环、扩容、版本 关于版本的部分

解决方法嘛,小标题就是结论也是规则,绕着走避坑便是啦

ArrayList分析2 :Itr、ListIterator以及SubList中的坑的更多相关文章

  1. ArrayList分析1-循环、扩容、版本

    ArrayList分析1-循环.扩容.版本 转载请注明出处 https://www.cnblogs.com/funnyzpc/p/16407733.html 前段时间抽空看了下ArrayList的源码 ...

  2. 关联分析FPGrowth算法在JavaWeb项目中的应用

    关联分析(关联挖掘)是指在交易数据.关系数据或其他信息载体中,查找存在于项目集合或对象集合之间的频繁模式.关联.相关性或因果结构.关联分析的一个典型例子是购物篮分析.通过发现顾客放入购物篮中不同商品之 ...

  3. 【转】busybox分析——arp设置ARP缓存表中的mac地址

    [转]busybox分析——arp设置ARP缓存表中的mac地址 转自:http://blog.chinaunix.net/uid-26009923-id-5098083.html 1. 将arp缓存 ...

  4. 插件开发之360 DroidPlugin源码分析(五)Service预注册占坑

    请尊重分享成果,转载请注明出处: http://blog.csdn.net/hejjunlin/article/details/52264977 在了解系统的activity,service,broa ...

  5. Golang中的坑二

    Golang中的坑二 for ...range 最近两周用Golang做项目,编写web服务,两周时间写了大概五千行代码(业务代码加单元测试用例代码).用Go的感觉很爽,编码效率高,运行效率也不错,用 ...

  6. Golang 中的坑 一

    Golang 中的坑 短变量声明  Short variable declarations 考虑如下代码: package main import ( "errors" " ...

  7. Mysql系列八:Mycat和Sharding-jdbc的区别、Mycat分片join、Mycat分页中的坑、Mycat注解、Catlet使用

    一.Mycat和Sharding-jdbc的区别 1)mycat是一个中间件的第三方应用,sharding-jdbc是一个jar包 2)使用mycat时不需要改代码,而使用sharding-jdbc时 ...

  8. Windows API中的坑

    本文主页链接:Windows API中的坑 ExpandEnvironmentStrings 风险: 进程会继承其父进程的环境变量.在展开如%APPDATA%等文件夹时,有可能父进程对此环境变量进行过 ...

  9. vue中的坑 --- 锚点与查询字符串

    在vue中,由于是单页面SPA,所以需要使用锚点来定位,在vue的官方文档中提到过也可以不使用锚点的情况,就是在vue-router中使用history模式,这样,在url中就不会出现丑陋的#了,但是 ...

随机推荐

  1. Java语言学习day33--8月8日

    今日内容介绍1.基本类型包装类2.System类3.Math类4.Arrays类5.大数据运算 ###01基本数据类型对象包装类概述 *A:基本数据类型对象包装类概述 *a.基本类型包装类的产生 在实 ...

  2. *CTF babyarm内核题目分析

    本文从漏洞分析.ARM64架构漏洞利用方式来讨论如何构造提权PoC达到读取root权限的文件.此题是一个ARM64架构的Linux 5.17.2 版本内核提权题目,目的是读取root用户的flag文件 ...

  3. 如何在Web前端实现CAD图文字全文搜索功能之技术分享

    现状 在CAD看图过程中我们经常会需要用到查找文字的功能,在AutoCAD软件查找一个文字时,可以通过打开左下角输入命令find,输入查找的文字,然后设置查找范围,就可以搜索到需要查询的文字.但在We ...

  4. RedirectAttributes重定向

    1.url显示参数信息(不安全) @Controller @RequestMapping("/UserOperate") public class UserController { ...

  5. 2003031121——浦娟——Python数据分析第七周作业——MySQL的安装及使用

    项目 要求 课程班级博客链接 20级数据班(本) 作业要求链接 Python第七周作业 博客名称 2003031121--浦娟--Python数据分析第七周作业--MySQL的安装及使用 要求 每道题 ...

  6. dfs深搜

    一.01背包dfs //回溯法,01背包 #include<iostream> #include<algorithm> using namespace std; const i ...

  7. Linux 系统安装 AutoFs 挂载服务

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 作者: Escape 链接: https://escapelife.github.io/pos ...

  8. 实战|Hadoop大数据集群搭建

    一个执着于技术的公众号 前言 今天来为粉丝圆梦啦 话不多说,咱直接进入实战环节 实验环境: 主机名        IP地址                 角色 qll251       192.16 ...

  9. SpringBoot从0到0.7——第四天

    SpringBoot从0到0.7--第四天 今天进行实战开发一个小项目,SpringBoot和Thymeleaf集成的小项目 因为懒得写写前端,直接找的别人的项目在它的的基础上进行配置,进行修改.gi ...

  10. web框架的本质、MVC框架MTV框架的介绍

    1.web框架的本质 所有的Web应用本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端,基于请求做出响应,客户都先请求,服务端做出对应的响应,按照http协议的请求协议发送请 ...