Потоки Java 8 - збирайте та зменшуйте


143

Коли ви використовуєте collect()vs reduce()? Хтось має хороші, конкретні приклади, коли, безумовно, краще йти тим чи іншим шляхом?

Згадки Javadoc, які збирають (), - це змінне зменшення .

Зважаючи на те, що це зменшене скорочення, я припускаю, що це потребує синхронізації (внутрішньо), що, в свою чергу, може завдати шкоди продуктивності. Імовірно, reduce()це більш легко паралелізується ціною необхідності створення нової структури даних для повернення після кожного кроку зменшення.

Наведені вище твердження є здогадками, але я хотів би, щоб експерт прислухався сюди.


1
Інша частина сторінки, на яку ви пов’язані, пояснює це: Як і у випадку зменшення (), користь вираження колекції таким абстрактним способом полягає в тому, що це прямо піддається паралелізації: ми можемо накопичувати часткові результати паралельно, а потім комбінувати їх до тих пір, поки функції накопичення та комбінування відповідають відповідним вимогам.
JB Nizet

1
також дивіться "Потоки в Java 8: Зменшити проти збирання" від Angelika Langer - youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe2

Відповіді:


115

reduceє операцією " складання ", вона застосовує двійковий оператор до кожного елемента потоку, де перший аргумент для оператора - це повернене значення попередньої програми, а другий аргумент - поточний елемент потоку.

collect- це операція агрегації, де створюється "колекція" і кожен елемент "додається" до цієї колекції. Потім колекції в різних частинах потоку додаються разом.

Документ , який ви пов'язані дає причину , що має два різних підходи:

Якби ми хотіли взяти потік рядків і об'єднати їх у одну довгу струну, ми могли б досягти цього звичайним скороченням:

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

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

Тож справа в тому, що паралелізація однакова в обох випадках, але у reduceвипадку, коли ми застосовуємо функцію до самих елементів потоку. У collectвипадку, коли ми застосуємо функцію до змінного контейнера.


1
Якщо це стосується збору: "Більш ефективним підходом було б накопичення результатів у StringBuilder", то чому б ми коли-небудь використовувати зменшення?
jimhooker2002

2
@ Jimhooker2002 перечитайте його. Якщо ви, скажімо, обчислюєте добуток, то функцію відновлення можна просто застосувати до розділених потоків паралельно, а потім об'єднати разом у кінці. Процес зменшення завжди призводить до типу типу потоку. Збір застосовується, коли потрібно збирати результати в контейнер, що змінюється, тобто коли результат є іншим типом потоку. Це має перевагу в тому, що один екземпляр контейнера може бути використаний для кожного розділеного потоку, але недолік, що контейнери потрібно комбінувати в кінці.
Борис Павук

1
@ jimhooker2002 у прикладі продукту intє незмінним, тому ви не можете легко використовувати операцію збирання. Ви можете зробити брудний злом, як, наприклад, використовувати AtomicIntegerабо якийсь звичай, IntWrapperале чому б це зробити? Операція складання просто відрізняється від операції збирання.
Борис Павук

17
Існує також інший reduceметод, коли можна повернути об'єкти типу, відмінного від елементів потоку.
damluar

1
ще один випадок, коли u використовує збирання замість скорочення, коли операція зменшення включає додавання елементів до колекції, то кожен раз, коли ваша функція акумулятора обробляє елемент, вона створює нову колекцію, що включає елемент, який є неефективним.
raghu

40

Причина просто в тому, що:

  • 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.

Якщо ви хочете використовувати BigDecimalзамість цього, containerви не могли б використовувати collect()метод, оскільки container.add(employee.getSalary());не змінили б, containerоскільки BigDecimalвін незмінний. (Окрім цього, BigDecimal::newце не спрацює, оскільки BigDecimalне має порожнього конструктора)


2
Зауважте, що ви використовуєте Integerконструктор ( new Integer(6)), який застаріло в наступних версіях Java.
MC імператор

1
Гарний улов @MCEmperor! Я змінив це наInteger.valueOf(6)
Сандро

@Sandro - я розгублений. Чому ви кажете, що збирати () працює лише із змінними об'єктами? Я використовував це для об'єднання рядків. Рядок allNames = службовці.stream () .map (Співробітник :: getNameString) .collect (Collectors.joining (",")) .toString ();
MasterJoe2

1
@ MasterJoe2 Це просто. Якщо коротко - реалізація все ще використовує те, StringBuilderщо є змінним. Дивіться: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…
Сандро

30

Нормальне зменшення має на увазі поєднання двох незмінних значень, таких як 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це не безпечно для потоків, і не безпечно додавати / видаляти значення з нього під час ітерації, тож ви отримаєте одночасне виключення ArrayIndexOutOfBoundsExceptionабо будь-який виняток (особливо при паралельному виконанні), коли ви оновлюєте список або комбінатор намагається об'єднати списки, оскільки ви мутуєте список, накопичуючи (додаючи) цілі числа до нього. Якщо ви хочете зробити цей потік безпечним, вам потрібно кожного разу переходити новий список, що може погіршити продуктивність.

На відміну від цього, Collectors.toList()твори аналогічно. Однак це гарантує безпеку потоку під час накопичення значень у списку. З документації щодо collectспособу :

Виконує операцію скорочення змінних елементів цього потоку за допомогою колектора. Якщо потік паралельний, а колектор паралельний, або потік не упорядкований, або колектор не упорядкований, тоді буде здійснено одночасне зменшення. При паралельному виконанні декілька проміжних результатів можуть бути інстанційними, заселеними та об'єднаними, щоб підтримувати ізоляцію змінних структур даних. Тому навіть при виконанні паралельно зі структурами даних, що не є безпечними для потоків (такими як ArrayList), додаткова синхронізація не потрібна для паралельного скорочення.

Отже, щоб відповісти на ваше запитання:

Коли ви використовуєте collect()vs reduce()?

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


У фрагменті коду я думаю, що проблема полягає в тому, що він займе ідентичність (в даному випадку - один екземпляр ArrayList) і припустимо, що він "незмінний", щоб вони могли запускати xнитки, кожен "додаючи ідентичність", а потім поєднуючись разом. Хороший приклад.
rogerdpack

чому ми отримаємо виняток одночасних модифікацій, виклики потоків просто повернуть послідовний потік, а це означає, що його буде оброблено єдиним потоком, а функція комбінатора взагалі не викликається?
amarnath harish

public static void main(String[] args) { List<Integer> l = new ArrayList<>(); l.add(1); l.add(10); l.add(3); l.add(-3); l.add(-4); List<Integer> numbers = l.stream().reduce( new ArrayList<Integer>(), (List<Integer> l2, Integer e) -> { l2.add(e); return l2; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; });for(Integer i:numbers)System.out.println(i); } }Я намагався і не отримав виключення CCm
amarnath зібрався

@amarnathharish Проблема виникає, коли ви намагаєтеся запустити її паралельно, і кілька потоків намагаються отримати доступ до одного списку
george

11

Нехай потік буде a <- b <- c <- d

У скороченні,

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

де # - та цікава робота, яку ви хотіли б зробити.

У колекції

у вашого колекціонера буде якась колекційна структура К.

K споживає a. K тоді споживає b. K тоді споживає c. K тоді споживає d.

Наприкінці ви запитуєте K, який кінцевий результат.

K потім віддає це вам.


2

Вони сильно відрізняються потенційним слідом пам’яті під час виконання. Поки 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 (функціонального програмування) і не може оптимізувати дані, які не використовуються внизу потоку через можливі побічні ефекти .


3
Якщо вам не байдуже результати економії на базі даних, вам слід скористатися forEach ... вам не потрібно використовувати скорочення. Якщо тільки це не було для ілюстративних цілей.
DaveEdelstein

2

Ось приклад коду

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

Функція зменшення обробляє два параметри, перший параметр - це попереднє значення повернення в потоці, другий параметр - поточне значення обчислення в потоці, він підсумовує перше значення і значення струму як перше значення в наступній какуляції.


0

Згідно з док

Колектори, що відновлюють (), є найбільш корисними при використанні в багаторівневому зменшенні, нижче за течією групиBy або розділенняBy. Щоб виконати просте зменшення потоку, використовуйте натомість 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якщо це можливо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.