为啥 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 idea 12 写一个helloworld,为啥新建不出来类啥的呢