Java 8 wprowadziła jedną z największych rewolucji w historii języka – Streams API. Jeśli dotychczas przetwarzałeś kolekcje używając tradycyjnych pętli, przygotuj się na zmianę myślenia. Streams pozwalają pisać kod w stylu funkcjonalnym, który jest bardziej ekspresyjny i często szybszy niż imperatywne podejście.
Dlaczego Streams są ważne
Java 8 to przełomowa wersja wydana w 2014 roku, która wprowadza programowanie funkcjonalne do ekosystemu Java. Streams API to odpowiedź na potrzeby nowoczesnego programowania – przetwarzanie dużych zbiorów danych, wykorzystanie wielordzeniowych procesorów i pisanie bardziej czytelnego kodu.
Co się nauczysz:
- Jak utworzyć i używać Stream z różnych źródeł danych
- Jakie są podstawowe operacje: filter, map, reduce, collect
- Kiedy używać Streams zamiast tradycyjnych pętli
- Jak zrównoleglić przetwarzanie z Parallel Streams
- Najlepsze praktyki i typowe pułapki w Streams API
Wymagania wstępne:
- Dobra znajomość kolekcji Java (List, Set, Map)
- Podstawy lambda expressions w Java 8
- Rozumienie pojęć: immutability, side effects
- Znajomość pętli for-each i iteratorów
Problem z tradycyjnym podejściem
Zobaczmy typowy kod do przetwarzania listy przed Java 8:
// Tradycyjne podejście - imperatywne List<String> names = Arrays.asList("Jan", "Anna", "Piotr", "Aleksandra", "Tomasz"); // Znajdź wszystkie imiona dłuższe niż 4 znaki, // przekonwertuj na wielkie litery i posortuj List<String> result = new ArrayList<>(); for (String name : names) { if (name.length() > 4) { // Filtrowanie result.add(name.toUpperCase()); // Transformacja } } Collections.sort(result); // Sortowanie System.out.println(result); // [ALEKSANDRA, ANNA, PIOTR, TOMASZ]
Ten sam kod z Java 8 Streams:
// Podejście funkcjonalne z Streams List<String> names = Arrays.asList("Jan", "Anna", "Piotr", "Aleksandra", "Tomasz"); List<String> result = names.stream() .filter(name -> name.length() > 4) // Filtrowanie .map(String::toUpperCase) // Transformacja .sorted() // Sortowanie .collect(Collectors.toList()); // Zbieranie wyniku System.out.println(result); // [ALEKSANDRA, PIOTR, TOMASZ]
Tworzenie Streams z różnych źródeł
Stream możesz utworzyć na różne sposoby:
// Z kolekcji List<String> list = Arrays.asList("a", "b", "c"); Stream<String> streamFromList = list.stream(); // Z tablicy String[] array = {"x", "y", "z"}; Stream<String> streamFromArray = Arrays.stream(array); // Z pojedynczych wartości Stream<String> streamFromValues = Stream.of("jeden", "dwa", "trzy"); // Nieskończony stream liczb Stream<Integer> infiniteNumbers = Stream.iterate(0, n -> n + 2); // Z zakresu liczb IntStream range = IntStream.range(1, 10); // 1,2,3...9 // Z pliku (każda linia to element) Stream<String> lines = Files.lines(Paths.get("plik.txt"));
Operacje pośrednie (Intermediate Operations)
Operacje pośrednie przekształcają Stream ale nie wykonują przetwarzania do momentu wywołania operacji terminalnej. To nazywa się „lazy evaluation”.
filter() – filtrowanie elementów
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // Tylko liczby parzyste List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); // [2, 4, 6, 8, 10] // Można łączyć warunki List<Integer> filtered = numbers.stream() .filter(n -> n > 3) .filter(n -> n < 8) .collect(Collectors.toList()); // [4, 5, 6, 7]
map() – transformacja elementów
List<String> words = Arrays.asList("hello", "world", "java", "streams"); // Konwersja na wielkie litery List<String> upperCase = words.stream() .map(String::toUpperCase) .collect(Collectors.toList()); // [HELLO, WORLD, JAVA, STREAMS] // Długość każdego słowa List<Integer> lengths = words.stream() .map(String::length) .collect(Collectors.toList()); // [5, 5, 4, 7] // Transformacja obiektów List<User> users = getUsers(); List<String> userNames = users.stream() .map(User::getName) .collect(Collectors.toList());
sorted() – sortowanie
List<String> names = Arrays.asList("Zofia", "Adam", "Barbara", "Czesław"); // Sortowanie naturalne List<String> sortedNames = names.stream() .sorted() .collect(Collectors.toList()); // [Adam, Barbara, Czesław, Zofia] // Sortowanie z własnym Comparatorem List<String> sortedByLength = names.stream() .sorted(Comparator.comparing(String::length)) .collect(Collectors.toList()); // [Adam, Zofia, Barbara, Czesław] // Sortowanie odwrotne List<String> reversed = names.stream() .sorted(Comparator.reverseOrder()) .collect(Collectors.toList());
distinct() i limit()
List<Integer> numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5); // Usunięcie duplikatów List<Integer> unique = numbersWithDuplicates.stream() .distinct() .collect(Collectors.toList()); // [1, 2, 3, 4, 5] // Pierwsze 3 elementy List<Integer> firstThree = numbersWithDuplicates.stream() .distinct() .limit(3) .collect(Collectors.toList()); // [1, 2, 3] // Pomiń pierwsze 2, weź kolejne 3 List<Integer> skipAndLimit = numbersWithDuplicates.stream() .distinct() .skip(2) .limit(3) .collect(Collectors.toList()); // [3, 4, 5]
Operacje terminalne (Terminal Operations)
Operacje terminalne wykonują przetwarzanie i „konsumują” Stream:
collect() – zbieranie wyników
List<String> words = Arrays.asList("java", "python", "javascript", "go"); // Do listy List<String> list = words.stream() .filter(w -> w.length() > 4) .collect(Collectors.toList()); // Do setu (unikalne wartości) Set<String> set = words.stream() .collect(Collectors.toSet()); // Do mapy (słowo -> długość) Map<String, Integer> wordLengths = words.stream() .collect(Collectors.toMap( word -> word, // klucz String::length // wartość )); // Łączenie w string String joined = words.stream() .collect(Collectors.joining(", ")); // "java,python,javascript,go"
forEach() – wykonanie akcji dla każdego elementu
List<String> names = Arrays.asList("Jan", "Anna", "Piotr"); // Wydrukuj każde imię names.stream() .map(String::toUpperCase) .forEach(System.out::println); // Nie rób tego! forEach tylko do side effects names.stream() .forEach(name -> { // Zła praktyka - modyfikowanie zewnętrznych obiektów // someExternalList.add(name.toUpperCase()); });
reduce() – redukcja do jednej wartości
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // Suma wszystkich liczb int sum = numbers.stream() .reduce(0, Integer::sum); // 15 // Iloczyn wszystkich liczb int product = numbers.stream() .reduce(1, (a, b) -> a * b); // 120 // Najdłuższe słowo List<String> words = Arrays.asList("kot", "pies", "słoń", "mysz"); Optional<String> longest = words.stream() .reduce((w1, w2) -> w1.length() > w2.length() ? w1 : w2); // Optional[słoń]
Operacje sprawdzające
List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10); // Czy wszystkie są parzyste? boolean allEven = numbers.stream() .allMatch(n -> n % 2 == 0); // true // Czy jakieś są większe niż 5? boolean anyGreaterThan5 = numbers.stream() .anyMatch(n -> n > 5); // true // Czy żadne nie są ujemne? boolean noneNegative = numbers.stream() .noneMatch(n -> n < 0); // true // Pierwszy element większy niż 5 Optional<Integer> firstGreater = numbers.stream() .filter(n -> n > 5) .findFirst(); // Optional[6]
Parallel Streams – wykorzystanie wielordzeniowości
Jedną z największych zalet Streams jest łatwość równoległego przetwarzania:
List<Integer> largeList = IntStream.range(0, 1_000_000) .boxed() .collect(Collectors.toList()); // Sekwencyjne przetwarzanie long sequentialSum = largeList.stream() .mapToLong(i -> expensiveOperation(i)) .sum(); // Równoległe przetwarzanie - często szybsze! long parallelSum = largeList.parallelStream() .mapToLong(i -> expensiveOperation(i)) .sum(); // Lub zamiana zwykłego Stream na parallel long parallelSum2 = largeList.stream() .parallel() .mapToLong(i -> expensiveOperation(i)) .sum();
Praktyczny przykład – analiza danych
Zobaczmy praktyczny przykład analizy listy pracowników:
public class Employee { private String name; private String department; private int salary; private int age; // konstruktor, gettery, settery... } List<Employee> employees = getEmployees(); // Średnia pensja w dziale IT OptionalDouble avgItSalary = employees.stream() .filter(emp -> "IT".equals(emp.getDepartment())) .mapToInt(Employee::getSalary) .average(); // Najstarszy pracownik w każdym dziale Map<String, Optional<Employee>> oldestByDept = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.maxBy(Comparator.comparing(Employee::getAge)) )); // Top 5 najlepiej zarabiających List<String> topEarners = employees.stream() .sorted(Comparator.comparing(Employee::getSalary).reversed()) .limit(5) .map(Employee::getName) .collect(Collectors.toList()); // Statystyki pensji IntSummaryStatistics salaryStats = employees.stream() .mapToInt(Employee::getSalary) .summaryStatistics(); System.out.println("Średnia: " + salaryStats.getAverage()); System.out.println("Min: " + salaryStats.getMin()); System.out.println("Max: " + salaryStats.getMax());
Best practices i typowe błędy
- Używaj method references gdy to możliwe:
String::length
zamiasts -> s.length()
- Unikaj side effects w operacjach pośrednich
- Preferuj collect() zamiast forEach() do budowania wyników
- Używaj specialized streams (IntStream, LongStream) dla primitives
Streams są lepsze gdy przetwarzasz kolekcje funkcjonalnie (filter, map, reduce). Tradycyjne pętle nadal są dobre do prostych iteracji i gdy potrzebujesz złożonej logiki sterującej.
Nie. Dla małych kolekcji tradycyjne pętle mogą być szybsze. Streams mają overhead, ale oferują lepszą czytelność kodu i możliwość łatwego równoległego przetwarzania.
Operacje pośrednie nie są wykonywane od razu, tylko zapisywane. Faktyczne przetwarzanie rozpoczyna się dopiero przy operacji terminalnej. To pozwala na optymalizacje.
Użyj .peek() do podglądania elementów w pipeline: .peek(System.out::println)
. Lub przerwij łańcuch i sprawdź wyniki pośrednie.
Technически tak, ale nie powinieneś. Streams promują functional programming – twórz nowe obiekty zamiast modyfikować istniejące.
Collectors to gotowe implementacje operacji collect(). Pozwalają zbierać wyniki do list, map, grupować dane, robić statystyki itp.
Operacje jak findFirst(), reduce(), min(), max() zwracają Optional bo mogą nie znaleźć wyniku (np. dla pustego Stream).
Przydatne zasoby:
- Java 8 Streams API Documentation
- Collectors Documentation
- Java 8 Tutorial Examples
- Oracle Streams Tutorial
🚀 Zadanie dla Ciebie
Masz listę transakcji bankowych:
class Transaction {
String type; // "CREDIT" lub "DEBIT"
double amount;
String category; // "FOOD", "TRANSPORT", "ENTERTAINMENT"
LocalDate date;
}
Używając Streams napisz kod który:
- Znajdzie sumę wszystkich wydatków (DEBIT) w kategorii FOOD
- Zgrupuje transakcje po kategorii i policzy średnią kwotę
- Znajdzie 3 największe single transakcje z ostatniego miesiąca
- Sprawdzi czy są jakieś transakcje większe niż 1000
Masz pytania o Java 8 Streams? Podziel się swoimi doświadczeniami w komentarzach – Streams na początku mogą wydawać się skomplikowane, ale szybko stają się naturalną częścią kodu!