ArrayList中remove方法遇到的坑

最近这一周总是迷迷糊糊的,各种踩坑,这不,踩了一个 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 方法,存在即是道理。走上正确的道路才能成功!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容