Подтвердить что ты не робот

Почему примитивный поток не собирает (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?

4b9b3361

Ответ 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 я лучше придерживаюсь более простых соглашений об объектных потоках и использую версию в штучной упаковке, так как это такое незначительное ограничение производительности.

Спасибо всем, кто прояснил эту проблему.