CodeGym /Kursy /JAVA 25 SELF /Wprowadzenie do wyrażeń lambda

Wprowadzenie do wyrażeń lambda

JAVA 25 SELF
Poziom 21 , Lekcja 0
Dostępny

1. Wprowadzenie

Jeśli krótko: wyrażenie lambda – to sposób, aby szybko utworzyć implementację interfejsu funkcyjnego „w locie”, bez deklarowania oddzielnej klasy ani klasy anonimowej. To jak mała metoda bez nazwy, którą można przekazać jako argument albo zapisać w zmiennej.

Przez długi czas Java była „klasycznym” językiem obiektowym (OOP). Jeśli chcieliście przekazać kawałek zachowania, na przykład co zrobić po kliknięciu przycisku, trzeba było pisać klasy anonimowe:

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick() {
        System.out.println("Przycisk naciśnięty!");
    }
});

Od Javy 8 pojawiła się magia lambd:

button.setOnClickListener(() -> System.out.println("Przycisk naciśnięty!"));

Gdzie () -> System.out.println("Przycisk naciśnięty!") – to właśnie wyrażenie lambda.

Formalnie: wyrażenie lambda to zwięzła forma zapisu implementacji jedynej metody abstrakcyjnej interfejsu funkcyjnego.

Dlaczego to ważne?

  • Przekazywać zachowanie jako wartość (np. co zrobić z każdym elementem listy).
  • Pisać kod zwięzły i czytelny.
  • Korzystać ze współczesnych API Javy: Stream API, obsługa zdarzeń, zadania asynchroniczne i wiele więcej.

2. Składnia wyrażeń lambda

Ogólny zapis

(parametry) -> wyrażenie
// albo
(parametry) -> { blok kodu }

Przykłady dla różnych interfejsów

1. Bez parametrów (np. Runnable):

Runnable r = () -> System.out.println("Cześć, lambda!");
r.run();

2. Jeden parametr (np. Consumer):

Consumer<String> printer = s -> System.out.println("Przekazano: " + s);
printer.accept("Java");

Jeśli parametr jest tylko jeden i jego typ da się wywnioskować, można pominąć nawiasy.
Typ String to typ zmiennej (s), którą tam przekazujemy.

3. Wiele parametrów (np. Comparator):

Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5

Typ Integer to typ zmiennych (a, b), które tam przekazujemy.

4. Wieloliniowe ciało (potrzebne klamry i return, jeśli jest jawny zwrot):

Function<Integer, Integer> square = x -> {
    int result = x * x;
    return result;
};
System.out.println(square.apply(6)); // 36

Pierwszy Integer to typ wyniku funkcji, drugi Integer to typ parametru.

Skróty i zwięzłość

  • Jeśli ciało to pojedyncze wyrażenie, można pominąć nawiasy i return.
  • Jeśli nie ma parametrów – piszemy puste nawiasy: () -> ...
  • Jeśli parametr jest jeden – można bez nawiasów: x -> ...
  • Jeśli parametrów jest więcej niż jeden – potrzebne są nawiasy: (a, b) -> ...

Tabela podsumowująca

Zobaczmy, jak tę samą ideę zapisać lambdą i klasą anonimową:

Interfejs Wyrażenie lambda (przykład) Równoważny zapis z klasą anonimową
Runnable
() -> System.out.println("Hi")
new Runnable() { public void run() { System.out.println("Hi"); } }
Consumer<String>
s -> System.out.println(s)
new Consumer<String>() { public void accept(String s) { System.out.println(s); } }
Comparator<Integer>
(a, b) -> a - b
new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; } }

Przykład z własnym interfejsem

Załóżmy, że mamy interfejs funkcyjny:

@FunctionalInterface
interface Operation {
    int apply(int a, int b);
}

Dawniej implementowano tak:

Operation sum = new Operation() {
    @Override
    public void apply(int a, int b) {
        return a + b;
    }
};

A teraz – nowocześnie:

Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8

Przykłady dla standardowych interfejsów

Runnable:

Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();

Comparator:

List<String> list = Arrays.asList("jabłko", "banan", "kiwi");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);

Function:

Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123

3. Zakres i przechwytywanie zmiennych

Zmienne z kontekstu zewnętrznego (effectively final)

Wyrażenia lambda mogą korzystać ze zmiennych z otaczającej metody. Ale jest zasada: takie zmienne muszą być effectively final – czyli albo jawnie oznaczone jako final, albo po prostu niezmieniane po inicjalizacji.

Przykład:

String prefix = "Wynik: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix jest tutaj „zamrożony” – po tym nie wolno go już zmieniać
System.out.println(f.apply(5)); // Wynik: 10

Jeśli spróbujemy zmienić prefix po tym, jak został użyty w lambdzie, kompilator zgłosi błąd.

Dlaczego tak?
Wyrażenie lambda może zostać wywołane po wyjściu z metody, w której zadeklarowano zmienną. Aby uniknąć „magicznych” błędów, Java pozwala używać tylko niezmiennych zmiennych.

Różnice względem klas anonimowych

W klasach anonimowych obowiązuje ta sama zasada: zmienne z zewnętrznej metody muszą być final/effectively final. Są jednak niuanse związane z zakresem: wewnątrz klasy anonimowej this odnosi się do samej klasy anonimowej, a w lambdzie – do klasy zewnętrznej.

public class Demo {
    public void test() {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(this); // Wypisze: Demo$1 (klasa anonimowa)
            }
        };

        Runnable r2 = () -> System.out.println(this); // Wypisze: Demo (klasa zewnętrzna)

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

Lambda i pola klasy

Wyrażenie lambda może odwoływać się do pól klasy zewnętrznej bez ograniczeń:

public class Counter {
    private int base = 10;

    public void printSum(int x) {
        Function<Integer, Integer> sum = y -> base + y + x;
        System.out.println(sum.apply(5));
    }
}

Tutaj base – można zmieniać (to pole klasy).
x – musi być effectively final.

4. Praktyka: napiszmy kilka wyrażeń lambda

Przykład: filtrowanie listy liczb

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);

nums.stream()
    .filter(n -> n % 2 == 0)
    .forEach(n -> System.out.println("Parzysta: " + n));

Więcej o Stream API poznasz na 30. poziomie :P

Przykład: funkcja przekształcenia napisu

Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA

Przykład: własny interfejs funkcyjny

@FunctionalInterface
interface StringTransformer {
    String transform(String s);
}

StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Cześć")); // Cześć!

Przykład: użycie zmiennych z kontekstu zewnętrznego

int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// factor nie można zmienić po tym!

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

Błąd nr 1: próba zmiany zmiennej przechwyconej przez lambdę.
Taki kod się nie skompiluje – zmienna musi być effectively final:

int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Błąd: sum nie jest final!

Jeśli trzeba akumulować wartości – użyj tablicy albo obiektu typu wrapper.

Błąd nr 2: zamieszanie z zakresem this.
Wewnątrz wyrażenia lambda this odnosi się do klasy zewnętrznej, a nie do samej lambdy (w przeciwieństwie do klasy anonimowej).

Błąd nr 3: brak nawiasów klamrowych i return w wieloliniowym wyrażeniu lambda.
Jeśli ciało lambdy to nie jedno wyrażenie, potrzebne są klamry i return:

Function<Integer, Integer> square = x -> {
    int y = x * x;
    return y;
};

Błąd nr 4: nieprawidłowe określenie typu wyrażenia lambda.
Wyrażenie lambda zawsze implementuje interfejs funkcyjny. Nie można po prostu napisać:

var f = x -> x + 1; // Błąd! Nie wiadomo, jakiego typu interfejs.

Trzeba jawnie wskazać typ:

Function<Integer, Integer> f = x -> x + 1;
1
Zadanie
JAVA 25 SELF, poziom 21, lekcja 0
Niedostępne
Głosowe ogłoszenie systemu 📣
Głosowe ogłoszenie systemu 📣
1
Zadanie
JAVA 25 SELF, poziom 21, lekcja 0
Niedostępne
Standaryzacja wprowadzania danych przez użytkownika 📝
Standaryzacja wprowadzania danych przez użytkownika 📝
1
Zadanie
JAVA 25 SELF, poziom 21, lekcja 0
Niedostępne
Ranking sportowców według "siły uderzenia" 🥊
Ranking sportowców według "siły uderzenia" 🥊
1
Zadanie
JAVA 25 SELF, poziom 21, lekcja 0
Niedostępne
Dynamiczne generowanie raportów 📊
Dynamiczne generowanie raportów 📊
Komentarze (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Agnieszka Poziom 27, Poland, Poland
8 lutego 2026
I think this example shows it nicely

        // Function which takes in a number
        // and returns half of it
        Function<Integer, Double> half = a -> a / 2.0;