collectingAndThen 方法是否足够有效?
Is collectingAndThen method enough efficient?
我最近开始使用 collectingAndThen,发现与我用于执行类似任务的其他编码程序相比,它花费的时间有点长。
这是我的代码:
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 花费的时间相对较长。
所以我的问题是 - 我应该继续 collectAndThen 还是有一些其他建议?
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
但 collectingAndThen 仍然快 2 倍。
我怀疑你的测试有问题。有很多可能的原因,例如,类加载、JIT 编译和其他预热,...
正如@assylias 在评论中指出的那样,您应该依靠精心设计的微基准来测量此类 "small" 方法的时间,以避免前面提到的副作用。参见:How do I write a correct micro-benchmark in Java?
不,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
以毫秒为单位的静止时间。
因此,如前所述,您只是没有正确地对这两种方法进行基准测试。
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,发现与我用于执行类似任务的其他编码程序相比,它花费的时间有点长。
这是我的代码:
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 花费的时间相对较长。
所以我的问题是 - 我应该继续 collectAndThen 还是有一些其他建议?
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
但 collectingAndThen 仍然快 2 倍。
我怀疑你的测试有问题。有很多可能的原因,例如,类加载、JIT 编译和其他预热,...
正如@assylias 在评论中指出的那样,您应该依靠精心设计的微基准来测量此类 "small" 方法的时间,以避免前面提到的副作用。参见:How do I write a correct micro-benchmark in Java?
不,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
以毫秒为单位的静止时间。
因此,如前所述,您只是没有正确地对这两种方法进行基准测试。
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
的解决方案更高效。