CodeGym /Kursy /JAVA 25 SELF /Metody reduce i collect: agregacja danych

Metody reduce i collect: agregacja danych

JAVA 25 SELF
Poziom 31 , Lekcja 1
Dostępny

1. Metoda reduce: uniwersalna redukcja

W programowaniu często trzeba „zwinąć” kolekcję do jednej wartości końcowej: policzyć sumę, znaleźć iloczyn, skleić ciągi, wyliczyć zagregowaną metrykę lub zebrać elementy do nowej struktury. Kiedyś robiło się to ręcznie pętlami i zmienną akumulatora. Dziś Stream API oferuje eleganckie sposoby – uniwersalne metody reduce i collect, które pozwalają pisać kod zwięźle i deklaratywnie.

  • reduce – zwija strumień do jednej wartości końcowej (suma, iloczyn, konkatenacja itp.).
  • collect – przekształca strumień w kolekcję, ciąg, mapę lub dowolną strukturę.

Przejdźmy po kolei.

Po co jest reduce?

reduce to metoda terminalna, która „zwija” elementy strumienia do jednej wartości, używając funkcji akumulatora. Myślowo wygląda to jak przejście po kolekcji ze stopniowym gromadzeniem wyniku.

Sygnatury metody reduce

W Stream API są trzy podstawowe warianty reduce():

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
  • accumulator – funkcja przyjmująca bieżącą zebrana wartość i kolejny element, a zwracająca nowy wynik.
  • identity – wartość początkowa akumulatora (np. 0 dla sumy, 1 dla iloczynu).
  • combiner – używany w strumieniach równoległych do łączenia wyników pośrednich.

Przykłady użycia reduce

Przykład 1: Suma liczb

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

// reduce bez identity – wynik Optional
Optional<Integer> sum1 = numbers.stream()
    .reduce((a, b) -> a + b);

System.out.println(sum1.orElse(0)); // 15

// reduce z identity – wynik zawsze istnieje
int sum2 = numbers.stream()
    .reduce(0, (a, b) -> a + b);

System.out.println(sum2); // 15

Przykład 2: Iloczyn wszystkich liczb

int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);
System.out.println(product); // 120

Przykład 3: Konkatenacja ciągów

List<String> words = List.of("Java", "Stream", "API");
String phrase = words.stream()
    .reduce("", (a, b) -> a + " " + b);
System.out.println(phrase.trim()); // Java Stream API

Przykład 4: Wyszukiwanie elementu maksymalnego

Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);
max.ifPresent(System.out::println); // 5

Przykład 5: Suma długości wszystkich ciągów

List<String> texts = List.of("kot", "pies", "słoń");
int totalLength = texts.stream()
    .map(String::length)
    .reduce(0, Integer::sum);
System.out.println(totalLength); // 11

Jak działa reduce

Logika reduce jest równoważna następującej pętli:

T result = identity;
for (T element : collection) {
    result = accumulator.apply(result, element);
}
return result;

Jeśli nie podano identity, to wartością startową jest pierwszy element strumienia, a metoda zwraca Optional (może być pusty, gdy strumień jest pusty).

2. Metoda collect: uniwersalne przekształcanie

collect to metoda terminalna, która zamienia strumień w kolekcję, ciąg, mapę lub inną strukturę. Służą do tego „kolektory” (Collector), opisujące proces zbierania. Najczęściej korzystamy z gotowych z klasy Collectors.

Najpopularniejsze kolektory

  • Collectors.toList() – zbiera elementy do List.
  • Collectors.toSet() – zbiera elementy do Set.
  • Collectors.toMap() – zbiera elementy do Map.
  • Collectors.joining() – skleja ciągi w jeden.
  • Collectors.groupingBy() – grupuje elementy według cechy.
  • Collectors.counting() – liczy liczbę elementów.
  • Collectors.summarizingInt() – zbiera statystyki dla liczb (suma, średnia, min/maks).

Przykłady użycia collect

Przykład 1: Zbieranie do listy

List<String> names = List.of("Anna", "Bob", "Wiktor", "Anna");
List<String> uniqueNames = names.stream()
    .distinct()
    .collect(Collectors.toList());
System.out.println(uniqueNames); // [Anna, Bob, Wiktor]

Przykład 2: Zbieranie do zbioru (Set)

Set<String> nameSet = names.stream()
    .collect(Collectors.toSet());
System.out.println(nameSet); // [Anna, Bob, Wiktor] (kolejność nie jest gwarantowana)

Przykład 3: Zbieranie do ciągu

String csv = names.stream()
    .collect(Collectors.joining(", "));
System.out.println(csv); // Anna, Bob, Wiktor, Anna

Przykład 4: Zbieranie do Map

Załóżmy, że mamy klasę:

public class Employee {
    private String name;
    private String department;
    public Employee(String name, String department) {
        this.name = name;
        this.department = department;
    }
    public String getName() { return name; }
    public String getDepartment() { return department; }
}

Zbierzmy mapę „imię → dział”:

List<Employee> employees = List.of(
    new Employee("Anna", "IT"),
    new Employee("Bob", "HR"),
    new Employee("Wiktor", "IT")
);

Map<String, String> nameToDept = employees.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        Employee::getDepartment,
        (oldValue, newValue) -> newValue // obsługa duplikatów imion
    ));

System.out.println(nameToDept); // {Anna=IT, Bob=HR, Wiktor=IT}

Przykład 5: Zebrać unikalne elementy do Set

Set<String> unique = names.stream()
    .collect(Collectors.toSet());
System.out.println(unique);

Przykład 6: Zebrać statystyki liczbowe

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream()
    .collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats.getSum());     // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMax());     // 5
System.out.println(stats.getMin());     // 1

3. Porównanie: kiedy używać reduce, a kiedy – collect

reduce – gdy trzeba otrzymać jedną wartość końcową za pomocą operacji binarnej: suma, iloczyn, maksimum, konkatenacja.

collect – gdy należy zebrać elementy do kolekcji/mapy/ciągu lub wykonać bardziej złożoną agregację z Collector. Do takich zadań collect zwykle jest potężniejszy i wydajniejszy.

Tabela: reduce vs collect

Zadanie Czego użyć Przykład
Suma liczb
reduce
reduce(0, Integer::sum)
Iloczyn
reduce
reduce(1, (a, b) -> a * b)
Zbieranie do List
collect
collect(Collectors.toList())
Zbieranie do Map
collect
collect(Collectors.toMap(...))
Grupowanie
collect
collect(Collectors.groupingBy(...))
Konkatenacja ciągów reduce / collect reduce("", String::concat) albo Collectors.joining()

4. Zadania praktyczne

Zadanie 1: Znajdź sumę długości wszystkich ciągów na liście

List<String> words = List.of("kot", "pies", "słoń");

int totalLength = words.stream()
    .mapToInt(String::length)
    .sum(); // albo przez reduce: .reduce(0, Integer::sum)

System.out.println(totalLength); // 11

Zadanie 2: Zebrać unikalne elementy do Set

List<String> fruits = List.of("jabłko", "gruszka", "jabłko", "pomarańcza");

Set<String> uniqueFruits = fruits.stream()
    .collect(Collectors.toSet());

System.out.println(uniqueFruits); // [jabłko, gruszka, pomarańcza]

Zadanie 3: Zbudować Map z listy obiektów

List<Employee> employees = List.of(
    new Employee("Anna", "IT"),
    new Employee("Bob", "HR"),
    new Employee("Wiktor", "IT")
);

Map<String, String> nameToDept = employees.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        Employee::getDepartment,
        (oldValue, newValue) -> newValue // jeśli imiona się powtarzają
    ));

System.out.println(nameToDept);

Zadanie 4: Zebrać wszystkie imiona po przecinku

String allNames = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.joining(", "));

System.out.println(allNames); // Anna, Bob, Wiktor

5. Specyfika implementacji i niuanse

Optional i reduce

Jeśli użyć reduce bez identity, wynikiem jest Optional. To bezpieczne: jeśli strumień jest pusty, wynik też jest pusty. Pamiętaj, aby poprawnie go obsłużyć: ifPresent(...), orElse(...), orElseThrow(...).

Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);

Własne kolektory: jeśli masz ochotę na ekstremum

Możesz napisać własny Collector, jeśli standardowych brakuje. Jednak w 99% przypadków wystarczą gotowe z Collectors.

Kolektory i strumienie równoległe

Gotowe kolektory z Collectors są zaprojektowane do poprawnej pracy z parallelStream(). Nie dodawaj elementów ręcznie do współdzielonej modyfikowalnej kolekcji wewnątrz forEach na strumieniu równoległym – złapiesz warunki wyścigu.

6. Typowe błędy przy pracy z reduce i collect

Błąd nr 1: Nie sprawdzasz Optional po reduce. Jeśli strumień jest pusty, reduce bez identity zwraca pusty Optional. Wywołanie get() spowoduje NoSuchElementException. Używaj ifPresent, orElse lub orElseThrow.

Błąd nr 2: Próbujesz zbierać kolekcję przez reduce. Da się tak zrobić, ale collect jest do tego lepiej przystosowany i szybszy:

// Niewydajne!
List<String> list = stream.reduce(
    new ArrayList<>(),
    (acc, elem) -> { acc.add(elem); return acc; },
    (acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// Lepiej tak:
List<String> list2 = stream.collect(Collectors.toList());

Błąd nr 3: Nie obsługujesz duplikatów kluczy w toMap. Gdy klucze się powtarzają, pojawi się wyjątek. Dodaj trzeci argument w toMap, aby rozwiązać konflikty.

Błąd nr 4: Używasz modyfikowalnych kolekcji w strumieniach równoległych bez synchronizacji. W collect używaj standardowych kolektorów – działają poprawnie w trybie równoległym. Nie rób list.add() w forEach na parallelStream().

Błąd nr 5: Mylisz reduce i collect w złożonych zadaniach. reduce – do prostych agregacji (suma, maksimum). collect – do zbierania do kolekcji, grupowania, budowania Map i złożonej agregacji.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION