Почему примитивный поток не собирает (Collector)?
Я пишу библиотеку для начинающих программистов, поэтому я стараюсь, чтобы API был как можно более чистым.
Одна из вещей, которую должна выполнить моя библиотека, - это выполнить некоторые сложные вычисления в большой коллекции ints или longs. Существует множество сценариев и бизнес-объектов, которые мои пользователи должны вычислить из этих значений, поэтому я подумал, что лучший способ - использовать потоки, чтобы позволить пользователям сопоставлять бизнес-объекты с IntStream
или LongStream
, а затем вычислять вычисления внутри коллектора.
Однако IntStream и LongStream имеют только метод сбора 3-х параметров:
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
Это слишком сложно для моих начинающих пользователей и очень подвержено ошибкам.
Моя работа - сделать статические методы, которые принимают IntStream
или LongStream
как входные данные и скрывают создание и выполнение коллекционера для вас
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, ...);
Потому что вам нужно либо сохранить временную переменную и передать ее статическому методу, либо создать поток внутри статического вызова, который может сбивать с толку, когда он смешивается с другими параметрами для моих вычислений.
Есть ли более чистый способ сделать это, продолжая работать с IntStream
или LongStream
?
Ответы
Ответ 1
Мы фактически создали прототип некоторых Collector.OfXxx
специализаций. То, что мы нашли - в дополнение к очевидной досаде более специализированных типов - заключалось в том, что это было не очень полезно без полного набора примитивно-специализированных коллекций (например, Trove или GS-Collections, но которые JDK делает не иметь). Без IntArrayList, например, Collector.OfInt просто толкает бокс где-то в другом месте - от коллектора до контейнера, который не имеет большого выигрыша и больше поверхности API.
Ответ 2
Возможно, если вместо lambdas используются ссылки на методы, код, необходимый для сбора примитивного потока, не будет казаться сложным.
MyResult result = businessObjs.stream()
.mapToInt( ... )
.collect(
MyComplexComputationBuilder::new,
MyComplexComputationBuilder::add,
MyComplexComputationBuilder::merge)
.build(); //prev collect returns Builder object
В Brian окончательный ответ на этот вопрос он упоминает еще две структуры фреймворка Java, которые имеют примитивные коллекции, которые фактически могут быть использованы с методом сбора в примитивных потоках. Я подумал, что было бы полезно проиллюстрировать некоторые примеры использования примитивных контейнеров в этих рамках с примитивными потоками. Код ниже также будет работать с параллельным потоком.
// 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.
Ответ 3
Я реализовал примитивные коллекторы в своей библиотеке StreamEx (начиная с версии 0.3.0). Существуют интерфейсы IntCollector
, LongCollector
и DoubleCollector
, которые расширяют интерфейс Collector
и специализируются на работе с примитивами. Там есть дополнительная незначительная разница в объединении процедуры, например, как IntStream.collect
принять BiConsumer
вместо BinaryOperator
.
Существует множество предопределенных методов сбора, чтобы объединить числа в строку, хранить в примитивном массиве, до BitSet
, находить min, max, sum, вычислять сводную статистику, выполнять групповые операции и операции разделения. Конечно, вы можете определить своих коллекционеров. Вот несколько примеров использования (предполагается, что у вас есть массив 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()));
Вот простой тест который сравнивает эти коллекторы и обычные коллекторы объектов.
Обратите внимание, что моя библиотека не предоставляет (и не предоставляет в будущем) любые видимые пользователем структуры данных, такие как карты на примитивах, поэтому группировка выполняется в обычном HashMap
. Однако, если вы используете Trove/GS/HFTC/что-то еще, не так сложно написать дополнительные примитивные коллекторы для структур данных, определенных в этих библиотеках, чтобы повысить производительность.
Ответ 4
Преобразуйте примитивные потоки в потоки объектов в коробке, если есть методы, которые вам не хватает.
MyResult result = businessObjs.stream()
.mapToInt( ... )
.boxed()
.collect( new MyComplexComputation(...));
Или не используйте примитивные потоки в первую очередь и работайте с Integer
все время.
MyResult result = businessObjs.stream()
.map( ... ) // map to Integer not int
.collect( new MyComplexComputation(...));
Ответ 5
г. Geotz дал окончательный ответ за то, почему было принято решение не включать специализированных коллекционеров, однако я хотел бы дополнительно изучить, насколько это решение повлияло на производительность.
Я думал, что отправлю результаты в качестве ответа.
Я использовал jmh microbenchmark framework время, затрачиваемое на вычисление вычислений с использованием обоих видов коллекционеров по коллекциям размером 1, 100, 1000, 100 000 и 1 миллион:
@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
Как вы можете видеть, версия примитивного потока немного быстрее, но даже когда в коллекции насчитывается 1 миллион элементов, она только в 0,05 секунды быстрее (в среднем).
Для моего API я лучше придерживаюсь более простых соглашений об объектных потоках и использую версию в штучной упаковке, так как это такое незначительное ограничение производительности.
Спасибо всем, кто прояснил эту проблему.