为啥 Intellij Idea 建议在使用循环将数组转换为 Set 时创建中间列表?

Posted

技术标签:

【中文标题】为啥 Intellij Idea 建议在使用循环将数组转换为 Set 时创建中间列表?【英文标题】:Why Intellij Idea suggests to create intermediate List while converting array to Set using loop?为什么 Intellij Idea 建议在使用循环将数组转换为 Set 时创建中间列表? 【发布时间】:2018-06-20 18:51:50 【问题描述】:

假设我有以下代码:

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 建议将我的 for-each 循环替换为 Collection.addAll one-liner 所以我得到:

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

完整的检查信息是:

在调用批量方法(例如 collection.addAll(listOfX) 时,可以替换循环中调用某些方法(例如 collection.add(x))时的此检查警告。 如果选中复选框“使用 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 检查并假设它不会提供任何效率较低的东西。 但今天我很好奇为什么两者:*** SO 答案和 Intellij Idea(使用默认设置)建议使用相同的单线创建无用的中间 List 对象,而自 JDK 6 以来还有一个 Collections.addAll(destCollection, yourArray)

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

【问题讨论】:

你在问什么?哪个最好?第三个。 @AndyTurner 哪个最好,哪里最好 = 最高性能? 使用asList(array) 只是在数组周围包装一些方便的行为;这样做允许您使用HashSet 构造函数,以便您可以创建集合并一步填充它。我不明白为什么会有任何性能下降。 @Derp 最好,因为它是最简洁的,并且创建该列表的性能影响 - 如果有的话 - 不值得担心。试图不创建列表是(可能没有根据的)微优化。这里经常重复的建议是:编写最易读的代码;如果您发现性能不足,请对其进行分析,并且只有当您发现 this 是瓶颈时,您才应该担心重写它。 @khelwood with for,没有对 Iterator 方法的调用,因此使用的委托较少,因此可能某些编译器循环优化在原始代码中效果更好(即软件流水线 en.wikipedia.org/wiki/Software_pipelining )。我认为 IntelliJ 做得不好 【参考方案1】:

关于为什么 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真的便宜。它不是免费的;但是您必须在真正对性能至关重要的循环中使用它才能注意到。

【讨论】:

这个包装很便宜,除非你以一种对性能非常敏感的方式使用它 哎哟 :-) 看来我真的需要更多的睡眠! :-) 这个答案表明,当 OP 尝试过早优化时,他们反而进行了 悲观化。 HashSet 构造函数参数的值不正确,最终创建的 HashSet 太小,更容易发生冲突。 API 开发人员更了解如何优化初始化 HashSet 以保存给定的集合,因此请利用他们的知识。此外,通过重用常用的 API 方法,您更有可能对其进行 JIT 编译。 有趣的答案,但 imo 有点离题了。 @KlitosKyriacou 我没有被提供以任何方式使用构造函数。我被提议使用 Set 的实例 addAll,它需要集合作为参数,而有一个静态方法 Collections.addAll(yourDestCollection, yourSrcArr) @KlitosKyriacou 关闭。不正确的初始大小确实太小了,但结果不会是更多的碰撞。相反,必须通过添加元素中途重新分配表。但是,是的,使用 HashSet(Collection) 构造函数可能是最好的方法。【参考方案2】:

我写了一个小函数来测量将数组添加到 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 提出了一种以更清晰和更少代码编写代码的方法。

【讨论】:

System.currentTimeMillis() 不是单调的。请改用System.nanoTime()(或JMH)。 @KlitosKyriacou 在每次运行 100 次迭代的平均值后: 有趣的结果。看起来显式循环毕竟是最快的。 考虑 HashSet 构造函数参数,数组大小的值并不太低在这个特定的测试案例中。事实上,它太高了!您要添加 1000 万个元素,但它们是 0 到 99 之间的随机数。因此,绝大多数是重复的,并且 HashSet 最终最多只有 100 个元素。 @KlitosKyriacou 我怀疑循环技术是否会产生重大影响。答案中列出的初始基准运行的高差异告诉我,很难从任何结果中得出结论。需要更精确的基准测试方法。

以上是关于为啥 Intellij Idea 建议在使用循环将数组转换为 Set 时创建中间列表?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 IntelliJ 建议将调用链转换为序列?

为啥 Intellij IDEA 说已使用的属性未使用?

如何用intellij idea 12 写一个helloworld,为啥新建不出来类啥的呢

intellij idea解析本地maven仓库无效是为啥

为啥 Intellij Idea 的嵌入式终端中只有 75 个可见字符?

为啥intellij idea安装eclipse code formatter插件装不上