zoukankan      html  css  js  c++  java
  • 面试题:写一个遍历ArrayList的时候,删除一个元素的例子?并说说原理。

    代码实现

    方法一:for循环
    public static void main(String[] args) {
            ArrayList<String>  list = new ArrayList<>();
            list.add("a");
            list.add("ab");
            list.add("abc");
            list.add("abc");
            list.add("abcr");
            list.add("abc");
            list.add("abcf");
            list.add("abc");
            list.add("abdc");
    
            for(int i=0;i<list.size();i++) {
                if(list.get(i).equals("abc")) {
                    System.out.println(i+":"+list.get(i));
                    list.remove(i);  // 删除后 下标调整 导致漏删
                }
            }
            System.out.println(list);
    
        }
    结果:漏删!
    2:abc
    4:abc
    5:abc
    [a, ab, abc, abcr, abcf, abdc]
    方法二: Iterator遍历,并使用自身的remove()删除元素
     public static void main(String[] args) {
            ArrayList<String>  list = new ArrayList<>();
            list.add("a");
            list.add("ab");
            list.add("abc");
            list.add("abc");
            list.add("abcr");
            list.add("abc");
            list.add("abcf");
            list.add("abc");
            list.add("abdc");
    
            Iterator<String> iter = list.iterator();
            while(iter.hasNext()){
                if(iter.next().equals("abc")){
                    iter.remove();
                }
    
            }
    
        }
    结果:
    [a, ab, abcr, abcf, abdc]
    
    

    原理

    上述的两种方法,第一种方法导致漏删。第二种方法是正确的。那么为什么在用for循环遍历的时候删除元素,会导致漏删的情况呢?这是因为在for循环时,数组会调整数组的下标,会导致漏删。由上述代码可以看出,由于下标位置在不断调整,而i也在++ ,所有导致i=3位置的元素被跳过了。从而漏删。所以我们不能用for循环遍历的同时删除元素。正确的做法是利用for循环从后往前遍历,或者使用Iterator遍历,并调用自身的remove方法删除。Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性!
    需要注意的是,使用Iterator遍历的时候,不能不允许并发调用ArrayList的remove/add操作进行修改,否则会抛出异常。这时为什么呢?
    原理是:Iterator是工作在一个独立的线程中,而且拥有一个mutex锁。Iterator在建立后会创建一个指向原来对象的单索引链表。当原来的对象元素发生改变(增加或者删除一个元素),这个索引表是不会同步改变的。所以当索引指针往后移动的时候就找不到要迭代的对象,这时候就会触发fast-fail快速失败机制。
    fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出 ConcurrentModificationException异常。那么,如果你在遍历的时候,调用list的add()或者remove(),使得集合的结构发生改变。Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。
    让我们看看源码,为什么会抛出这个并发修改异常?
    从源码知道,每次调用next()方法,在实际访问元素前,都会调用checkForComodification方法,该方法源码如下:

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

    此时,需要去看看ArrayList的源码。
    可以看出,该方法才是判断是否抛出ConcurrentModificationException异常的关键。在该段代码中,当modCount != expectedModCount
    时,就会抛出该异常。但是在一开始的时候,expectedModCount初始值默认等于modCount,为什么会出现modCount != expectedModCount,很明显expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变,所以可能发生改变的就只有modCount,在前面关于ArrayList扩容机制的分析中,可以知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(如modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用add/remove()使集合的个数发生改变,就会使modCount发生变化,这样在checkForComodification方法中就会抛出ConcurrentModificationException异常。从而触发fast-fail机制。

    避免fast-fail

    方法一
    在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。
    方法二
    使用java并发包(java.util.concurrent)中的类来代替 ArrayList 和hashMap。比如CopyONWriterArrayList, CopyOnWriterArrayList在是使用上跟 ArrayList几乎一样, CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于 CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
    对于HashMap,可以使用ConcurrentHashMap, ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。
    参考链接:https://blog.csdn.net/zymx14/article/details/78394464

    学习让我快乐,工作让我快乐。学习和工作都是为了更好的生活!
  • 相关阅读:
    nginx -s reload 时报错 [error] open() "/run/nginx.pid" failed (2: No such file or directory)
    系统调用(四):SSDT
    系统调用(三): 分析KiFastCallEntry(二)
    系统调用(二): 分析KiFastCallEntry(一)
    系统调用(一): 重写WriteProcessMemory
    CVE-2009-0927分析
    CVE-2008-0015分析
    CVE-2006-4868分析
    在linux上使用impdp命令时提示ORA-12154: TNS:could not resolve the connect identifier specified的问题
    破解myeclipse10失败的一个奇葩原因
  • 原文地址:https://www.cnblogs.com/xyuanzi/p/15115147.html
Copyright © 2011-2022 走看看