CodeGym /Kursy /JAVA 25 SELF /Wyrażenia lambda: składnia, zasięgi

Wyrażenia lambda: składnia, zasięgi

JAVA 25 SELF
Poziom 48 , Lekcja 0
Dostępny

1. Wprowadzenie

Bądźmy szczerzy: pisanie klas anonimowych dla jednej–dwóch linijek kodu — to jak wynajmowanie ogromnej ciężarówki, żeby przewieźć z piekarni do sklepu jedną bułkę.

Na przykład, jeśli chcesz posortować listę napisów po długości, przed Java 8 trzeba było pisać tak:

List<String> list = Arrays.asList("jabłko", "banan", "kiwi");
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Niby zadanie proste, a kodu — na pół ekranu. Szczególnie irytuje, gdy takich operacji jest dużo: kod staje się „hałaśliwy”, gubi się istota zadania. Programiści płakali, cierpieli, a potem wymyślili wyrażenia lambda. Już trochę je poznaliśmy, teraz będziemy powtarzać i pogłębiać naszą wiedzę.

Czym jest wyrażenie lambda

Wyrażenie lambda — to zwięzła forma zapisu implementacji interfejsu funkcyjnego, czyli interfejsu z jedną abstrakcyjną metodą (na przykład Comparator, Runnable, Consumer i wiele innych).

Prościej mówiąc, wyrażenie lambda pozwala napisać funkcję „w locie”, dokładnie tam, gdzie jest potrzebna, bez deklarowania osobnej klasy lub metody.

Ogólna składnia:

(parametry) -> { ciało }

Przykłady:

  • (a, b) -> a + b — funkcja dodająca dwie liczby
  • x -> x * x — funkcja podnosząca liczbę do kwadratu
  • () -> System.out.println("Hello!") — funkcja bez parametrów

Powiązanie z interfejsami funkcyjnymi:
Wyrażenie lambda można przypisać do zmiennej typu interfejsu funkcyjnego albo przekazać jako argument do metody, która oczekuje takiego interfejsu.

2. Składnia wyrażeń lambda

Bez parametrów

Runnable r = () -> System.out.println("Witaj, świecie!");
r.run(); // Wypisze: Witaj, świecie!

Jeden parametr
Jeśli parametr jest jeden, można pominąć nawiasy:

Consumer<String> print = s -> System.out.println(s);
print.accept("Java jest super!");

Wiele parametrów
Nawiasy są obowiązkowe:

Comparator<String> cmp = (a, b) -> a.length() - b.length();

Ciało z jednego wyrażenia
Jeśli ciało składa się z jednego wyrażenia, nawiasy klamrowe i return nie są potrzebne:

Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25

Ciało jako blok
Jeśli potrzebujesz kilku instrukcji, użyj nawiasów klamrowych i return (jeśli jest wartość zwracana):

Function<Integer, Integer> abs = x -> {
    if (x < 0) {
        return -x;
    }
    return x;
};
System.out.println(abs.apply(-3)); // 3

Typy parametrów
Najczęściej typów parametrów nie trzeba podawać — kompilator domyśli się z kontekstu. Ale jeśli chcesz, możesz określić je jawnie:

Comparator<String> cmp = (String a, String b) -> a.length() - b.length();

Lambda bez wartości zwracanej
Jeśli interfejs zwraca void, po prostu wpisz instrukcje:

list.forEach(s -> System.out.println("Element: " + s));

Tabela: Warianty składni wyrażeń lambda

Co robimy Przykład Komentarz
Bez parametrów
() -> System.out.println("Hi")
Na przykład dla Runnable
Jeden parametr
x -> x * x
Można bez nawiasów
Wiele parametrów
(a, b) -> a + b
Nawiasy są obowiązkowe
Jedno wyrażenie
x -> x + 1
Bez return i nawiasów klamrowych
Blok kodu
x -> { int y = x + 1; return y * 2; }
Z return, jeśli jest wynik

3. Zastosowanie: gdzie i jak używać wyrażeń lambda

Wyrażenia lambda najczęściej stosuje się tam, gdzie trzeba przekazać „zachowanie” — funkcję — jako argument. To była prawdziwa rewolucja dla kolekcji, strumieni (Stream API), zdarzeń i wielu innych obszarów.

Sortowanie listy

Przed Java 8:

list.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Z wyrażeniem lambda:

list.sort((a, b) -> a.length() - b.length());

Tworzenie wątku (Thread)

Thread t = new Thread(() -> System.out.println("Wątek uruchomiony!"));
t.start();

Przetwarzanie kolekcji

list.forEach(s -> System.out.println(s.toUpperCase()));

Filtrowanie listy

List<String> longWords = list.stream()
    .filter(s -> s.length() > 5)
    .collect(Collectors.toList());

Przykład: Rozwijamy naszą aplikację edukacyjną

Załóżmy, że mamy listę użytkowników:

List<String> users = Arrays.asList("Alice", "Bob", "Charlie");

Wypisz wszystkich użytkowników, których imiona są dłuższe niż 4 znaki:

users.stream()
    .filter(name -> name.length() > 4)
    .forEach(name -> System.out.println("Użytkownik: " + name));

4. Zasięg zmiennych w wyrażeniach lambda

Wyrażenia lambda mogą używać zmiennych z otaczającej metody, ale są pewne niuanse!

Zmienne i lambdy

Lambda w Java może „przechwytywać” tylko takie zmienne, które po inicjalizacji już się nie zmieniają. Jeśli zmienna jest zadeklarowana jako final — wszystko jest oczywiste. Ale nawet jeśli słowo final nie jest napisane, kompilator sam sprawdza: czy wartość się zmienia, czy nie. Jeśli nie, uznaje ją za „jak gdyby final” i spokojnie dopuszcza w lambdzie.

Przykład:

int minLength = 4; // wartość nigdzie nie jest zmieniana
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});

Tutaj wszystko działa, ponieważ minLength pozostaje tą samą liczbą.

Jednak jeśli po użyciu w lambdzie spróbujesz przepisać minLength, otrzymasz błąd kompilacji:

int minLength = 4;
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});
minLength = 10; // Błąd! Lambda już "utrwaliła" wartość

W istocie zasada jest bardzo prosta: zmienna, która trafiła do lambdy, musi być niezmienna.

Różnica względem klas anonimowych

W klasach anonimowych i wyrażeniach lambda zmienne z zewnętrznej metody działają tak samo: tylko final/efektywnie final.

ALE!
W wyrażeniu lambda this odnosi się do obiektu zewnętrznego (bieżącej instancji klasy), a w klasie anonimowej — do instancji klasy anonimowej. To ważne, jeśli wewnątrz lambdy odwołujesz się do pól lub metod bieżącej klasy.

Przykład:

public class Example {
    String name = "Klasa zewnętrzna";

    void demo() {
        Runnable r1 = new Runnable() {
            String name = "Klasa anonimowa";
            @Override
            public void run() {
                System.out.println(this.name); // "Klasa anonimowa"
            }
        };

        Runnable r2 = () -> System.out.println(this.name); // "Klasa zewnętrzna"

        r1.run();
        r2.run();
    }
}

5. Typowe błędy przy pracy z wyrażeniami lambda

Błąd nr 1: Użycie zmiennej, która nie jest final/nieefektywnie final. Jeśli postanowisz zmienić zmienną po tym, jak została użyta w lambdzie, kompilator natychmiast Cię powstrzyma. Robi się tak dla bezpieczeństwa: inaczej byłoby niejasne, jaką wartość zmiennej zastosować.

Błąd nr 2: Pomyłka z this. W wyrażeniu lambda this — to klasa zewnętrzna, a w klasie anonimowej — sama klasa anonimowa. Jeśli próbujesz wywołać metodę klasy zewnętrznej z lambdy, wszystko będzie działać, natomiast z klasy anonimowej — niekoniecznie (jeśli liczysz na kontekst klasy zewnętrznej).

Błąd nr 3: Lambda bez kontekstu. Wyrażenia lambda nie można użyć samodzielnie — trzeba je albo przypisać do zmiennej interfejsu funkcyjnego, albo przekazać tam, gdzie taki interfejs jest oczekiwany. Próba po prostu napisania x -> x + 1 poza kontekstem spowoduje błąd.

Błąd nr 4: Zbyt złożona lambda. Jeśli wyrażenie lambda robi się dłuższe niż 3–5 wierszy, trudno je czytać. W takich przypadkach lepiej wynieść logikę do osobnej metody.

1
Zadanie
JAVA 25 SELF, poziom 48, lekcja 0
Niedostępne
Warsztat sortowania nazw 📜
Warsztat sortowania nazw 📜
1
Zadanie
JAVA 25 SELF, poziom 48, lekcja 0
Niedostępne
Mistyczny klasyfikator liczb 🔮
Mistyczny klasyfikator liczb 🔮
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION