【问题标题】:Is collectingAndThen method enough efficient?collectAndThen 方法是否足够有效?
【发布时间】:2018-05-28 09:01:20
【问题描述】:

我最近开始使用 collectAndThen,发现与我用于执行类似任务的其他编码程序相比,它花费的时间有点长。

这是我的代码:

        System.out.println("CollectingAndThen");
        Long t = System.currentTimeMillis();
        String personWithMaxAge = persons.stream()
                                        .collect(Collectors.collectingAndThen(
                                                                Collectors.maxBy(Comparator.comparing(Person::getAge)),
                                                                (Optional<Person> p) -> p.isPresent() ? p.get().getName() : "none"
                                                ));


        System.out.println("personWithMaxAge - "+personWithMaxAge + " time taken = "+(System.currentTimeMillis() - t));
        Long t2 = System.currentTimeMillis();
        String personWithMaxAge2 = persons.stream().sorted(Comparator.comparing(Person::getAge).reversed())
                                                    .findFirst().get().name;
        System.out.println("personWithMaxAge2 : "+personWithMaxAge2+ " time taken = "+(System.currentTimeMillis() - t2));

这里是输出:

CollectingAndThen
personWithMaxAge - Peter time taken = 17
personWithMaxAge2 : Peter time taken = 1

这表明collectingAndThen相对而言花费了更多时间。

所以我的问题是 - 我应该继续收集AndThen 还是有其他建议?

【问题讨论】:

  • 这是你衡量事物的方式......
  • @Eugene 这并不是How to write a micro benchmark 的真正副本,尽管应用答案中的建议可能会有所帮助。
  • 为什么不只是persons.stream().max(Comparator.comparing(Person::getAge)).get().name
  • @AndyTurner 我只是想发布:D 此外,您可以将 g‌​et().name 替换为 map(p -&gt; p.name).orElse('none') 以避免 NoSuchElementExceptions。
  • 这种差异几乎可以肯定取决于您如何设置测试。如果你颠倒顺序,然后你先做personWithMaxAge2,会发生什么?

标签: java java-8 java-stream collectors collect


【解决方案1】:

TL;DR;你衡量事物的方式可能已经偏离了。

我使用 JMH 创建了一个更有效的测试性能基准,设置如下(应以不同方式初始化人员列表以进一步增强对结果的信心):

public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder().include(SO.class.getSimpleName()).jvmArgs("-Dfile.encoding=UTF-8").build();
    new Runner(opt).run();
}

public static final class Person {
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    String name;
    int age;
    public int getAge() {return age;};
    public String getName() {return this.name;}
}

public static final List<Person> persons = IntStream.range(0, 100).mapToObj(i -> new Person("person" + i, RandomUtils.nextInt(0, 50))).collect(Collectors.toList());

@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 5, time = 4, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS)
@Fork(2)
public float collectingAndThen() {
    String personWithMaxAge = persons.stream()
            .collect(Collectors.collectingAndThen(
                    Collectors.maxBy(Comparator.comparing(Person::getAge)),
                    (Optional<Person> p) -> p.isPresent() ? p.get().getName() : "none"
            ));
    return personWithMaxAge.length() * 1.0f;
}

@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 5, time = 4, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS)
@Fork(2)
public float sortFindFirst() {
    String personWithMaxAge2 = persons.stream().sorted(Comparator.comparing(Person::getAge).reversed())
            .findFirst().get().name;
    return personWithMaxAge2.length() * 1.0f;
}

本次测试结果毋庸置疑(名单中有100人):

# Run complete. Total time: 00:02:41

Benchmark                        Mode  Cnt        Score       Error  Units
Benchmark.SO.collectingAndThen  thrpt    8  1412304,072 ± 53963,266  ops/s
Benchmark.SO.sortFindFirst      thrpt    8   331214,270 ±  7966,082  ops/s

collectingAndThen 快 4 倍。

如果将人员列表缩小到 5 人,数字会完全改变:

Benchmark                        Mode  Cnt         Score        Error  Units
Benchmark.SO.collectingAndThen  thrpt    8  14529905,529 ± 423196,066  ops/s
Benchmark.SO.sortFindFirst      thrpt    8   7645716,643 ± 538730,614  ops/s

但 collectAndThen 仍然快 2 倍。

我怀疑您的测试已关闭。可能的原因有很多,例如,类加载、JIT 编译和其他预热,...

正如@assylias 在 cmets 中指出的那样,您应该依靠更好的微基准来测量这种“小”方法的时间,以避免前面提到的副作用。见:How do I write a correct micro-benchmark in Java?

【讨论】:

    【解决方案2】:

    不,从效率的角度来看,collectingAndThen 很好。

    考虑使用此代码生成随机整数列表:

    List<Integer> list =
        new Random().ints(5).boxed().collect(Collectors.toList());
    

    您可以使用两种方法从此列表中获取最大值:

        list.stream().collect(Collectors.collectingAndThen(
            Collectors.maxBy(Comparator.naturalOrder()),
            (Optional<Integer> n) -> n.orElse(0)));
    

        list.stream().sorted().findFirst().get();
    

    如果您只是为这两种方法的一次执行计时,您可能会得到类似这样的时间 (ideone):

    collectAndThen 2.884806
    sortFindFirst  1.898522
    

    这些是以毫秒为单位的时间。

    但是继续迭代,你会发现时代发生了巨大的变化。 100 次迭代后:

    collectAndThen 0.00792
    sortFindFirst  0.010873
    

    仍然以毫秒为单位。

    因此,如前所述,您只是没有正确地对这两种方法进行基准测试。

    【讨论】:

      【解决方案3】:

      collectingAndThen 添加一个仅在集合末尾执行的操作。

      所以

      String personWithMaxAge = persons.stream()
          .collect(Collectors.collectingAndThen(
              Collectors.maxBy(Comparator.comparing(Person::getAge)),
              (Optional<Person> p) -> p.isPresent() ? p.get().getName() : "none"
          ));
      

      不一样

      Optional<Person> p = persons.stream()
          .collect(Collectors.maxBy(Comparator.comparing(Person::getAge)));
      String personWithMaxAge = p.isPresent() ? p.get().getName() : "none";
      

      当您使用生成的收集器作为另一个收集器的输入时,在收集器中指定操作的实际优势就会显现出来,例如groupingBy(f1, collectingAndThen(collector, f2)).

      由于这是一个在最后只执行一次的简单操作,因此对性能没有影响。此外,对于任何重要的输入,基于 sorted 的解决方案不太可能比 maxBy 操作更快。

      您只是在使用违反“How do I write a correct micro-benchmark in Java?”中列出的几条规则的损坏基准。最值得注意的是,您正在测量第一次使用 Stream 框架的初始初始化开销。只是交换这两个操作的顺序会给你一个完全不同的结果。

      不过,没有必要让操作变得不必要地复杂。如前所述,收集器镜像现有 Stream 操作的优点是它们可以与其他收集器组合。如果不需要这样的组合,只需使用直接的代码

      String personWithMaxAge = persons.stream()
          .max(Comparator.comparing(Person::getAge))
          .map(Person::getName).orElse("none");
      

      这比使用收集器更简单,比基于sorted 的解决方案更高效。

      【讨论】:

      • 所有这些都是正确的,我将添加另一个我们添加collectingAndThen 的原因,当然我们可以让您收集结果然后自己应用后置函数:因为通过烘焙这个进入Collector,中间集合不会转义到客户端代码中,因此不会被意外使用。
      猜你喜欢
      • 2023-03-15
      • 1970-01-01
      • 2016-12-08
      • 2018-04-11
      • 2010-11-27
      • 1970-01-01
      • 1970-01-01
      • 2011-01-28
      相关资源
      最近更新 更多