为什么原始Stream没有收集(收集器)?

时间:2015-05-18 18:47:41

标签: java java-8 java-stream api-design

我正在为新手程序员编写一个库,所以我试图保持API尽可能干净。

我的库需要做的一件事是对大量的int或long进行一些复杂的计算。我的用户需要从中计算这些值所需的大量场景和业务对象,因此我认为最好的方法是使用流来允许用户将业务对象映射到IntStreamLongStream然后计算收集器内部的计算。

但IntStream和LongStream只有3参数collect方法:

collect(Supplier<R> supplier, ObjIntConsumer<R> accumulator, BiConsumer<R,R> combiner)

并没有collect(Collector)具有的简单Stream<T>方法。

所以不能做

Collection<T> businessObjs = ...
MyResult result = businessObjs.stream()
                              .mapToInt( ... )
                              .collect( new MyComplexComputation(...));

我必须提供这样的供应商,累加器和合并器:

MyResult result = businessObjs.stream()
                              .mapToInt( ... )
                              .collect( 
                                  ()-> new MyComplexComputationBuilder(...),
                                  (builder, v)-> builder.add(v),
                                  (a,b)-> a.merge(b))
                              .build(); //prev collect returns Builder object

这对我的新手用户来说太复杂了,而且非常容易出错。

我的工作是创建以IntStreamLongStream作为输入的静态方法,并为您隐藏收集器的创建和执行

public static MyResult compute(IntStream stream, ...){
       return .collect( 
                        ()-> new MyComplexComputationBuilder(...),
                        (builder, v)-> builder.add(v),
                        (a,b)-> a.merge(b))
               .build();
}

但这并不符合使用Streams的常规惯例:

IntStream tmpStream = businessObjs.stream()
                              .mapToInt( ... );

 MyResult result = MyUtil.compute(tmpStream, ...);

因为你必须保存一个临时变量并将其传递给静态方法,或者在静态调用中创建Stream,当它与我的计算中的其他参数混合时可能会引起混淆。

在使用IntStreamLongStream时,有更简洁的方法吗?

5 个答案:

答案 0 :(得分:24)

我们确实做了一些 gulp.task('fonts:dev', function () { return gulp.src($.mainBowerFiles()) .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}')) .pipe($.flatten()) .pipe(gulp.dest(options.tmp + '/serve/fonts/')); }); 专业化的原型。我们发现 - 除了明显的更专业类型的烦恼之外 - 如果没有完整的原始专用集合(如Trove,或者GS-Collections,但是JDK所做的那样),这并不是非常有用。没有)。例如,没有IntArrayList,Collector.OfInt只是将拳击推送到其他地方 - 从收集器到容器 - 这没有大的胜利,以及更多的API表面。

答案 1 :(得分:6)

如果使用方法引用而不是lambdas,原始流收集所需的代码似乎并不复杂。

MyResult result = businessObjs.stream()
                              .mapToInt( ... )
                              .collect( 
                                  MyComplexComputationBuilder::new,
                                  MyComplexComputationBuilder::add,
                                  MyComplexComputationBuilder::merge)
                              .build(); //prev collect returns Builder object

在Brian的definitive answer to this question中,他提到了另外两个具有原始集合的Java集合框架,这些集合实际上可以与原始流上的collect方法一起使用。我认为说明如何在这些框架中使用原始流的原始容器的一些示例可能是有用的。下面的代码也适用于并行流。

// Eclipse Collections
List<Integer> integers = Interval.oneTo(5).toList();

Assert.assertEquals(
        IntInterval.oneTo(5),
        integers.stream()
                .mapToInt(Integer::intValue)
                .collect(IntArrayList::new, IntArrayList::add, IntArrayList::addAll));

// Trove Collections

Assert.assertEquals(
        new TIntArrayList(IntStream.range(1, 6).toArray()),
        integers.stream()
                .mapToInt(Integer::intValue)
                .collect(TIntArrayList::new, TIntArrayList::add, TIntArrayList::addAll));

注意:我是Eclipse Collections的提交者。

答案 2 :(得分:3)

如果您缺少某些方法,则将原始流转换为盒装对象流。

MyResult result = businessObjs.stream()
                          .mapToInt( ... )
                          .boxed()
                          .collect( new MyComplexComputation(...));

或者首先不要使用原始流,并且一直使用Integer

MyResult result = businessObjs.stream()
                          .map( ... )     // map to Integer not int
                          .collect( new MyComplexComputation(...));

答案 3 :(得分:3)

我已经在我的库StreamEx中实现了原始收集器(从版本0.3.0开始)。接口IntCollectorLongCollectorDoubleCollector扩展了Collector接口,专门用于处理基元。在IntStream.collect等方法接受BiConsumer而不是BinaryOperator的方法中,组合过程还有一个细微差别。

有许多预定义的集合方法可以将数字连接到字符串,存储到基本数组,BitSet,查找最小值,最大值,总和,计算汇总统计信息,执行分组操作和分区操作。当然,您可以定义自己的收藏家。这里有几个用法示例(假设您有int[] input数组和输入数据。

将数字作为字符串与分隔符连接:

String nums = IntStreamEx.of(input).collect(IntCollector.joining(","));

按最后一位分组:

Map<Integer, int[]> groups = IntStreamEx.of(input)
      .collect(IntCollector.groupingBy(i -> i % 10));

分别对正数和负数求和:

Map<Boolean, Integer> sums = IntStreamEx.of(input)
      .collect(IntCollector.partitioningBy(i -> i > 0, IntCollector.summing()));

这是一个简单的benchmark,可以比较这些收藏家和通常的对象收藏家。

请注意,我的库未提供(并且将来不会提供)任何用户可见的数据结构,例如基元上的地图,因此将分组执行到通常的HashMap。但是,如果您使用的是Trove / GS / HFTC /等等,那么为这些库中定义的数据结构编写额外的原始收集器以获得更高的性能并不困难。

答案 4 :(得分:0)

先生。但是,Geotz provided the definitive answer for why the decision was made not to include specialized Collectors,我想进一步调查这个决定对性能的影响程度。

我以为我会将结果作为答案发布。

我使用jmh microbenchmark framework来计算使用两种收集器计算大小为1,100,1000,10000和100万的集合的计算所需的时间:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class MyBenchmark {

@Param({"1", "100", "1000", "100000", "1000000"})
public int size;

List<BusinessObj> seqs;

@Setup
public void setup(){
    seqs = new ArrayList<BusinessObj>(size);
    Random rand = new Random();
    for(int i=0; i< size; i++){
        //these lengths are random but over 128 so no caching of Longs
        seqs.add(BusinessObjFactory.createOfRandomLength());
    }
}
@Benchmark
public double objectCollector() {       

    return seqs.stream()
                .map(BusinessObj::getLength)
                .collect(MyUtil.myCalcLongCollector())
                .getAsDouble();
}

@Benchmark
public double primitiveCollector() {

    LongStream stream= seqs.stream()
                                    .mapToLong(BusinessObj::getLength);
    return MyUtil.myCalc(stream)        
                        .getAsDouble();
}

public static void main(String[] args) throws RunnerException{
    Options opt = new OptionsBuilder()
                        .include(MyBenchmark.class.getSimpleName())
                        .build();

    new Runner(opt).run();
}

}

结果如下:

# JMH 1.9.3 (released 4 days ago)
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.sample.MyBenchmark.objectCollector

# Run complete. Total time: 01:30:31

Benchmark                        (size)  Mode  Cnt          Score         Error  Units
MyBenchmark.objectCollector           1  avgt  200        140.803 ±       1.425  ns/op
MyBenchmark.objectCollector         100  avgt  200       5775.294 ±      67.871  ns/op
MyBenchmark.objectCollector        1000  avgt  200      70440.488 ±    1023.177  ns/op
MyBenchmark.objectCollector      100000  avgt  200   10292595.233 ±  101036.563  ns/op
MyBenchmark.objectCollector     1000000  avgt  200  100147057.376 ±  979662.707  ns/op
MyBenchmark.primitiveCollector        1  avgt  200        140.971 ±       1.382  ns/op
MyBenchmark.primitiveCollector      100  avgt  200       4654.527 ±      87.101  ns/op
MyBenchmark.primitiveCollector     1000  avgt  200      60929.398 ±    1127.517  ns/op
MyBenchmark.primitiveCollector   100000  avgt  200    9784655.013 ±  113339.448  ns/op
MyBenchmark.primitiveCollector  1000000  avgt  200   94822089.334 ± 1031475.051  ns/op

正如您所看到的,原始Stream版本稍微快一些,但即使集合中有100万个元素,它的速度也只有0.05秒(平均而言)。

对于我的API,我宁愿保持更清晰的对象流约定并使用盒装版本,因为它是一个很小的性能损失。

感谢大家深入了解这个问题。