it-swarm.com.ru

Java 8 потоков - сбор против сокращения

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

Javadoc упоминает, что collect () является изменяемым сокращением .

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

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

115
jimhooker2002

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

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

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

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

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

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

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

96
Boris the Spider

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

  • 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() with mutable"

Например. если вы хотите вручную рассчитать сумму с помощью 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.

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

30
Sandro

Под обычным сокращением подразумевается объединение двух неизменяемых значений, таких как 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) дополнительная синхронизация не требуется. link знак равно

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

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

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

20
george

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

В сокращении,

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

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

В коллекции,

ваш коллекционер будет иметь какую-то коллекционную структуру K.

К потребляет. К тогда потребляет б. K затем потребляет c. K затем потребляет d.

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

К затем дает это вам.

7
Yan Ng

Они очень различаются по потенциальному объему памяти во время выполнения. В то время как collect() собирает и помещает all данные в коллекцию, 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 (функциональное программирование) и не может оптимизировать данные, которые не используются. на дне потока из-за возможных побочных эффектов.

2
averasko

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

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 обрабатывает два параметра, первый параметр - это предыдущее возвращаемое значение в потоке, второй параметр - текущее вычисляемое значение в потоке, оно суммирует первое значение и текущее значение в качестве первого значения в следующей операции вычисления.

1
JetQin

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

Коллекторы 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, если это возможно.

0
rogerdpack