jjzjj

ArrayList分析2 :Itr、ListIterator以及SubList中的坑

funnyZpC 2023-03-28 原文

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. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  6. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  7. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  8. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  9. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐