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 | |
|
| Iloczyn | |
|
| Zbieranie do List | |
|
| Zbieranie do Map | |
|
| Grupowanie | |
|
| 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.
GO TO FULL VERSION