美文网首页
ArrayList中remove方法遇到的坑

ArrayList中remove方法遇到的坑

作者: 驶向灯塔的小船 | 来源:发表于2018-01-22 18:03 被阅读0次

最近这一周总是迷迷糊糊的,各种踩坑,这不,踩了一个 ArrayList 的 remove 方法的坑,下面我就来介绍一下这个方法的坑。。。

    /** 初始化一个集合 */
    private static List<Integer> integers = Lists.newArrayList(1, 2, 2, 3, 4, 25);

    /**
     * 使用 main 方法调用
     * 想调用哪个方法就将哪个方法解开注释吧
     * @param args
     */
    public static void main(String[] args) {
        remove();
//        foreachRemove();
//        foreachRemove2();
//        iteratorRemove();
//        iteratorRemove2();
//        removeEven();
        // 遍历集合
        integers.stream().forEach(System.out::println);
    }

初始化一个集合列表,使用 main 方法调用,接下来就让我们看看有哪些坑吧。

    /**
     * remove(int index)、remove(Object o) 猜猜下面删除的到底是哪个元素
     */
    static void remove() {
        // 在这里,25 不会自动装箱,表示的还是索引值
        integers.remove(25);
    }

看到注释中的两个方法,一个是根据索引删除指定的值,一个是删除指定对象。当集合中存储的是 Integer 类型的时候,传入数字1,删除到底是根据索引还是根据对象删除呢?我们猜一下吧,根据对象删除!

Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 25, Size: 6

咦,怎么报错了,根据报错的信息我们很清楚的看到,ArrayList 是根据索引来删除元素的,由于没有索引值为 25 的元素,所以就报越界异常了。其实我们了解重载方法我们就会知道 Java 会根据重载方法精确匹配,看下面的重载方法就明白了。

    static void revoew(int i) {
        System.out.println("我是普通变量");
    }
    static void revoew(Integer i) {
        System.out.println("我是对象");
    }
    public static void main(String[] args) {
        revoew(1);
    }
    // 输出
    我是普通变量

跨过重载删除的坑,我们来看看迭代器中使用 remove 方法删除的坑吧。

    /**
     * 使用迭代器删除指定元素,两个元素都出现在元素中的前几个
     */
    static void iteratorRemove() {
        Iterator<Integer> iterator = integers.iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();
            if (Integer.valueOf(1).equals(next)) {
                integers.remove(next);
            }
            if (Integer.valueOf(2).equals(next)) {
                integers.remove(next);
            }
        }
    }

来,猜猜上面方法会输出啥?我猜应该输出 3 4 5,运行 main 方法试试。。。

Exception in thread "main" java.util.ConcurrentModificationException

纳尼?怎么报出了个并发修改的异常啊!我明明跑的是单线程啊,吓得我赶紧看断点调一调到底是哪个地方抛出来的异常。


image.png

源码如下

        public E next() {
            // 判断当前链表结构是否被改变,如果改变了则 抛出 ConcurrentModificationException 异常
            checkForComodification();
            // 游标,迭代器每改变 list 中的结构游标都会改变,前提是通过迭代器来修改,否则游标不会改变
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            // 当数组中元素个数小于游标值时,就会报出下面这个错误,也就是我们要找的
            // 使用迭代器的时候不要使用 list 的 remove 方法,而要使用 迭代器的remove 方法的原因
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

现在终于明白为啥会报这个错了,弄懂了这个坑,下一个坑又来了。代码如下:

    /**
     * 使用迭代器删除指定元素,删除指定元素,两个元素是集合中最后两个
     */
    static void iteratorRemove2() {
        Iterator<Integer> iterator = integers.iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();
            if (Integer.valueOf(4).equals(next)) {
                integers.remove(next);
            }
            if (Integer.valueOf(25).equals(next)) {
                integers.remove(next);
            }
        }
    }

大家一看,肯定会说,这个方法和上面那个方法不是类似么,肯定一运行就报并发修改的那个错。还别说,真不是报那个错,而且还有结果输出呢!

1
2
2
3
25

咦,为啥没有报错?4 也被删除了,可是为啥 25 还在呢,可怕,有 bug。冷静分析一波,4 和 25 是集合中最后两个元素,删除 4 的时候,迭代器的游标值为 4,当把 4 删掉之后,游标还是 4 ,而size 也是4 ,那么继续循环应该会报错才对。但此时 iterator.hashNext() 返回的却是 false,直接终止循环,那么也就不会调用 iterator.next() 报错了。因为根本就不会执行了,那现在大家应该会问。25 去哪了?从结果看 25 并没有被删除。那么我们看看 remove 方法,由于 remove 最终还是调用 fastRemove(int index) ,直接看这个方法

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 删除一个元素,数组会将删除的元素后面的元素往前移
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

正是因为 4 后面的元素往前移动了一个位置,所以 25 就放到了原来 4 的位置,而 iterator 指针还是指向4的位置,那么调用 iterator.hashNext() 发现没有元素了,所以就退出循环,这也就是为什么不会报错,而且 25 还没有被删除的原因。
下面我们纪录一下迭代器的字节码,后面会用到

  static void iteratorRemove();
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: getstatic     #3                  // Field integers:Ljava/util/List;
         3: invokeinterface #10,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
         8: astore_0
         9: aload_0
        10: invokeinterface #11,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
        15: ifeq          73
        18: aload_0
        19: invokeinterface #12,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
        24: checkcast     #13                 // class java/lang/Integer
        27: astore_1
        28: iconst_1
        29: invokestatic  #14                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        32: aload_1
        33: invokevirtual #15                 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
        36: ifeq          49
        39: getstatic     #3                  // Field integers:Ljava/util/List;
        42: aload_1
        43: invokeinterface #16,  2           // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
        48: pop
        49: iconst_2
        50: invokestatic  #14                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        53: aload_1
        54: invokevirtual #15                 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
        57: ifeq          70
        60: getstatic     #3                  // Field integers:Ljava/util/List;
        63: aload_1
        64: invokeinterface #16,  2           // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
        69: pop
        70: goto          9
        73: return
      LineNumberTable:
        line 77: 0
        line 78: 9
        line 79: 18
        line 80: 28
        line 81: 39
        line 83: 49
        line 84: 60
        line 86: 70
        line 87: 73
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           28      42     1  next   Ljava/lang/Integer;
            9      65     0 iterator   Ljava/util/Iterator;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            9      65     0 iterator   Ljava/util/Iterator<Ljava/lang/Integer;>;
      StackMapTable: number_of_entries = 4
        frame_type = 252 /* append */
          offset_delta = 9
          locals = [ class java/util/Iterator ]
        frame_type = 252 /* append */
          offset_delta = 39
          locals = [ class java/lang/Integer ]
        frame_type = 250 /* chop */
          offset_delta = 20
        frame_type = 2 /* same */

好啦!迭代器中使用 list 的 remove 方法删除元素的坑踩完了,我们踩踩 foreach 循环里面删除元素的坑吧,代码如下:

    /**
     * 使用 foreach 循环,删除指定元素,两个元素都出现在元素中的前几个
     */
    static void foreachRemove() {
        for (Integer integer : integers) {
            if (Integer.valueOf(1).equals(integer)) {
                integers.remove(integer);
            }
            if (Integer.valueOf(2).equals(integer)) {
                integers.remove(integer);
            }
        }
    }

    /**
     * 使用 foreach 循环,删除指定元素,两个元素是集合中最后两个
     */
    static void foreachRemove2() {
        for (Integer integer : integers) {
            if (Integer.valueOf(3).equals(integer)) {
                integers.remove(integer);
            }
            if (Integer.valueOf(4).equals(integer)) {
                integers.remove(integer);
            }
        }
    }

这两个方法和上面迭代器的坑是一样的,我们看看 forEach循环的字节码吧

static void foreachRemove();
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: getstatic     #3                  // Field integers:Ljava/util/List;
         3: invokeinterface #10,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
         8: astore_0
         9: aload_0
        10: invokeinterface #11,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
        15: ifeq          73
        18: aload_0
        19: invokeinterface #12,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
        24: checkcast     #13                 // class java/lang/Integer
        27: astore_1
        28: iconst_1
        29: invokestatic  #14                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        32: aload_1
        33: invokevirtual #15                 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
        36: ifeq          49
        39: getstatic     #3                  // Field integers:Ljava/util/List;
        42: aload_1
        43: invokeinterface #16,  2           // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
        48: pop
        49: iconst_2
        50: invokestatic  #14                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        53: aload_1
        54: invokevirtual #15                 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
        57: ifeq          70
        60: getstatic     #3                  // Field integers:Ljava/util/List;
        63: aload_1
        64: invokeinterface #16,  2           // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
        69: pop
        70: goto          9
        73: return
      LineNumberTable:
        line 49: 0
        line 50: 28
        line 51: 39
        line 53: 49
        line 54: 60
        line 56: 70
        line 57: 73
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           28      42     1 integer   Ljava/lang/Integer;
      StackMapTable: number_of_entries = 4
        frame_type = 252 /* append */
          offset_delta = 9
          locals = [ class java/util/Iterator ]
        frame_type = 252 /* append */
          offset_delta = 39
          locals = [ class java/lang/Integer ]
        frame_type = 250 /* chop */
          offset_delta = 20
        frame_type = 250 /* chop */
          offset_delta = 2 /* same */

有没有发现 forEach 字节码和迭代器的字节码一样,没错,就是一样,连坑都一样,知道上面的坑后那么 forEach 里面的坑也就明白是怎么一回事了。
正确的删除方法, 应该使用迭代器里面的 remove 方法

    static void correctRemove() {
        Iterator<Integer> iterator = integers.iterator();
        while (iterator.hasNext()){
            Integer next = iterator.next();
            if (Integer.valueOf(1).equals(next)) {
                iterator.remove();
            }
            if (Integer.valueOf(2).equals(next)) {
                iterator.remove();
            }
        }
    }

总结:以上就是我使用 ArrayList 的 remove 方法遇到的一些坑,都是自己使用了不正确的方法来删除集合中的元素,希望大家以后都是用正确的删除方式删除集合中的元素。Iterator 里面有 remove 方法,存在即是道理。走上正确的道路才能成功!!!

相关文章

网友评论

      本文标题:ArrayList中remove方法遇到的坑

      本文链接:https://www.haomeiwen.com/subject/uzplaxtx.html