Learning Log #4: Strumienie, lambdy i funkcyjne programowanie w Java

with Brak komentarzy
programowanie funkcyjne Java

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ć:

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:

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ę:

  1. Źródło: Surowce wchodzą na taśmę
  2. Operacje pośrednie: Czyszczenie → Cięcie → Malowanie
  3. Operacja końcowa: Pakowanie do pudełka

W Javie:

  1. Źródło: list.stream()
  2. Operacje pośrednie: .filter().map().sorted()
  3. 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

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

Ł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:

Realny użytek: Teraz mogę w sekundę wygenerować raport miesięczny dla księgowej!

Optional – koniec z NullPointerException?

Problem z null:

Rozwiązanie – Optional:

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:

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ń!

Podąrzaj Danuta:

Blogująca mama dwóch chłopców. Ciągle ucząca się i poszukująca pomysłu na siebie. Obecnie pogłębiająca tajniki programowania.

Zostaw Komentarz