Java 8 Streams - собирать и уменьшать

Когда вы используете collect() vs reduce()? Кто-нибудь имеет хорошие, конкретные примеры того, когда определенно лучше идти так или иначе?

Javadoc упоминает, что collect() - изменяемое изменение.

Учитывая, что это изменчивое сокращение, я предполагаю, что для этого требуется синхронизация (внутренне), которая, в свою очередь, может нанести ущерб производительности. Предположительно reduce() более легко распараллеливается за счет необходимости создания новой структуры данных для возврата после каждого шага сокращения.

Вышеприведенные утверждения - это догадки, и мне бы хотелось, чтобы эксперт звонил здесь.

Ответ 1

reduce - это операция " сворачивания ", она применяет бинарный оператор к каждому элементу в потоке, где первый аргумент оператора - это возвращаемое значение предыдущего приложения, а второй аргумент - текущий элемент потока.

collection - это операция агрегации, в которой создается "коллекция" и каждый элемент "добавляется" в эту коллекцию. Коллекции в разных частях потока затем добавляются вместе.

В документе, на который вы ссылаетесь, есть два разных подхода:

Если бы мы хотели взять поток строк и объединить их в одну длинную строку, мы могли бы добиться этого с помощью обычного сокращения:

 String concatenated = strings.reduce("", String::concat)  

Мы получили бы желаемый результат, и он даже работал бы параллельно. Тем не менее, мы не можем быть счастливы от производительности! Такая реализация будет выполнять большое количество операций копирования строк, а время выполнения будет равно O (n ^ 2) в количестве символов. Более производительным подходом было бы накапливать результаты в StringBuilder, который является изменяемым контейнером для накопления строк. Мы можем использовать ту же технику для распараллеливания изменчивого сокращения, как мы делаем с обычным сокращением.

Итак, дело в том, что распараллеливание одинаково в обоих случаях, но в reduce случае мы применяем функцию к самим элементам потока. В случае collect мы применяем функцию к изменяемому контейнеру.

Ответ 2

Причина в том, что:

  • collect() может работать только с изменяемыми объектами результата.
  • reduce() предназначен для работы с неизменяемыми объектами результата.

Пример " reduce() с неизменным"

public class Employee {
  private Integer salary;
  public Employee(String aSalary){
    this.salary = new Integer(aSalary);
  }
  public Integer getSalary(){
    return this.salary;
  }
}

@Test
public void testReduceWithImmutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));
  list.add(new Employee("3"));

  Integer sum = list
  .stream()
  .map(Employee::getSalary)
  .reduce(0, (Integer a, Integer b) -> Integer.sum(a, b));

  assertEquals(Integer.valueOf(6), sum);
}

Пример " collect() с изменяемым"

Например, если вы хотите вручную рассчитать сумму с помощью collect() она не может работать с BigDecimal но только с MutableInt из org.apache.commons.lang.mutable. Увидеть:

public class Employee {
  private MutableInt salary;
  public Employee(String aSalary){
    this.salary = new MutableInt(aSalary);
  }
  public MutableInt getSalary(){
    return this.salary;
  }
}

@Test
public void testCollectWithMutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));

  MutableInt sum = list.stream().collect(
    MutableInt::new, 
    (MutableInt container, Employee employee) -> 
      container.add(employee.getSalary().intValue())
    , 
    MutableInt::add);
  assertEquals(new MutableInt(3), sum);
}

Это работает, потому что аккумулятор container.add(employee.getSalary().intValue()); не должен возвращать новый объект с результатом, но должен изменять состояние изменяемого container типа MutableInt.

Если вы хотите использовать вместо container BigDecimal вы не можете использовать метод collect() как container.add(employee.getSalary()); не изменил бы container потому что BigDecimal это неизменный. (Кроме этого BigDecimal::new не будет работать, так как BigDecimal не имеет пустого конструктора)

Ответ 3

Под обычным сокращением подразумевается объединение двух неизменных значений, таких как int, double и т.д., И создание нового значения; это неизменное сокращение. В отличие от этого, метод сбора предназначен для изменения контейнера с целью накопления результата, который он должен произвести.

Чтобы проиллюстрировать проблему, предположим, что вы хотите достичь Collectors.toList() используя простое сокращение, как показано ниже

    List<Integer> numbers = stream.reduce( new ArrayList<Integer>(), 
    (List<Integer> l, Integer e) -> {
     l.add(e); 
     return l; 
    },
     (List<Integer> l1, List<Integer> l2) -> { 
    l1.addAll(l2); return l1; });

Это эквивалент Collectors.toList(). Однако в этом случае вы изменяете List<Integer>. Как мы знаем, ArrayList не является потокобезопасным и не может безопасно добавлять/удалять значения из него во время итерации, поэтому при обновлении списка вы получите либо параллельное исключение, либо исключение arrayIndexOutBound, либо любое другое исключение (особенно при параллельном запуске). или объединитель пытается объединить списки, потому что вы изменяете список, накапливая (добавляя) целые числа к нему. Если вы хотите сделать этот потокобезопасным, вам нужно каждый раз передавать новый список, что ухудшит производительность.

Напротив, Collectors.toList() работает аналогичным образом. Тем не менее, это гарантирует безопасность потоков, когда вы накапливаете значения в списке. Из документации по методу collect:

Выполняет изменяемую операцию сокращения над элементами этого потока, используя Collector. Если поток параллелен, а коллектор является параллельным, и либо поток неупорядочен, либо коллектор неупорядочен, то будет выполнено одновременное сокращение. При параллельном выполнении несколько промежуточных результатов могут быть созданы, заполнены и объединены, чтобы обеспечить изоляцию изменяемых структур данных. Следовательно, даже при параллельном выполнении с не поточно-ориентированными структурами данных (такими как ArrayList) дополнительная синхронизация не требуется. ссылка на сайт

Итак, чтобы ответить на ваш вопрос:

Когда бы вы использовали collect() против reduce()?

если у вас есть неизменяемые значения, такие как ints, doubles, Strings то нормальное сокращение работает просто отлично. Однако, если вам нужно reduce ваши значения, скажем, до List (изменяемой структуры данных), вам нужно использовать изменяемое сокращение с методом collect.

Ответ 4

Пусть поток будет < -b < -c < -d

При уменьшении

у вас будет ((a # b) # c) # d

где # - интересная операция, которую вы хотели бы сделать.

В коллекции

у вашего коллекционера будет какая-то структура сбора K.

K потребляет a. K затем потребляет b. K затем потребляет c. K затем потребляет d.

В конце вы спрашиваете K, каков окончательный результат.

K затем дает его вам.

Ответ 5

Они отличаются очень в потенциальном объеме памяти во время выполнения. Пока collect() собирает и помещает в коллекцию все данные, reduce() явно просит указать, как уменьшить данные, которые сделали это через поток.

Например, если вы хотите прочитать некоторые данные из файла, обработать его и поместить в какую-нибудь базу данных, вы можете получить код потока java, подобный этому:

streamDataFromFile(file)
            .map(data -> processData(data))
            .map(result -> database.save(result))
            .collect(Collectors.toList());

В этом случае мы используем collect(), чтобы заставить java передавать данные через и сохранить результат в базе данных. Без collect() данные никогда не читаются и никогда не сохраняются.

Этот код с радостью генерирует ошибку времени выполнения java.lang.OutOfMemoryError: Java heap space, если размер файла достаточно велик или размер кучи достаточно низкий. Очевидная причина заключается в том, что он пытается стекать все данные, которые сделали его через поток (и, фактически, уже сохранен в базе данных), в результирующую коллекцию, и это взрывает кучу.

Однако, если вы замените collect() на reduce() - это больше не будет проблемой, так как последнее уменьшит и отбросит все данные, которые его выполнили.

В представленном примере просто замените collect() на что-то с помощью reduce:

.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);

Вам не нужно даже заботиться о том, чтобы вычисление зависело от result, поскольку Java не является чистым FP (функциональным программированием) и не может оптимизировать данные, которые не используются в нижней части потока, потому что возможных побочных эффектов.

Ответ 6

Вот пример кода

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().reduce((x,y) -> {
        System.out.println(String.format("x=%d,y=%d",x,y));
        return (x + y);
    }).get();

System.out.println (сумма);

Вот результат выполнения:

x=1,y=2
x=3,y=3
x=6,y=4
x=10,y=5
x=15,y=6
x=21,y=7
28

Функция Reduce обрабатывает два параметра, первый параметр - это предыдущее возвращаемое значение в потоке, второй параметр - текущее вычисляемое значение в потоке, оно суммирует первое значение и текущее значение как первое значение в следующей операции вычисления.

Ответ 7

Согласно документам

Коллекторы Reduction() наиболее полезны, когда используются в многоуровневом редукции, ниже по течению от groupingBy или partitioningBy. Чтобы выполнить простое сокращение потока, используйте вместо этого Stream.reduce(BinaryOperator).

Таким образом, в основном вы будете использовать reducing() только в принудительном порядке при сборе. Вот еще один пример:

 For example, given a stream of Person, to calculate the longest last name 
 of residents in each city:

    Comparator<String> byLength = Comparator.comparing(String::length);
    Map<String, String> longestLastNameByCity
        = personList.stream().collect(groupingBy(Person::getCity,
            reducing("", Person::getLastName, BinaryOperator.maxBy(byLength))));

Согласно этому уроку уменьшение иногда менее эффективно

Операция сокращения всегда возвращает новое значение. Однако функция аккумулятора также возвращает новое значение каждый раз, когда обрабатывает элемент потока. Предположим, что вы хотите уменьшить элементы потока до более сложного объекта, такого как коллекция. Это может снизить производительность вашего приложения. Если ваша операция сокращения включает добавление элементов в коллекцию, то каждый раз, когда ваша функция-накопитель обрабатывает элемент, она создает новую коллекцию, которая включает этот элемент, что неэффективно. Вместо этого было бы более эффективно обновить существующую коллекцию. Вы можете сделать это с помощью метода Stream.collect, который описан в следующем разделе...

Таким образом, идентичность "повторно используется" в сценарии сокращения, так .reduce если это возможно, немного более эффективно использовать .reduce.