ArrayList中remove方法遇到的坑

最近這一周總是迷迷糊糊的,各種踩坑,這不朴摊,踩了一個(gè) ArrayList 的 remove 方法的坑鲸鹦,下面我就來介紹一下這個(gè)方法的坑。膝擂。。

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

    /**
     * 使用 main 方法調(diào)用
     * 想調(diào)用哪個(gè)方法就將哪個(gè)方法解開注釋吧
     * @param args
     */
    public static void main(String[] args) {
        remove();
//        foreachRemove();
//        foreachRemove2();
//        iteratorRemove();
//        iteratorRemove2();
//        removeEven();
        // 遍歷集合
        integers.stream().forEach(System.out::println);
    }

初始化一個(gè)集合列表,使用 main 方法調(diào)用醋奠,接下來就讓我們看看有哪些坑吧。

    /**
     * remove(int index)伊佃、remove(Object o) 猜猜下面刪除的到底是哪個(gè)元素
     */
    static void remove() {
        // 在這里窜司,25 不會(huì)自動(dòng)裝箱,表示的還是索引值
        integers.remove(25);
    }

看到注釋中的兩個(gè)方法航揉,一個(gè)是根據(jù)索引刪除指定的值塞祈,一個(gè)是刪除指定對(duì)象。當(dāng)集合中存儲(chǔ)的是 Integer 類型的時(shí)候帅涂,傳入數(shù)字1议薪,刪除到底是根據(jù)索引還是根據(jù)對(duì)象刪除呢?我們猜一下吧媳友,根據(jù)對(duì)象刪除斯议!

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

咦,怎么報(bào)錯(cuò)了醇锚,根據(jù)報(bào)錯(cuò)的信息我們很清楚的看到哼御,ArrayList 是根據(jù)索引來刪除元素的坯临,由于沒有索引值為 25 的元素,所以就報(bào)越界異常了恋昼。其實(shí)我們了解重載方法我們就會(huì)知道 Java 會(huì)根據(jù)重載方法精確匹配看靠,看下面的重載方法就明白了。

    static void revoew(int i) {
        System.out.println("我是普通變量");
    }
    static void revoew(Integer i) {
        System.out.println("我是對(duì)象");
    }
    public static void main(String[] args) {
        revoew(1);
    }
    // 輸出
    我是普通變量

跨過重載刪除的坑液肌,我們來看看迭代器中使用 remove 方法刪除的坑吧挟炬。

    /**
     * 使用迭代器刪除指定元素,兩個(gè)元素都出現(xiàn)在元素中的前幾個(gè)
     */
    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);
            }
        }
    }

來嗦哆,猜猜上面方法會(huì)輸出啥谤祖?我猜應(yīng)該輸出 3 4 5,運(yùn)行 main 方法試試老速。泊脐。。

Exception in thread "main" java.util.ConcurrentModificationException

納尼烁峭?怎么報(bào)出了個(gè)并發(fā)修改的異常叭菘汀!我明明跑的是單線程啊约郁,嚇得我趕緊看斷點(diǎn)調(diào)一調(diào)到底是哪個(gè)地方拋出來的異常缩挑。


image.png

源碼如下

        public E next() {
            // 判斷當(dāng)前鏈表結(jié)構(gòu)是否被改變,如果改變了則 拋出 ConcurrentModificationException 異常
            checkForComodification();
            // 游標(biāo)鬓梅,迭代器每改變 list 中的結(jié)構(gòu)游標(biāo)都會(huì)改變供置,前提是通過迭代器來修改,否則游標(biāo)不會(huì)改變
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            // 當(dāng)數(shù)組中元素個(gè)數(shù)小于游標(biāo)值時(shí)绽快,就會(huì)報(bào)出下面這個(gè)錯(cuò)誤芥丧,也就是我們要找的
            // 使用迭代器的時(shí)候不要使用 list 的 remove 方法,而要使用 迭代器的remove 方法的原因
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

現(xiàn)在終于明白為啥會(huì)報(bào)這個(gè)錯(cuò)了坊罢,弄懂了這個(gè)坑续担,下一個(gè)坑又來了。代碼如下:

    /**
     * 使用迭代器刪除指定元素活孩,刪除指定元素物遇,兩個(gè)元素是集合中最后兩個(gè)
     */
    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);
            }
        }
    }

大家一看,肯定會(huì)說憾儒,這個(gè)方法和上面那個(gè)方法不是類似么询兴,肯定一運(yùn)行就報(bào)并發(fā)修改的那個(gè)錯(cuò)。還別說起趾,真不是報(bào)那個(gè)錯(cuò)诗舰,而且還有結(jié)果輸出呢!

1
2
2
3
25

咦训裆,為啥沒有報(bào)錯(cuò)眶根?4 也被刪除了蜀铲,可是為啥 25 還在呢,可怕汛闸,有 bug蝙茶。冷靜分析一波艺骂,4 和 25 是集合中最后兩個(gè)元素诸老,刪除 4 的時(shí)候,迭代器的游標(biāo)值為 4钳恕,當(dāng)把 4 刪掉之后别伏,游標(biāo)還是 4 ,而size 也是4 忧额,那么繼續(xù)循環(huán)應(yīng)該會(huì)報(bào)錯(cuò)才對(duì)厘肮。但此時(shí) iterator.hashNext() 返回的卻是 false,直接終止循環(huán)睦番,那么也就不會(huì)調(diào)用 iterator.next() 報(bào)錯(cuò)了类茂。因?yàn)楦揪筒粫?huì)執(zhí)行了,那現(xiàn)在大家應(yīng)該會(huì)問托嚣。25 去哪了巩检?從結(jié)果看 25 并沒有被刪除。那么我們看看 remove 方法示启,由于 remove 最終還是調(diào)用 fastRemove(int index) ,直接看這個(gè)方法

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 刪除一個(gè)元素兢哭,數(shù)組會(huì)將刪除的元素后面的元素往前移
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

正是因?yàn)?4 后面的元素往前移動(dòng)了一個(gè)位置,所以 25 就放到了原來 4 的位置夫嗓,而 iterator 指針還是指向4的位置迟螺,那么調(diào)用 iterator.hashNext() 發(fā)現(xiàn)沒有元素了,所以就退出循環(huán)舍咖,這也就是為什么不會(huì)報(bào)錯(cuò)矩父,而且 25 還沒有被刪除的原因。
下面我們紀(jì)錄一下迭代器的字節(jié)碼排霉,后面會(huì)用到

  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 循環(huán)里面刪除元素的坑吧郑诺,代碼如下:

    /**
     * 使用 foreach 循環(huán)夹姥,刪除指定元素,兩個(gè)元素都出現(xiàn)在元素中的前幾個(gè)
     */
    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 循環(huán)辙诞,刪除指定元素辙售,兩個(gè)元素是集合中最后兩個(gè)
     */
    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);
            }
        }
    }

這兩個(gè)方法和上面迭代器的坑是一樣的,我們看看 forEach循環(huán)的字節(jié)碼吧

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 */

有沒有發(fā)現(xiàn) forEach 字節(jié)碼和迭代器的字節(jié)碼一樣飞涂,沒錯(cuò)旦部,就是一樣祈搜,連坑都一樣,知道上面的坑后那么 forEach 里面的坑也就明白是怎么一回事了士八。
正確的刪除方法, 應(yīng)該使用迭代器里面的 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();
            }
        }
    }

總結(jié):以上就是我使用 ArrayList 的 remove 方法遇到的一些坑容燕,都是自己使用了不正確的方法來刪除集合中的元素,希望大家以后都是用正確的刪除方式刪除集合中的元素婚度。Iterator 里面有 remove 方法蘸秘,存在即是道理。走上正確的道路才能成功;茸隆4茁病!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末哮翘,一起剝皮案震驚了整個(gè)濱河市颈嚼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌饭寺,老刑警劉巖阻课,帶你破解...
    沈念sama閱讀 216,744評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異艰匙,居然都是意外死亡限煞,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門旬薯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晰骑,“玉大人,你說我怎么就攤上這事绊序∷队撸” “怎么了?”我有些...
    開封第一講書人閱讀 163,105評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵骤公,是天一觀的道長抚官。 經(jīng)常有香客問我,道長阶捆,這世上最難降的妖魔是什么凌节? 我笑而不...
    開封第一講書人閱讀 58,242評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮洒试,結(jié)果婚禮上倍奢,老公的妹妹穿的比我還像新娘。我一直安慰自己垒棋,他們只是感情好卒煞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評(píng)論 6 389
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著叼架,像睡著了一般畔裕。 火紅的嫁衣襯著肌膚如雪衣撬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,215評(píng)論 1 299
  • 那天扮饶,我揣著相機(jī)與錄音具练,去河邊找鬼。 笑死甜无,一個(gè)胖子當(dāng)著我的面吹牛扛点,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播毫蚓,決...
    沈念sama閱讀 40,096評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼占键,長吁一口氣:“原來是場噩夢啊……” “哼昔善!你這毒婦竟也來了元潘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,939評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤君仆,失蹤者是張志新(化名)和其女友劉穎翩概,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體返咱,經(jīng)...
    沈念sama閱讀 45,354評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钥庇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咖摹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片评姨。...
    茶點(diǎn)故事閱讀 39,745評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖萤晴,靈堂內(nèi)的尸體忽然破棺而出吐句,到底是詐尸還是另有隱情,我是刑警寧澤店读,帶...
    沈念sama閱讀 35,448評(píng)論 5 344
  • 正文 年R本政府宣布嗦枢,位于F島的核電站,受9級(jí)特大地震影響屯断,放射性物質(zhì)發(fā)生泄漏文虏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評(píng)論 3 327
  • 文/蒙蒙 一殖演、第九天 我趴在偏房一處隱蔽的房頂上張望氧秘。 院中可真熱鬧,春花似錦趴久、人聲如沸丸相。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽已添。三九已至妥箕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間更舞,已是汗流浹背畦幢。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缆蝉,地道東北人宇葱。 一個(gè)月前我還...
    沈念sama閱讀 47,776評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像刊头,于是被迫代替她去往敵國和親黍瞧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容