在 Java 7 和 8 中创建与现有列表不同的列表?

2022-09-02 03:03:12

如果我有:

List<Integer> listInts = { 1, 1, 3, 77, 2, 19, 77, 123, 14, 123... }

在 Java 中,创建仅包含不同值的有效方法是什么?List<Integer> listDistinctIntslistInts

我立即想到的是创建一个包含所有值的调用Set<Integer> setIntslistIntsList<Integer> listDistinctInts = new ArrayList<>(setInts);

但这似乎可能效率低下 - 使用Java 7是否有更好的解决方案?

我没有使用Java 8,但我相信使用它我可以做这样的事情(?):

List<Integer> listDistinctInts = listInts.stream().distinct().collect(Collectors.toList());

这是否比上面的方法更高性能和/或是否有任何更有效的方法在Java 8中执行此操作?

最后,(我知道问多个问题可能会令人不快,但它是直接相关的),如果我只关心不同元素的计数,是否有更有效的方法来获得该值(在Java 7和8中) - 而无需首先创建所有不同元素的列表或集合?listInts

我最感兴趣的是本地Java实现此目的并避免重新发明任何轮子的方法,但如果它们提供更好的清晰度或性能,我会考虑手工编写的代码或库。我已经阅读了这个相关的问题Java - 不同的对象列表,但它并不完全清楚Java 7和8方法之间的性能差异,或者是否有更好的技术?


答案 1

我现在已经从提供的优秀答案中标记了大多数建议的选项。像大多数与性能相关的非平凡问题一样,关于哪个最好的答案是“视情况而定”。

我所有的测试都是用JMH Java Microbenchmarking Harness执行的

这些测试中的大多数都是使用JDK 1.8执行的,尽管我也使用JDK 1.7执行了一些测试,以确保其性能不会有太大差异(几乎相同)。我测试了从到目前为止提供的答案中获取的以下技术:


1. Java 8 Stream - 如果使用 Java8,则使用 I 的解决方案是一种可能性:stream()

public List<Integer> testJava8Stream(List<Integer> listInts) {
    return listInts.stream().distinct().collect(Collectors.toList());
}

优点现代Java 8方法,没有第三方依赖关系

缺点 需要 Java 8


2. 添加到列表 - Victor2748 提出的解决方案,其中构建并添加新列表,当且仅当列表尚未包含该值时。请注意,我还以原始大小(最大可能)预先分配目标列表,以防止任何重新分配:

public List<Integer> testAddingToList(List<Integer> listInts) {
    List<Integer> listDistinctInts = new ArrayList<>(listInts.size());
    for(Integer i : listInts)
    {
        if( !listDistinctInts.contains(i) ) { listDistinctInts.add(i); }
    }
    return listDistinctInts;
}

pros 适用于任何Java版本,无需创建集合然后复制,无需第三方设备

缺点 在构建列表时需要反复检查列表中的现有值


3. GS Collections Fast(现为 Eclipse Collections) - Craig P. Motlin 使用 GS Collections 库及其自定义列表类型提出的解决方案:FastList

public List<Integer> testGsCollectionsFast(FastList listFast)
{
    return listFast.distinct();
}

优点 据报道,非常快速,简单的表达性代码,可在Java 7和8中工作

缺点 需要第三方库和快速列表,而不是常规列表<Integer>


4. GS Collections Adapted - FastList解决方案并没有完全比较同类产品,因为它需要传递给方法而不是一个好的方法,所以我也测试了Craig提出的适配器方法:FastListArrayList<Integer>

public List<Integer> testGsCollectionsAdapted(List<Integer> listInts)
{
    return listAdapter.adapt(listInts).distinct();
}

pros 不需要 FastList,可在 Java 7 和 8 中工作

缺点必须适应列表,所以可能表现不佳,需要第三方库


5. 番石榴不可变集 - 路易斯·瓦瑟曼在评论中提出的方法,卢声远圣远卢在回答中使用番石榴提出的方法:

public List<Integer> testGuavaImmutable(List<Integer> listInts)
{
    return ImmutableSet.copyOf(listInts).asList();
}

优点 据说非常快,可以在Java 7或8中工作

缺点 返回不可变列表,无法处理输入列表中的 null,并且需要第三方库


7. HashSet - 我最初的想法(也由EverV0idulix和Radiodef推荐)

public List<Integer> testHashSet(List<Integer> listInts)
{
    return new ArrayList<Integer>(new HashSet<Integer>(listInts));
}

pros 适用于 Java 7 和 8,没有第三方依赖项

缺点 不保留列表的原始顺序,必须构造集合然后复制到列表。


6. LinkedHashSet - 由于解决方案没有保留原始列表中整数的顺序,我还测试了一个使用LinkedHashSet来保持顺序的版本:HashSet

public List<Integer> testLinkedHashSet(List<Integer> listInts)
{
    return new ArrayList<Integer>(new LinkedHashSet<Integer>(listInts));
}

pros 保留原始排序,适用于 Java 7 和 8,无第三方依赖项

缺点 不太可能像常规哈希集方法一样快


结果

以下是我对各种不同大小的结果(结果按从最慢到最快的顺序排列):listInts

1. 取自 ArrayList 中 100,000 个介于 0-50,000 之间的随机 ints(即大列表,一些重复项)

Benchmark                Mode       Samples     Mean   Mean error    Units

AddingToList            thrpt        10        0.505        0.012    ops/s
Java8Stream             thrpt        10      234.932       31.959    ops/s
LinkedHashSet           thrpt        10      262.185       16.679    ops/s
HashSet                 thrpt        10      264.295       24.154    ops/s
GsCollectionsAdapted    thrpt        10      357.998       18.468    ops/s
GsCollectionsFast       thrpt        10      363.443       40.089    ops/s
GuavaImmutable          thrpt        10      469.423       26.056    ops/s

2. 取自 ArrayList 中 0-50 之间的 1000 个随机 ints(即中等列表,许多重复项)

Benchmark                Mode       Samples     Mean   Mean error    Units

AddingToList            thrpt        10    32794.698     1154.113    ops/s
HashSet                 thrpt        10    61622.073     2752.557    ops/s
LinkedHashSet           thrpt        10    67155.865     1690.119    ops/s
Java8Stream             thrpt        10    87440.902    13517.925    ops/s
GsCollectionsFast       thrpt        10   103490.738    35302.201    ops/s
GsCollectionsAdapted    thrpt        10   143135.973     4733.601    ops/s
GuavaImmutable          thrpt        10   186301.330    13421.850    ops/s

3. 取自 ArrayList 中 0-100 之间的 100 个随机 ints(即小列表,一些重复项)

Benchmark                Mode       Samples     Mean   Mean error    Units

AddingToList            thrpt        10   278435.085    14229.285    ops/s
Java8Stream             thrpt        10   397664.052    24282.858    ops/s
LinkedHashSet           thrpt        10   462701.618    20098.435    ops/s
GsCollectionsAdapted    thrpt        10   477097.125    15212.580    ops/s
GsCollectionsFast       thrpt        10   511248.923    48155.211    ops/s
HashSet                 thrpt        10   512003.713    25886.696    ops/s
GuavaImmutable          thrpt        10  1082006.560    18716.012    ops/s

4. 取自 ArrayList 中 0-50 之间的 10 个随机 ints(即小列表,几个重复项)

Benchmark                Mode       Samples     Mean   Mean error    Units

Java8Stream             thrpt        10  2739774.758   306124.297    ops/s
LinkedHashSet           thrpt        10  3607479.332   150331.918    ops/s
HashSet                 thrpt        10  4238393.657   185624.358    ops/s
GsCollectionsAdapted    thrpt        10  5919254.755   495444.800    ops/s
GsCollectionsFast       thrpt        10  7916079.963  1708778.450    ops/s
AddingToList            thrpt        10  7931479.667   966331.036    ops/s
GuavaImmutable          thrpt        10  9021621.880   845936.861    ops/s

结论

  • 如果您只从列表中获取一次不同的项目,并且列表不是很长,那么这些方法中的任何一个都应该足够了。

  • 最有效的通用方法来自第三方图书馆:GS Collections和Guava的表现令人钦佩。

  • 在选择性能最高的方法时,您可能需要考虑列表的大小和可能的重复项数。

  • 只有当值尚未在其中时,才将其添加到新列表的幼稚方法对于小列表非常有用,但是一旦输入列表中的值超过少数,它就会执行最坏的尝试方法。

  • 在大多数情况下,番石榴方法的效果最快。但请注意这些限制:返回的列表是,输入列表不能包含 null。ImmutableSet.copyOf(listInts).asList()Immutable

  • 该方法执行非第三方方法中最好的方法,通常比Java 8流更好,但对整数进行重新排序(根据您的用例,这可能是也可能不是问题)。HashSet

  • 该方法保持排序,但不足为奇通常比HashSet方法差。LinkedHashSet

  • 当使用具有复杂 HashCode 计算的数据类型列表时,和 方法的性能都会更差,因此,如果您尝试从 中选择不同的 ,请进行自己的分析。HashSetLinkedHashSetFooList<Foo>

  • 如果您已经将GS Collections作为依赖项,那么它的性能非常好,并且比ImmutableList Guava方法更灵活。如果没有将其作为依赖项,则如果选择不同项的性能对应用程序的性能至关重要,则值得考虑添加它。

  • 令人失望的是,Java 8流似乎表现相当差。可能有比我使用的方式更好的方法来编写调用代码,因此当然欢迎评论或其他答案。distinct()

铌。我不是MicroBenchmarking的专家,所以如果有人发现我的结果或方法有缺陷,请通知我,我会努力纠正答案。


答案 2

如果您使用的是 Eclipse Collections(以前称为 GS Collections),则可以使用以下方法。distinct()

ListIterable<Integer> listInts = FastList.newListWith(1, 1, 3, 77, 2, 19, 77, 123, 14, 123);
Assert.assertEquals(
        FastList.newListWith(1, 3, 77, 2, 19, 123, 14),
        listInts.distinct());

使用而不是转换为 Set 然后转换回 List 的优点是保留了原始 List 的顺序,保留了每个元素的第一次出现。它是通过使用 Set 和 List 来实现的。distinct()distinct()

MutableSet<T> seenSoFar = UnifiedSet.newSet();
int size = list.size();
for (int i = 0; i < size; i++)
{
    T item = list.get(i);
    if (seenSoFar.add(item))
    {
        targetCollection.add(item);
    }
}
return targetCollection;

如果无法将原始列表转换为 GS 集合类型,则可以使用 ListAdapter 获取相同的 API。

MutableList<Integer> distinct = ListAdapter.adapt(integers).distinct();

没有办法避免创建集合。尽管如此,UnifiedSet比HashSet更有效,因此会有一些速度优势。

如果您想要的只是不同项目的数量,那么只创建一个集合而不创建列表会更有效。

Verify.assertSize(7, UnifiedSet.newSet(listInts));

Eclipse Collections 8.0 需要 Java 8。Eclipse Collections 7.x与Java 8配合得很好,但只需要Java 5。

注意:我是 Eclipse 集合的提交者。