2020年8月

实际开发中遇到的问题,仅此记录

map中对Arrays.asList的结果集合进行操作无效。 知识点太久没复习,一不小心就掉咧。完整测试代码

一、出现问题

// 需求要删掉map中key值跟数组元素相同的数据。
Integer[] idsInt = {2, 3};

Map<Integer, String> map = HashMap<>();
map.put(1,"hello");
map.put(2,"world");
map.put(3,"go");
map.put(4,"ahead");

map.keySet().removeIf(v -> Arrays.asList(idsInt).contains(v)); 
// 最后结果应该只有key为1、4的数据。
// 然而结果是所有map原始数据(key1~key4)

二、定位问题

removeIf

这个方法是java8中新增。直接写死参数看结果:
map.keySet().removeIf(v -> v==2); 确实key为2的数据被删了,确实是可以删除数据的,pass继续。

Arrays.asList

  • 因为用到了lambda表达式,分析的时候有点卡-->lambda表达式复习
    v -> Arrays.asList(idsInt).contains(v)
  • 先来分析 ->,这是一个Predicate函数,执行test方法,返回boolean类型。所以完整的v -> Arrays.asList(idsInt).contains(v)执行流程是:

    map.keySet().removeIf(new Predicate<Integer>() {
      @Override
      public boolean test(Integer v) {
          return Arrays.asList(idsInt).contains(v);
      }
    });
  • 来看看Arrays.asList代码

    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
      return new ArrayList<>(a);
    }

    返回了一个ArrayList数组集合,没毛病。

结合代码,如果key存在于该数组中,就返回true,返回true就执行删除操作。为什么没有删掉呢?

再退回来看map.keySet().removeIf代码

default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) { // 在此处调用了lambda表达式,如果包含则返回true
            each.remove();
           removed = true;
        }
   }
    return removed;
}
最终map.keySet().removeIf(v -> Arrays.asList(idsInt).contains(v)); 的代码执行流程如下:
removeIf(Predicate<Integer> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<Integer> each = iterator();
    while (each.hasNext()) {
        // if(filter.test(each.next())) 等于
        if (Arrays.asList(idsInt).contains(each.next())) {
            // 这里调用了迭代器的remove方法对Arrays.asList的结果集进行了remove操作
            // 迭代器使用的注意事项:不能调用集合的remove方法改变集合,否则会ConcurrentModificationException
            each.remove();  
            removed = true;
        }
    }
    return removed;
}
map.keySet().removeIf(Predicate..);

发现问题

  • removeIf方法的if语句中Arrays.asList返回了一个数组集合,数组集合增删查改应该没问题才对。然鹅,当按住ctrl键点击该ArrayList类时,并不是跳转到java.util.ArrayList类!!!
  • 而是跳转到Arrays私有的内部类(这个内部类紧接着asList方法,滑稽),其路径为:java.util.Arrays中的ArrayList内部类

    private static class ArrayList<E> extends AbstractList<E>
          implements RandomAccess, java.io.Serializable {
      xxxx //并没有增删方法
    }
  • 因为该内部类继承了List的实现类AbstractList,所以该内部类ArrayList有List所有定义的方法可以调用,List中存在remove方法的定义,但是在ArrayList内部类中该定义没有被实现。所以就发生了删不了数据的情况!!

三、解决方法

原来的代码:map.keySet().removeIf(v -> Arrays.asList(idsInt).contains(v));可修改为下面代码使用:

第一种方法:
map.keySet().removeIf(v -> {
    for(int j=0;j<idsInt;j++) 
        if(j == v) return true;
    return false;
});
  • 把集合相关的操作都用for循环解决,避免使用Arrays.asList
第二种方法
map.keySet().removeIf(v -> new ArrayList<Integer>(Arrays.asList(idsInt)).contains(v));
  • 还是使用Arrays.asList,但是转换成了java.util.ArrayList类来用

四、效率

long startTime = System.nanoTime();
// 代码(上面两种方法)
long endTime = System.nanoTime();
long duration = endTime - startTime;
System.out.println("use:  " + duration / 1000000);
  • 设置2000000条map数据,idsInt = {2000,13000,57000,1000000},每种方法都跑三遍,发现:

    • for循环的最高时间为:270毫秒 且三次时间都在200~300毫秒内;
    • 集合Arrays.asList最高为369秒,300~400毫秒内。

所以建议使用for循环来做。OVER~~~~,搞完睡觉