
Learning Log #4: Strumienie, lambdy i funkcyjne programowanie w Java
Kawa trzecia dzisiaj. Kod na ekranie wygląda… dziwnie. Zamiast pętli for mam
.stream().filter().map().collect()
Zamiast:
java
for (int i = 0; i < list.size(); i++) { ... }
Mam:
java
list.stream().forEach(item -> ...)
Witajcie w świecie programowania funkcyjnego w Java.
Tydzień 13-14 mojej nauki (po oficjalnym zakończeniu podstaw Zajavka) poświęciłam na zaawansowane tematy: strumienie (Streams API), wyrażenia lambda i Optional. Tematy, które na początku wyglądały jak hieroglify, a teraz… no cóż, nadal są trudne, ale już rozumiem PO CO.
Czym są wyrażenia lambda w Java?
Najprostsza definicja:
Lambda to funkcja bez nazwy, którą można przekazać jako argument.
Zamiast pisać:
java
public interface Greeting {
void sayHello(String name);
}
public class PolishGreeting implements Greeting {
@Override
public void sayHello(String name) {
System.out.println("Cześć " + name);
}
}
// Użycie:
Greeting greeting = new PolishGreeting();
greeting.sayHello("Anna");
Możesz napisać:
java
Greeting greeting = (name) -> System.out.println("Cześć " + name);
greeting.sayHello("Anna");
10 linii → 1 linia. To jest moc lambd.
Składnia lambda – od długiej do krótkiej:
java
// Wersja pełna (typ parametru + nawias + return)
(String name) -> { return "Cześć " + name; }
// Bez typu (Java infers - wywnioskuje)
(name) -> { return "Cześć " + name; }
// Bez return (single expression)
(name) -> "Cześć " + name
// Jeden parametr = bez nawiasu
name -> "Cześć " + name
Wszystkie 4 wersje robią TO SAMO.
Mój przykład – filtrowanie terapii:
Przed lambdą:
java
public class ExpensiveTherapyFilter { public List<Therapy> filter(List<Therapy> therapies) {
List<Therapy> result = new ArrayList<>();
for (Therapy therapy : therapies) {
if (therapy.getCost() > 70) {
result.add(therapy);
}
}
return result;
}
}
Z lambdą:
java
List<Therapy> expensive = therapies.stream() .filter(t -> t.getCost() > 70)
.collect(Collectors.toList());
15 linii → 3 linie. Czytelniejsze. Mniej szablonowe.
Streams API – pipeline przetwarzania danych
Czym jest strumień (stream)?
Strumień to sekwencja elementów które możesz przetwarzać w pipeline (rurociąg).
Analogia – taśma produkcyjna:
Wyobraź sobie fabrykę:
- Źródło: Surowce wchodzą na taśmę
- Operacje pośrednie: Czyszczenie → Cięcie → Malowanie
- Operacja końcowa: Pakowanie do pudełka
W Javie:
- Źródło:
list.stream() - Operacje pośrednie:
.filter()→.map()→.sorted() - Operacja końcowa:
.collect()lub.forEach()
Podstawowe operacje:
filter() – filtrowanie
java
// Tylko terapie droższe niż 70 zł
List<Therapy> expensive = therapies.stream() .filter(t -> t.getCost() > 70)
.collect(Collectors.toList());
map() – transformacja
java
// Lista kosztów (z Therapy robi Double)
List<Double> costs = therapies.stream()
.map(t -> t.getCost())
.collect(Collectors.toList());
// Skrócona wersja (method reference):
List<Double> costs = therapies.stream()
.map(Therapy::getCost)
.collect(Collectors.toList());
sorted() – sortowanie
java
// Posortowane po cenie (rosnąco)
List<Therapy> sorted = therapies.stream() .sorted(Comparator.comparing(Therapy::getCost))
.collect(Collectors.toList());
forEach() – akcja na każdym
java
// Wypisz każdą terapię
therapies.stream()
.forEach(t -> System.out.println(t.getName()));
// Lub krócej:
therapies.stream()
.forEach(System.out::println);`
Łańcuchowanie operacji (chaining):
java
// Znajdź 3 najdroższe terapie typu "SI" i wypisz ich nazwy
therapies.stream() .filter(t -> t.getType().equals("SI")) // tylko SI
.sorted(Comparator.comparing(Therapy::getCost).reversed()) // od najdroższych
.limit(3) // tylko 3
.map(Therapy::getName) // same nazwy
.forEach(System.out::println); // wypisz
Piękno: Czytasz to jak zdanie po polsku – „weź terapie, odfiltruj SI, posortuj po cenie malejąco, weź 3, wyciągnij nazwy, wypisz”
Mój projekt – analiza kosztów terapii z Streams
Zbudowałam system analizy moich wydatków na terapie używając Streams API.
java
public class TherapyAnalyzer { private List<TherapySession> sessions;
public TherapyAnalyzer(List<TherapySession> sessions) {
this.sessions = sessions;
}
// 1. Całkowity koszt wszystkich terapii
public double getTotalCost() {
return sessions.stream()
.mapToDouble(TherapySession::getCost)
.sum();
}
// 2. Średni koszt terapii
public double getAverageCost() {
return sessions.stream()
.mapToDouble(TherapySession::getCost)
.average()
.orElse(0.0);
}
// 3. Najdroższa terapia
public Optional<TherapySession> getMostExpensive() {
return sessions.stream()
.max(Comparator.comparing(TherapySession::getCost));
}
// 4. Grupowanie po typie terapii
public Map<String, List<TherapySession>> groupByType() {
return sessions.stream()
.collect(Collectors.groupingBy(TherapySession::getType));
}
// 5. Suma kosztów per typ terapii
public Map<String, Double> getCostByType() {
return sessions.stream()
.collect(Collectors.groupingBy(
TherapySession::getType,
Collectors.summingDouble(TherapySession::getCost)
));
}
// 6. Liczba sesji per typ
public Map<String, Long> getCountByType() {
return sessions.stream()
.collect(Collectors.groupingBy(
TherapySession::getType,
Collectors.counting()
));
}
// 7. Terapie w danym miesiącu
public List<TherapySession> getSessionsInMonth(int year, int month) {
return sessions.stream()
.filter(s -> s.getDate().getYear() == year)
.filter(s -> s.getDate().getMonthValue() == month)
.sorted(Comparator.comparing(TherapySession::getDate))
.collect(Collectors.toList());
}
// 8. Top 5 najdroższych sesji
public List<TherapySession> getTop5MostExpensive() {
return sessions.stream()
.sorted(Comparator.comparing(TherapySession::getCost).reversed())
.limit(5)
.collect(Collectors.toList());
}
// 9. Czy są nieopłacone terapie?
public boolean hasUnpaidSessions() {
return sessions.stream()
.anyMatch(s -> !s.isPaid());
}
// 10. Wszystkie opłacone?
public boolean allPaid() {
return sessions.stream()
.allMatch(TherapySession::isPaid);
}
}
Użycie:
java
// Przykładowe dane
List<TherapySession> sessions = List.of(
new TherapySession("SI", LocalDate.of(2026, 4, 1), 75.0, true),
new TherapySession("Logopeda", LocalDate.of(2026, 4, 3), 65.0, true),
new TherapySession("SI", LocalDate.of(2026, 4, 8), 75.0, false),
new TherapySession("Pedagog", LocalDate.of(2026, 4, 10), 60.0, true),
new TherapySession("Logopeda", LocalDate.of(2026, 4, 15), 65.0, false)
);
TherapyAnalyzer analyzer = new TherapyAnalyzer(sessions);
// Analiza
System.out.println("Total: " + analyzer.getTotalCost() + " zł");
// Output: Total: 340.0 zł
System.out.println("Average: " + analyzer.getAverageCost() + " zł");
// Output: Average: 68.0 zł
Map<String, Double> costByType = analyzer.getCostByType();
System.out.println("SI: " + costByType.get("SI") + " zł");
// Output: SI: 150.0 zł
System.out.println("Has unpaid: " + analyzer.hasUnpaidSessions());
// Output: Has unpaid: true`
Realny użytek: Teraz mogę w sekundę wygenerować raport miesięczny dla księgowej!
Optional – koniec z NullPointerException?
Problem z null:
java
public Therapy getMostExpensiveTherapy(List<Therapy> therapies) {
Therapy result = null; for (Therapy t : therapies) {
if (result == null || t.getCost() > result.getCost()) {
result = t;
}
} return result;
// Co jeśli lista pusta? → null
}
// Użycie:
Therapy expensive = getMostExpensiveTherapy(therapies); System.out.println(expensive.getName()); // BOOM! NullPointerException jeśli lista pusta`
Rozwiązanie – Optional:
java
public Optional<Therapy> getMostExpensiveTherapy(List<Therapy> therapies) {
return therapies.stream()
.max(Comparator.comparing(Therapy::getCost));
}
// Użycie - wersja 1 (sprawdź czy jest)
Optional<Therapy> expensive = getMostExpensiveTherapy(therapies);
if (expensive.isPresent()) {
System.out.println(expensive.get().getName());
} else {
System.out.println("No therapies");
}
// Użycie - wersja 2
(functional style) getMostExpensiveTherapy(therapies)
.ifPresent(t -> System.out.println(t.getName()));
// Użycie - wersja 3 (default value)
String name = getMostExpensiveTherapy(therapies)
.map(Therapy::getName)
.orElse("No therapies");
Optional mówi wyraźnie: „Ta metoda może NIE zwrócić wartości”
Kiedy używać Optional:
✅ TAK:
- Zwracane wartości metod które mogą być puste
- Rezultat wyszukiwania
stream().findFirst(),.max(),.min()
❌ NIE:
- Pola w klasach (ciężkie, memory overhead)
- Parametry metod (null-check prostszy)
- Collections (używaj pustej listy zamiast Optional<List>)
Method references – skróty lambd
Czasem lambda to tylko przekazanie do innej metody:
java
// Lambda
list.forEach(item -> System.out.println(item));
// Method reference
list.forEach(System.out::println);`
Typy method references:
1. Static method
java
// Lambda
list.stream().map(s -> Integer.parseInt(s))
// Method reference
list.stream().map(Integer::parseInt)
2. Instance method
java
// Lambda
therapies.stream().map(t -> t.getName())
// Method reference
therapies.stream().map(Therapy::getName)
3. Constructor
java
// Lambda
list.stream().map(name -> new Therapy(name))
// Method reference
list.stream().map(Therapy::new)
Typowe błędy które popełniłam
1. Modyfikowanie źródła w stream
Błąd:
java
List<Therapy> therapies = new ArrayList<>(originalList); therapies.stream()
.forEach(t -> therapies.add(new Therapy("Copy"))); // ConcurrentModificationException!
Lekcja: Streams NIE mogą modyfikować kolekcji źródłowej.
2. Używanie side effects w lambda
Błąd:
java
int[] counter = {0}; // zmienna zewnętrzna zmienna therapies.stream() .forEach(t -> counter[0]++); // ŹLE! Efekt uboczny
Lepiej:
java
long count = therapies.stream().count(); // Sposób funkcjonalny
3. Zapomnienie .collect()
Błąd:
java
therapies.stream() .filter(t -> t.getCost() > 70)
.map(Therapy::getName); // To NIE zwraca List! To Stream!
Poprawka:
java
List<String> names = therapies.stream() .filter(t -> t.getCost() > 70)
.map(Therapy::getName) .collect(Collectors.toList()); // Teraz tak!
4. Używanie Optional.get() bez sprawdzenia
Błąd:
java
Optional<Therapy> therapy = findTherapy();System.out.println(therapy.get().getName()); // NoSuchElementException jeśli empty!
Lepiej:
java
therapy.ifPresent(t -> System.out.println(t.getName()));
// Lub
String name = therapy.map(Therapy::getName).orElse("Unknown");
Kiedy używać programowania funkcyjnego, a kiedy imperatywnego?
Użyj functional (streams, lambda):
Transformacja danych:
java
List<String> names = therapies.stream() .map(Therapy::getName)
.collect(Collectors.toList());
Filtrowanie:
java
List<Therapy> expensive = therapies.stream() .filter(t -> t.getCost() > 70)
.collect(Collectors.toList());
Agregacja:
java
double total = therapies.stream() .mapToDouble(Therapy::getCost)
.sum();
Użyj imperative (pętla for):
✅ Wczesne wyjście (early exit):
java
for (Therapy t : therapies) { if (t.getName().equals("SI")) {
return t; // Znalazłem, wychodzę
}
}
(Stream też może: .filter().findFirst() ale pętla czytelniejsza)
✅ Modyfikacja elementów in-place:
java
for (Therapy t : therapies) { t.markAsPaid(); // Zmieniam obiekt
}
✅ Proste iteracje:
java
for (int i = 0; i < 10; i++) { System.out.println(i);
}
(Stream: IntStream.range(0, 10).forEach(System.out::println) – przesada)
Wydajność – czy streams są szybkie?
Benchmark test (100,000 elementów):
Pętla FOR:
java
// Czas: ~15ms
List<Integer> result = new ArrayList<>(); for (Integer num : numbers) { if (num % 2 == 0) {
result.add(num * 2);
}
}
Stream:
java
// Czas: ~25ms
List<Integer> result = numbers.stream() .filter(n -> n % 2 == 0)
.map(n -> n * 2)
.collect(Collectors.toList());
Parallel stream:
java
// Czas: ~10ms (na multi-core!)
List<Integer> result = numbers.parallelStream() .filter(n -> n % 2 == 0)
.map(n -> n * 2)
.collect(Collectors.toList());
Wnioski:
- For pętla najszybsza dla prostych operacji
- Stream ~30-50% wolniejszy (ogólnie)
- Parallel stream może być najszybszy (jeśli CPU ma wiele rdzeni)
Ale: Różnica 10ms vs 25ms? W 99% aplikacji nieistotna. Czytelność > Performance (chyba że przetwarzasz miliony rekordów).
Co dalej – moja droga nauki
Po 17 tygodniach z Java (podstawy Zajavka + zaawansowane tematy):
Decydowałam się: JavaScript!
Dlaczego nie Spring/zaawansowana Java:
- Za ciężkie przy dzieciach i freelancingu
- Potrzebuję czegoś bardziej natychmiast przydatnego
Dlaczego JavaScript:
- Synergia z WordPress (mogę od razu stosować!)
- Frontend umiejętności są rynkowe
- Łatwiejszy balans życie/nauka
Ale Java była nieoceniona:
- Nauczyła mnie OOP solidnie
- Pokazała, że mogę uczyć się programowania
- Dała fundamenty które pomogą w JS
Next: Learning Log #5 będzie o… JavaScript! Od Javy do JS – co jest podobne, co inne, pierwsze wrażenia.
Podsumowanie – tydzień 13-14
✅ Nauczyłam się: Lambdy, Streams API, Optional, method references
✅ Napisałam: TherapyAnalyzer – realny użytek streams
✅ Zrozumiałam: Kiedy programowanie funkcyjne, kiedy imperative
✅ Odkryłam: Parallel streams (game changer dla dużych zbiorów!)
✅ Czas nauki: 12h w ciągu 2 tygodni (poprawiła się regularność!)
Programowanie funkcyjne w Java początkowo: Hieroglify
Programowanie funkcyjne w Java teraz: Potężne narzędzie które skraca kod 3x
Czy było warto? Absolutnie. Stream API to must-know dla każdego Java developera.
PS: Też uczysz się Java? Streams wydają Ci się trudne czy intuicyjne? A może przechodzisz z Javy na inny język? Zostaw komentarz – ciekawa jestem Waszych doświadczeń!
Zostaw Komentarz