美文网首页
210418:ArrayList与HashMap删除元素-定时任

210418:ArrayList与HashMap删除元素-定时任

作者: 弹钢琴的崽崽 | 来源:发表于2021-04-18 20:14 被阅读0次

    一. ArrayList遍历时删除元素的正确姿势是什么?

    1. 简介

    我们在项目开发过程中,经常会有需求需要删除ArrayList中的某个元素,而使用不正确的删除方式,就有可能抛出异常。或者在面试中,会遇到面试官询问遍历时如何正常删除元素

    ArrayList遍历时删除元素的几种姿势

    首先结论如下:

    1. 第1种方法 - 普通for循环正序删除(结果:会漏掉元素判断)
    2. 第2种方法 - 普通for循环倒序删除(结果:正确删除)
    3. 第3种方法 - for-each循环删除(结果:抛出异常)
    4. 第4种方法 - Iterator遍历,使用ArrayList.remove()删除元素(结果:抛出异常)
    5. 第5种方法 - Iterator遍历,使用Iterator的remove删除元素(结果:正确删除)

    首先初始化一个数组arrayList,假设我们要删除等于3的元素。

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        arrayList.add(3);
        arrayList.add(4);
        arrayList.add(5);
        removeWayOne(arrayList);
    }
    

    2. 第1种方法 - 普通for循环正序删除(结果:会漏掉元素判断)

    for (int i = 0; i < arrayList.size(); i++) {
        if (arrayList.get(i) == 3) {//3是要删除的元素
            arrayList.remove(i);
            //解决方案: 加一行代码i = i - 1; 删除元素后,下标减1
        }
        System.out.println("当前arrayList是"+arrayList.toString());
    }
    

    //原ArrayList是[1, 2, 3, 3, 4, 5]
    //删除后是[1, 2, 3, 4, 5]

    输出结果:

    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 4, 5]
    当前arrayList是[1, 2, 3, 4, 5]
    当前arrayList是[1, 2, 3, 4, 5]
    

    可以看到少删除了一个3,原因在于调用remove删除元素时,remove方法调用System.arraycopy()方法将后面的元素移动到前面的位置,也就是第二个3会移动到数组下标为2的位置,而在下一次循环时,i+1之后,i会为3,不会对数组下标为2这个位置进行判断,所以这种写法,在删除元素时,被删除元素a的后一个元素b会移动a的位置,而i已经加1,会忽略对元素b的判断,所以如果是连续的重复元素,会导致少删除。

    解决方案

    可以在删除元素后,执行i=i-1,使得下次循环时再次对该数组下标进行判断。

    3. 第2种方法 - 普通for循环倒序删除(结果:正确删除)

     for (int i = arrayList.size() -1 ; i>=0; i--) {
        if (arrayList.get(i).equals(3)) {
            arrayList.remove(i);
        }
        System.out.println("当前arrayList是"+arrayList.toString());
    }
    

    输出结果:

    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 4, 5]
    当前arrayList是[1, 2, 4, 5]
    当前arrayList是[1, 2, 4, 5]
    当前arrayList是[1, 2, 4, 5]
    

    这种方法可以正确删除元素,因为调用remove删除元素时,remove方法调用System.arraycopy()将被删除元素a后面的元素向前移动,而不会影响元素a之前的元素,所以倒序遍历可以正常删除元素。

    4. 第3种方法 - for-each循环删除(结果:抛出异常)

    public static void removeWayThree(ArrayList<Integer> arrayList) {
        for (Integer value : arrayList) {
            if (value.equals(3)) {//3是要删除的元素
                arrayList.remove(value);
            }
        System.out.println("当前arrayList是"+arrayList.toString());
        }
    }
    

    输出结果:

    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 4, 5]
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
        at java.util.ArrayList$Itr.next(ArrayList.java:851)
        at com.test.ArrayListTest1.removeWayThree(ArrayListTest1.java:50)
        at com.test.ArrayListTest1.main(ArrayListTest1.java:24)
    

    会抛出ConcurrentModificationException异常,主要在于for-each的底层实现是使用ArrayList.iterator的hasNext()方法和next()方法实现的,我们可以使用反编译进行验证,对包含上面的方法的类使用以下命令反编译验证

    javac ArrayTest.java//生成ArrayTest.class文件
    
    javap -c ArrayListTest.class//对class文件反编译
    

    得到removeWayThree方法的反编译代码如下:

     public static void removeWayThree(java.util.ArrayList<java.lang.Integer>);
        Code:
           0: aload_0
           1: invokevirtual #12   // Method java/util/ArrayList.iterator:()Ljava/util/Iterator;
           4: astore_1
           5: aload_1
           6: invokeinterface #13,  1 // InterfaceMethod java/util/Iterator.hasNext:()Z   调用Iterator.hasNext()方法
          11: ifeq          44
          14: aload_1
          15: invokeinterface #14,  1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;调用Iterator.next()方法
          20: checkcast     #9                  // class java/lang/Integer
          23: astore_2
          24: aload_2
          25: iconst_3
          26: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
          29: invokevirtual #10                 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z 
          32: ifeq          41
          35: aload_0
          36: aload_2
          37: invokevirtual #15                 // Method java/util/ArrayList.remove:(Ljava/lang/Object;)Z
          40: pop
          41: goto          5
          44: return
    
    

    可以很清楚得看到Iterator.hasNext()来判断是否还有下一个元素,和Iterator.next()方法来获取下一个元素。而因为在删除元素时,remove()方法会调用fastRemove()方法,其中会对modCount+1,代表对数组进行了修改,将修改次数+1。

     public boolean remove(Object o) {
         if (o == null) {
             for (int index = 0; index < size; index++)
                 if (elementData[index] == null) {
                     fastRemove(index);
                 return true;
             }
         } else {
             for (int index = 0; index < size; index++)
                 if (o.equals(elementData[index])) {
                     fastRemove(index);
                     return true;
                 }
         }
            return false;
    }
    
    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
    }
    

    而当删除完元素后,进行下一次循环时,会调用下面源码中Itr.next()方法获取下一个元素,会调用checkForComodification()方法对ArrayList进行校验,判断在遍历ArrayList是否已经被修改,由于之前对modCount+1,而expectedModCount还是初始化时ArrayList.Itr对象时赋的值,所以会不相等,然后抛出ConcurrentModificationException异常。

    可以看到下面Itr的源码中,在Itr.remove()方法中删除元素后会对 expectedModCount更新,所以我们在使用删除元素时使用Itr.remove()方法来删除元素就可以保证expectedModCount的更新了,具体看第5种方法。

    private class Itr implements Iterator<E> {
        int cursor;       // 游标
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;//期待的modCount值
    
        public boolean hasNext() {
            return cursor != size;
        }
    
        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();//判断expectedModCount与当前的modCount是否一致
            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];
        }
    
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;//更新expectedModCount
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
    

    5. 第4种方法 - Iterator遍历,使用ArrayList.remove()删除元素(结果:抛出异常)

    Iterator<Integer> iterator = arrayList.iterator();
    while (iterator.hasNext()) {
        Integer value = iterator.next();
        if (value.equals(3)) {//3是要删除的元素
                arrayList.remove(value);
        }
        System.out.println("当前arrayList是"+arrayList.toString());
    }
    

    输出结果:

    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 4, 5]
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
        at java.util.ArrayList$Itr.next(ArrayList.java:851)
        at com.test.ArrayListTest1.removeWayFour(ArrayListTest1.java:61)
        at com.test.ArrayListTest1.main(ArrayListTest1.java:25)
    

    第3种方法在编译后的代码,其实是跟第4种是一样的,所以第四种写法也会抛出ConcurrentModificationException异常。这种需要注意的是,每次调用iterator的next()方法,会导致游标向右移动,从而达到遍历的目的。所以在单次循环中不能多次调用next()方法,不然会导致每次循环时跳过一些元素

    先调用iterator.next()获取元素,与elem进行比较,如果相等,再调用list.remove(iterator.next());来移除元素,这个时候的iterator.next()其实已经不是与elem相等的元素了,而是后一个元素了,我们可以写个demo来测试一下

    ArrayList<Integer> arrayList = new ArrayList();
    arrayList.add(1);
    arrayList.add(2);
    arrayList.add(3);
    arrayList.add(4);
    arrayList.add(5);
    arrayList.add(6);
    arrayList.add(7);
    
    Integer elem = 3;
    Iterator iterator = arrayList.iterator();
    while (iterator.hasNext()) {
        System.out.println(arrayList);
        if(iterator.next().equals(elem)) {
                arrayList.remove(iterator.next());
        }
    } 
    

    输出结果如下:

    [1, 2, 3, 4, 5, 6, 7]
    [1, 2, 3, 4, 5, 6, 7]
    [1, 2, 3, 4, 5, 6, 7]
    [1, 2, 3, 5, 6, 7]
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
        at java.util.ArrayList$Itr.next(ArrayList.java:851)
        at com.test.ArrayListTest1.main(ArrayListTest1.java:29)
    

    可以看到移除的元素其实不是3,而是3之后的元素,因为调用了两次next()方法,导致游标多移动了。所以应该使用Integer value = iterator.next();将元素取出进行判断。

    6. 第5种方法 - Iterator遍历,使用Iterator的remove删除元素(结果:正确删除)

    Iterator<Integer> iterator = arrayList.iterator();
    while (iterator.hasNext()) {
        Integer value = iterator.next();
        if (value.equals(3)) {//3是需要删除的元素
            iterator.remove();
        }
    }
    

    输出结果:

    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 3, 4, 5]
    当前arrayList是[1, 2, 3, 4, 5]
    当前arrayList是[1, 2, 4, 5]
    当前arrayList是[1, 2, 4, 5]
    当前arrayList是[1, 2, 4, 5]
    

    可以正确删除元素。

    跟第3种和第4种方法的区别在于是使用iterator.remove();来移除元素,而在remove()方法中会对iterator的expectedModCount变量进行更新,所以在下次循环调用iterator.next()方法时,expectedModCount与modCount相等,不会抛出异常。

    二. HashMap遍历时删除元素的几种姿势

    首先结论如下:

    1. 第1种方法 - for-each遍历HashMap.entrySet,使用HashMap.remove()删除(结果:抛出异常)。
    2. 第2种方法-for-each遍历HashMap.keySet,使用HashMap.remove()删除(结果:抛出异常)。
    3. 第3种方法-使用HashMap.entrySet().iterator()遍历删除(结果:正确删除)。

    下面让我们来详细探究一下原因吧!

    HashMap的遍历删除方法与ArrayList的大同小异,只是api的调用方式不同。首先初始化一个HashMap,我们要删除key包含"3"字符串的键值对。

    HashMap<String,Integer> hashMap = new HashMap<String,Integer>();
    hashMap.put("key1",1);
    hashMap.put("key2",2);
    hashMap.put("key3",3);
    hashMap.put("key4",4);
    hashMap.put("key5",5);
    hashMap.put("key6",6);
    

    第1种方法 - for-each遍历HashMap.entrySet,使用HashMap.remove()删除(结果:抛出异常)

    for (Map.Entry<String,Integer> entry: hashMap.entrySet()) {
        String key = entry.getKey();
        if(key.contains("3")){
            hashMap.remove(entry.getKey());
        }
        System.out.println("当前HashMap是"+hashMap+" 当前entry是"+entry);
    }
    

    输出结果:

    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前entry是key1=1
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前entry是key2=2
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前entry是key5=5
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前entry是key6=6
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 当前entry是key3=3
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
        at java.util.HashMap$EntryIterator.next(HashMap.java:1463)
        at java.util.HashMap$EntryIterator.next(HashMap.java:1461)
        at com.test.HashMapTest.removeWayOne(HashMapTest.java:29)
        at com.test.HashMapTest.main(HashMapTest.java:22)
    

    第2种方法-for-each遍历HashMap.keySet,使用HashMap.remove()删除(结果:抛出异常)

    HashMap.remove()删除(结果:抛出异常)
    Set<String> keySet = hashMap.keySet();
    for(String key : keySet){
        if(key.contains("3")){
            keySet.remove(key);
        }
        System.out.println("当前HashMap是"+hashMap+" 当前key是"+key);
    }
    

    输出结果如下:

    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前key是key1
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前key是key2
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前key是key5
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 当前key是key6
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 当前key是key3
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
        at java.util.HashMap$KeyIterator.next(HashMap.java:1453)
        at com.test.HashMapTest.removeWayTwo(HashMapTest.java:40)
        at com.test.HashMapTest.main(HashMapTest.java:23)
    

    第3种方法-使用HashMap.entrySet().iterator()遍历删除(结果:正确删除)

    Iterator<Map.Entry<String, Integer>> iterator  = hashMap.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, Integer> entry = iterator.next();
        if(entry.getKey().contains("3")){
            iterator.remove();
        }
        System.out.println("当前HashMap是"+hashMap+" 当前entry是"+entry);
    }
    

    输出结果:

    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key1=1
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key2=2
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key5=5
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key6=6
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 当前entry是key4=4
    当前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 当前entry是deletekey=3
    

    第1种方法和第2种方法抛出ConcurrentModificationException异常与上面ArrayList错误遍历-删除方法的原因一致,HashIterator也有一个expectedModCount,在遍历时获取下一个元素时,会调用next()方法,然后对expectedModCount和modCount进行判断,不一致就抛出ConcurrentModificationException异常。

    1. ConcurrentModificationException是什么?

    根据ConcurrentModificationException的文档介绍,一些对象不允许并发修改,当这些修改行为被检测到时,就会抛出这个异常。(例如一些集合不允许一个线程一边遍历时,另一个线程去修改这个集合)。

    一些集合(例如Collection, Vector, ArrayList,LinkedList, HashSet, Hashtable, TreeMap, AbstractList, Serialized Form)的Iterator实现中,如果提供这种并发修改异常检测,那么这些Iterator可以称为是"fail-fast Iterator",意思是快速失败迭代器,就是检测到并发修改时,直接抛出异常,而不是继续执行,等到获取到一些错误值时在抛出异常。

    异常检测主要是通过modCount和expectedModCount两个变量来实现的,

    modCount
    集合被修改的次数,一般是被集合(ArrayList之类的)持有,每次调用add(),remove()方法会导致modCount+1

    expectedModCount
    期待的modCount,一般是被Iterator(ArrayList.iterator()方法返回的iterator对象)持有,一般在Iterator初始化时会赋初始值,在调用Iterator的remove()方法时会对expectedModCount进行更新。(可以看看上面的ArrayList.Itr源码)

    然后在Iterator调用next()遍历元素时,会调用checkForComodification()方法比较modCount和expectedModCount,不一致就抛出ConcurrentModificationException。

    单线程操作Iterator不当时也会抛出ConcurrentModificationException异常。(上面的例子就是)

    2. 总结

    因为ArrayList和HashMap的Iterator都是上面所说的“fail-fast Iterator”,Iterator在获取下一个元素,删除元素时,都会比较expectedModCount和modCount,不一致就会抛出异常。

    所以当使用Iterator遍历元素(for-each遍历底层实现也是Iterator)时,需要删除元素,一定需要使用 Iterator的remove()方法 来删除,而不是直接调用ArrayList或HashMap自身的remove()方法,否则会导致Iterator中的expectedModCount没有及时更新,之后获取下一个元素或者删除元素时,expectedModCount和modCount不一致,然后抛出ConcurrentModificationException异常。

    三. Spring 中,定时任务接口 SchedulingConfigurer

    Spring 中,创建定时任务除了使用@Scheduled 注解外,还可以使用 SchedulingConfigurer。

    @Schedule 注解有一个缺点,其定时的时间不能动态的改变,而基于 SchedulingConfigurer 接口的方式可以做到。SchedulingConfigurer 接口可以实现在@Configuration 类上,同时不要忘了,还需要@EnableScheduling 注解的支持。

    该接口的实现方法如下:

    public void configureTasks(ScheduledTaskRegistrar taskRegistrar)

    其中 ScheduledTaskRegistrar 类的方法有下列几种:

    从方法的命名上可以猜到,方法包含定时任务,延时任务,基于 Cron 表达式的任务,以及 Trigger 触发的任务。

    下面演示了使用方法。

    @Configuration
    @ComponentScan(value = "com.learn")
    @EnableScheduling
    public class Config implements SchedulingConfigurer {
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            taskRegistrar.addFixedRateTask(() -> System.out.println("执行定时任务1: " + new Date()), 1000);
            TriggerTask triggrtTask = new TriggerTask( // 任务内容.拉姆达表达式
                    () -> {System.out.println("执行定时任务2: " + new Date());},
                    // 设置触发器,这里是一个拉姆达表达式,传入的TriggerContext类型,返回的是Date类型
                    triggerContext -> {
                        // 2.3 返回执行周期(Date)
                        return new CronTrigger("*/2 * * * * ?").nextExecutionTime(triggerContext);
                    });
     
            taskRegistrar.addTriggerTask(triggrtTask);
        }
    }
    

    默认的,SchedulingConfigurer 使用的也是单线程的方式,如果需要配置多线程,则需要指定 PoolSize,加入如下代码即可:

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(10);
        taskScheduler.initialize();
        taskRegistrar.setTaskScheduler(taskScheduler);
    }
    

    定时任务配置相关参考文章

    四. java 字符串加一天_如何实现String型时间加一天和减一天

    增加一天和减少一天分别采用了两种方法,喜欢那种用哪种,代码如下:

    import java.text.DateFormat;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.Date;
    public class TestTime {
        public static void main(String[] args) {
            String d = "2004-1-1";
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
            /**
    * 加一天
    */
            try {
                Date dd = df.parse(d);
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(dd);
                calendar.add(Calendar.DAY_OF_MONTH, 1);//加一天
                System.out.println("增加一天之后:" + df.format(calendar.getTime()));
            } catch (ParseException e) {
                e.printStackTrace();
            }
            /**
    
    * 减一天
    
    */
            try {
                long dif = df.parse(d).getTime()-86400*1000;//减一天
                Date date=new Date();
                date.setTime(dif);
                System.out.println("减少一天之后:" + df.format(date));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }
    

    输出地结果:

    增加一天之后:2004-01-02

    减少一天之后:2003-12-31

    注:

    有的人说增加一天和减少一天可以用一个方法就实现了:

    calendar.roll(Calendar.DAY_OF_MONTH, 1);//加一天
    calendar.roll(Calendar.DAY_OF_MONTH, -1);//减一天
    

    一定不要用这个方法,经试验加一天的没问题,减一天的会出现一下问题:当时间为“2013-2-1”时,calendar.roll(Calendar.DAY_OF_MONTH, -1)得到结果的是“2013-2-28”,即这个月的月末一天,换句话说他只能够更改“yyyy-MM-dd”中的“dd”。api的解释是:向指定日历字段添加指定(有符号的)时间量,不更改更大的字段。负的时间量意味着向下滚动。

    五. 413 Request Entity Too Large(请求实体太大)

    我们可以看到请求的body的大小,在Content-Length后显示,Nginx默认的request body为1M,小于我们上传的大小

    解决方案

    找到自己主机的nginx.conf配置文件,打开
    在http{}中加入 client_max_body_size 10m;
    然后重启nginx

    /etc/init.d/nginx restart
    

    六. Oracle中特殊字段

    问题:在表中添加了一个level的字段,导致查询的时候报异常

    CONNECT BY clause required in this query block
    

    解决:level是特殊字段,伪列关键字,重新将字段命名就可解决

    相关文章

      网友评论

          本文标题:210418:ArrayList与HashMap删除元素-定时任

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