为什么Intellij Idea建议在使用循环将数组转换为Set时创建中间List?

时间:2018-01-11 12:29:28

标签: java intellij-idea collections

假设我有以下代码:

public Set<String> csvToSet(String src) {
    String[] splitted = src.split(",");
    Set<String> result = new HashSet<>(splitted.length);
    for (String s : splitted) {
        result.add(s);
    }
    return result;
}

所以我需要将数组转换为Set。 并且Intellij Idea建议用Collection.addAll单行替换我的for-each循环,所以我得到:

...
Set<String> result = new HashSet<>(splitted.length);
result.addAll(Arrays.asList(splitted));
return result;

完整的检查信息是:

  

当调用批量方法(例如collection.addAll(listOfX))时,可以在循环中调用某些方法(例如collection.add(x))时,此检查会发出警告。   如果选中复选框“Use Arrays.asList()来包装数组”,则即使原始代码在数组上迭代而批量方法需要Collection,检查也会发出警告。在这种情况下,快速修复操作将使用Arrays.asList()调用自动换行数组。

从检查描述来看,它听起来像预期的那样。

如果我们引用关于将数组转换为Set(How to convert an Array to a Set in Java)的问题的最佳答案,则建议使用相同的衬垫:

Set<T> mySet = new HashSet<T>(Arrays.asList(someArray));

即使从数组创建一个ArrayList是O(1),我也不喜欢创建一个额外的List对象。

通常我信任Intellij检查并假设它没有提供任何效率低下的东西。 但今天我很好奇为什么两者:top SO答案和Intellij Idea(默认设置)建议使用相同的单行程创建无用的中间List对象,而JDK 6后也有Collections.addAll(destCollection, yourArray)

我看到它的唯一原因是(检查和答案)都太旧了。如果是这样,这就是改善intellij想法并给出更多投票给出Collections.addAll()的答案的理由:)

2 个答案:

答案 0 :(得分:4)

提示为什么Intellij不建议Arrays.asList替换

Set<String> result = new HashSet<>(splitted.length);
result.addAll(Arrays.asList(splitted));
return result;

位于source code for HashSet(Collection)

public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

请注意,集的容量的大小不是c

因此,更改在语义上不等同。

不要担心创建List真的便宜。它不是免费的;但你必须在一个真正的性能关键循环中使用它才能注意到。

答案 1 :(得分:1)

我编写了一个小函数来测量将数组添加到HashSet的三种方法的性能,结果如下。

首先使用的所有基本代码生成maxSize数组,其值介于0-100

之间
    int maxSize = 10000000; // 10M values
    String[] s = new String[maxSize];
    Random r = new Random();

    for (int i = 0; i < maxSize; i++) {
        s[i] = "" + r.nextInt(100);
    }

然后是基准功能:

public static void benchmark(String name, Runnable f) {
    Long startTime = System.nanoTime();
    f.run();
    Long endTime = System.nanoTime();
    System.out.println("Total execution time for: " + name + ": " + (endTime-startTime) / 1000000 + "ms");
}

所以第一种方法是使用带循环的代码,10M values使用150ms and 190ms(我为每种方法运行基准测试几次)

    Main.benchmark("Normal loop ", () -> {
        Set<String> result = new HashSet<>(s.length);
        for (String a : s) {
            result.add(a);
        }
    });

其次是使用result.addAll(Arrays.asList(s));,它花费在180ms and 220ms

之间
        Main.benchmark("result.addAll(Arrays.asList(s)): ", () -> {
            Set<String> result = new HashSet<>(s.length);
            result.addAll(Arrays.asList(s));
        });

第三种方式是使用Collections.addAll(result, s);,它花费在170ms and 200ms

之间
    Main.benchmark("Collections.addAll(result, s); ", () -> {
        Set<String> result = new HashSet<>(s.length);
        Collections.addAll(result, s);
    });

现在解释。从运行时复杂性来看,它们都在O(n)中运行,这意味着N values N operations将会运行(基本上会添加N个值)。

从内存复杂性的角度来看,对所有O(N)来说也是如此。只创建了新的HashSet

Arrays.asList(someArray)没有创建 new 数组,只是创建一个对该数组有reference的新对象。你可以在java代码中看到它:

    private final E[] a;

    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }

除此之外,所有addAll方法都将完全按照您的方式执行,for-loop

// addAll method for Collections.addAll(result, s);
public static <T> boolean addAll(Collection<? super T> c, T... elements) {
    boolean result = false;
    for (T element : elements)
        result |= c.add(element);
    return result;
}

// addAll method for result.addAll(Arrays.asList(s));
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

总而言之,运行时差异非常小,IntelliJ提出了一种以更清晰的方式编写代码并减少代码的方法。