Java 8 – wprowadzenie do lambda expressions

TL;DR: Lambda expressions w Java 8 to sposób na zwięzłe zapisywanie anonymous functions. Zamiast pisać długie klasy anonimowe, używasz składni (parameters) -> expression. Ułatwia to programowanie funkcyjne i znacznie skraca kod.

Dlaczego lambda expressions są tak ważne?

Java 8 wprowadza największą zmianę w języku od czasu jego powstania. Lambda expressions to nie tylko syntactic sugar – to fundamentalna zmiana w sposobie myślenia o programowaniu w Javie. Po latach używania verbose anonymous classes, w końcu możemy pisać kod w stylu bardziej przypominającym języki funkcyjne.

Co się nauczysz z tego artykułu:

  • Czym są lambda expressions i dlaczego zostały dodane do Java 8
  • Jak zastąpić anonymous classes używając lambd
  • Podstawową składnię lambda expressions
  • Functional interfaces i jak działają z lambdami
  • Praktyczne przykłady użycia w kolekcjach
  • Najczęstsze błędy początkujących z lambdami
Wymagania wstępne: Znajomość Java na poziomie podstawowym, anonymous classes, interfaces, kolekcje (List, Set).

Życie przed lambdami – anonymous classes

Przed Java 8, gdy chcieliśmy przekazać „kawałek kodu” jako parametr, musieliśmy używać anonymous classes. Zobaczmy typowy przykład z sortowaniem:

List names = Arrays.asList("Anna", "Piotr", "Zofia", "Jan");

// Stary sposób - anonymous class
Collections.sort(names, new Comparator() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

System.out.println(names); // [Anna, Jan, Piotr, Zofia]

Ten kod działa, ale jest bardzo verbose. Z 6 linii kodu tylko jedna rzeczywiście implementuje logikę biznesową – porównanie stringów.

Lambda expressions – nowy sposób

Lambda expressions pozwalają na znacznie zwięźlejszy zapis tej samej funkcjonalności:

List names = Arrays.asList("Anna", "Piotr", "Zofia", "Jan");

// Nowy sposób - lambda expression
Collections.sort(names, (a, b) -> a.compareTo(b));

System.out.println(names); // [Anna, Jan, Piotr, Zofia]
Pro tip: Lambda można jeszcze bardziej uprościć używając method references: Collections.sort(names, String::compareTo);

Składnia lambda expressions

Podstawowa składnia lambda wygląda tak:
„`
(parameters) -> expression
„`
lub dla bardziej złożonej logiki:
„`
(parameters) -> { statements; }
„`

Przykłady różnych form lambda expressions:

// Bez parametrów
() -> System.out.println("Hello World")

// Jeden parametr (nawiasy opcjonalne)
x -> x * 2
(x) -> x * 2  // identyczne

// Wiele parametrów
(x, y) -> x + y

// Z blokiem kodu
(x, y) -> {
    int result = x + y;
    System.out.println("Result: " + result);
    return result;
}

// Z typami (zazwyczaj niepotrzebne - type inference)
(Integer x, Integer y) -> x + y

Functional Interfaces – kluczowe pojęcie

Lambda expressions w Javie działają tylko z functional interfaces – interfejsami które mają dokładnie jedną abstract method.

Functional Interface – interfejs z dokładnie jedną abstract method. Może zawierać default i static methods, ale tylko jedna może być abstract.
// To jest functional interface
@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);
    
    // default methods nie liczą się jako abstract
    default void printResult(int result) {
        System.out.println("Result: " + result);
    }
}

// Użycie z lambda
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;

System.out.println(add.calculate(5, 3));      // 8
System.out.println(multiply.calculate(5, 3)); // 15
Adnotacja @FunctionalInterface nie jest obowiązkowa, ale pomaga kompilatorowi sprawdzić czy interface rzeczywiście ma tylko jedną abstract method.

Wbudowane functional interfaces

Java 8 dostarcza wiele przydatnych functional interfaces w package java.util.function:

InterfaceMethodOpisPrzykład użycia
Predicate<T>boolean test(T t)Testuje warunekx -> x > 10
Function<T,R>R apply(T t)Transformuje T na Rx -> x.toString()
Consumer<T>void accept(T t)Wykonuje akcję na Tx -> System.out.println(x)
Supplier<T>T get()Dostarcza wartość T() -> new Date()

Praktyczne przykłady z kolekcjami

Lambda expressions świetnie sprawdzają się przy operacjach na kolekcjach:

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Filtrowanie - znajdź liczby parzyste
List evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6, 8, 10]

// Transformacja - podnieś do kwadratu
List squares = numbers.stream()
    .map(n -> n * n)
    .collect(Collectors.toList());
System.out.println(squares); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

// Działania na każdym elemencie
numbers.forEach(n -> System.out.print(n + " "));
// Wypisuje: 1 2 3 4 5 6 7 8 9 10
Streams API to osobny temat, który zasługuje na oddzielny artykuł. Tu pokazujemy tylko jak lambdy współpracują z metodami filter, map i forEach.

Zastępowanie istniejącego kodu

Zobaczmy jak przepisać typowy kod z anonymous classes na lambdy:

// PRZED - anonymous class dla ActionListener
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Button clicked!");
    }
});

// PO - lambda expression
button.addActionListener(e -> System.out.println("Button clicked!"));

// PRZED - anonymous class dla Runnable
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Running in thread");
    }
});

// PO - lambda expression
Thread thread = new Thread(() -> System.out.println("Running in thread"));

Najczęstsze błędy i pułapki

Błąd początkujących: Próba używania lambda z interfejsem który ma więcej niż jedną abstract method.
// To NIE jest functional interface - ma 2 abstract methods
interface NotFunctional {
    void method1();
    void method2(); // druga abstract method!
}

// Próba użycia lambda - BŁĄD KOMPILACJI
// NotFunctional obj = () -> System.out.println("Hello"); // ERROR!
Pułapka: Lambda może odwoływać się tylko do effectively final zmiennych z otaczającego scope.
public void exampleMethod() {
    int counter = 0;
    
    // To NIE zadziała - counter nie jest effectively final
    Runnable r = () -> {
        // counter++; // BŁĄD KOMPILACJI!
        System.out.println(counter); // To jest OK - tylko odczyt
    };
    
    counter++; // Ta linijka sprawia że counter nie jest effectively final
}
Uwaga: Nie przesadzaj z lambdami. Jeśli lambda ma więcej niż 2-3 linijki, rozważ stworzenie osobnej metody dla czytelności.

Performance i wydajność

Lambda expressions nie mają znaczącego narzutu wydajnościowego. W rzeczywistości mogą być szybsze od anonymous classes, ponieważ:

– Anonymous classes generują osobne .class pliki
– Lambda używa metod invoke dynamic (invokedynamic)
– JVM może lepiej optymalizować lambdy

W benchmarkach lambda expressions są zazwyczaj równie szybkie lub szybsze od equivalent anonymous classes.

Kiedy NIE używać lambd?

Lambda expressions nie zawsze są najlepszym rozwiązaniem:

1. **Złożona logika** – jeśli „lambda” ma więcej niż kilka linijek
2. **Reużywalność** – jeśli ta sama logika jest używana w wielu miejscach
3. **Testowanie** – trudniej testować logikę ukrytą w lambda
4. **Debugging** – stack traces są mniej czytelne

// Źle - zbyt skomplikowane dla lambda
Collections.sort(employees, (e1, e2) -> {
    if (e1.getDepartment().equals(e2.getDepartment())) {
        if (e1.getSalary() == e2.getSalary()) {
            return e1.getName().compareTo(e2.getName());
        }
        return Double.compare(e1.getSalary(), e2.getSalary());
    }
    return e1.getDepartment().compareTo(e2.getDepartment());
});

// Lepiej - wydziel do osobnej metody lub klasy
Collections.sort(employees, this::compareEmployees);
Czy lambda expressions są szybsze od anonymous classes?

Tak, zazwyczaj są równie szybkie lub szybsze. Lambda używa invokedynamic z Java 7, co pozwala JVM na lepsze optymalizacje. Anonymous classes generują dodatkowe .class pliki co może wpływać na startup time.

Czy mogę używać lambda z każdym interfejsem?

Nie, lambda działa tylko z functional interfaces – interfejsami które mają dokładnie jedną abstract method. Kompilator sprawdza to automatycznie.

Dlaczego muszę używać 'effectively final’ zmiennych w lambda?

To ograniczenie wynika z tego jak JVM implementuje closures. Lambda może „przechwycić” wartość zmiennej, ale nie referencję. Effectively final gwarantuje że wartość się nie zmieni.

Jak debugować kod z lambda expressions?

IDE jak IntelliJ IDEA 14 już wspiera debugging lambd. Możesz stawiać breakpointy wewnątrz lambda. Stack traces pokazują „lambda$methodName$0” zamiast nazwy klasy.

Czy lambda może wyrzucać checked exceptions?

To zależy od functional interface. Jeśli interface deklaruje throws Exception, to lambda też może. W przeciwnym razie musisz obsłużyć exception wewnątrz lambda lub użyć wrapper method.

Czy lambda tworzy nową instancję za każdym wywołaniem?

Nie koniecznie. JVM może cache’ować lambda instances jeśli nie capture żadnych zmiennych. To szczegół implementacyjny który może się różnić między wersjami JVM.

Kiedy powinienem używać method references zamiast lambda?

Method references (::) są bardziej czytelne gdy lambda tylko wywołuje istniejącą metodę. Zamiast x -> x.toString() lepiej napisać Object::toString.

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Przepisz ten kod używając lambda expressions:

List people = // lista osób
Collections.sort(people, new Comparator() {
    public int compare(Person p1, Person p2) {
        return p1.getAge() - p2.getAge();
    }
});

Następnie stwórz lambda która filtruje osoby starsze niż 18 lat i wypisuje ich imiona.

Czy już eksperymentowałeś z lambda expressions w swoich projektach? Jakie są Twoje pierwsze wrażenia z tej nowości w Java 8?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry