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
Ż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:
Listnames = 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]
Lambda expressions – nowy sposób
Lambda expressions pozwalają na znacznie zwięźlejszy zapis tej samej funkcjonalności:
Listnames = 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]
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.
// 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
Wbudowane functional interfaces
Java 8 dostarcza wiele przydatnych functional interfaces w package java.util.function:
Interface | Method | Opis | Przykład użycia |
---|---|---|---|
Predicate<T> | boolean test(T t) | Testuje warunek | x -> x > 10 |
Function<T,R> | R apply(T t) | Transformuje T na R | x -> x.toString() |
Consumer<T> | void accept(T t) | Wykonuje akcję na T | x -> 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:
Listnumbers = 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
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
// 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!
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 }
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
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);
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.
Nie, lambda działa tylko z functional interfaces – interfejsami które mają dokładnie jedną abstract method. Kompilator sprawdza to automatycznie.
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.
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.
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.
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.
Method references (::) są bardziej czytelne gdy lambda tylko wywołuje istniejącą metodę. Zamiast x -> x.toString() lepiej napisać Object::toString.
Przydatne zasoby:
- Oracle Java Tutorial – Lambda Expressions
- java.util.function Package Documentation
- Java 8 Tutorial by Benjamin Winterberg
🚀 Zadanie dla Ciebie
Przepisz ten kod używając lambda expressions:
Listpeople = // 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?