CodeGym /Kursy /JAVA 25 SELF /Klasy anonimowe: różnice względem lambd, przykłady

Klasy anonimowe: różnice względem lambd, przykłady

JAVA 25 SELF
Poziom 48 , Lekcja 4
Dostępny

1. Zagłębiamy się w klasy anonimowe

Klasa anonimowa — to bezimienna podklasa lub implementacja interfejsu, tworzona bezpośrednio w miejscu użycia. Przed pojawieniem się lambd (Java 8) był to najwygodniejszy sposób „jednorazowej” implementacji interfejsu lub klasy abstrakcyjnej.

Klasyka gatunku:

Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("Pozdrowienia z klasy anonimowej!");
    }
};
r.run();

Tutaj zadeklarowaliśmy i od razu zaimplementowaliśmy interfejs Runnable — bez osobnego pliku i nazwy klasy. Takie implementacje często wykorzystywano do obsługi zdarzeń, komparatorów, wątków i innych zadań, gdzie trzeba szybko „podstawić” zachowanie.

Jeśli lambda — to „wyrażenie na szybko”, to klasa anonimowa — „mały aktor bez imienia”, zagrał epizodyczną rolę i zniknął.

2. Porównanie z wyrażeniami lambda

Składnia

Klasa anonimowa:

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

Wyrażenie lambda:

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

Różnica jest oczywista: lambda jest bardziej zwięzła — nie trzeba jawnie podawać typów, nazwy metody ani zbędnych nawiasów klamrowych, jeśli działanie jest proste.

Funkcjonalność

  • Klasa anonimowa — pełnoprawny obiekt. Można deklarować pola, dodatkowe metody, nadpisywać metody Object (toString, equals itd.).
  • Wyrażenie lambda — implementacja jednej metody abstrakcyjnej interfejsu funkcyjnego. Wewnątrz nie można deklarować własnych pól ani dodatkowych metod.

Kiedy co wybrać?

  • Lambda — gdy trzeba krótko zaimplementować jedną metodę interfejsu funkcyjnego.
  • Klasa anonimowa — gdy trzeba:
    • zaimplementować kilka metod (np. klasy abstrakcyjnej);
    • zadeklarować pola do przechowywania stanu;
    • nadpisać metody Object (np. toString);
    • wykorzystać szczegóły dziedziczenia/dostępu (np. do chronionych składowych nadklasy).

3. Zasięg i słowo kluczowe this

Tu kryje się częsta pułapka:

  • w klasie anonimowej this wskazuje na egzemplarz klasy anonimowej;
  • w wyrażeniu lambda this odnosi się do klasy zewnętrznej, w której lambda została zadeklarowana.

Przykład: porównajmy zachowanie

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

    void test() {
        Runnable anon = new Runnable() {
            String name = "Klasa anonimowa";
            @Override
            public void run() {
                System.out.println(this.name); // "Klasa anonimowa"
            }
        };
        Runnable lambda = () -> System.out.println(this.name); // "Klasa zewnętrzna"

        anon.run();
        lambda.run();
    }
}

Wynik:

Klasa anonimowa
Klasa zewnętrzna

W klasie anonimowej this wskazuje na samą klasę anonimową (pobierane jest jej pole name). W lambdzie this — to Outer.

4. Kiedy używać klas anonimowych?

Gdy trzeba zaimplementować więcej niż jedną metodę

Lambda działa tylko z interfejsami funkcyjnymi (dokładnie jedna metoda abstrakcyjna). Jeśli interfejs/klasa abstrakcyjna wymaga zaimplementowania kilku metod — potrzebna jest klasa anonimowa.

abstract class Animal {
    abstract void say();
    abstract void jump();
}

Animal cat = new Animal() {
    @Override
    void say() {
        System.out.println("Miau!");
    }
    @Override
    void jump() {
        System.out.println("Hop!");
    }
};

Gdy trzeba przechowywać stan (pola)

Runnable r = new Runnable() {
    int counter = 0;
    @Override
    public void run() {
        counter++;
        System.out.println("Wywołano " + counter + " razy");
    }
};
r.run(); // Wywołano 1 razy
r.run(); // Wywołano 2 razy

Gdy trzeba nadpisać metody Object

Comparator<String> comp = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
    @Override
    public String toString() {
        return "Komparator według długości łańcucha";
    }
};
System.out.println(comp); // Komparator według długości łańcucha

5. Przykłady: Comparator i Runnable — lambda vs klasa anonimowa

Sortowanie łańcuchów według długości

Klasa anonimowa:

List<String> words = Arrays.asList("kot", "słoń", "mysz", "tygrys");
words.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});
System.out.println(words);

Wyrażenie lambda:

List<String> words = Arrays.asList("kot", "słoń", "mysz", "tygrys");
words.sort((a, b) -> a.length() - b.length());
System.out.println(words);

Wynik jest taki sam, ale kod z lambdą jest krótszy i łatwiejszy do czytania.

Runnable: uruchomienie wątku

Klasa anonimowa:

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Wątek przez klasę anonimową");
    }
});
t1.start();

Wyrażenie lambda:

Thread t2 = new Thread(() -> System.out.println("Wątek przez lambdę"));
t2.start();

Klasa anonimowa z polami

Runnable r = new Runnable() {
    int count = 0;
    @Override
    public void run() {
        count++;
        System.out.println("Wywołano " + count + " razy");
    }
};
r.run(); // Wywołano 1 razy
r.run(); // Wywołano 2 razy

W lambdzie tak się nie da — nie ma możliwości zadeklarowania pola.

6. Szczegóły: zasięg, zmienne i final

Zarówno w klasach anonimowych, jak i w wyrażeniach lambda lokalne zmienne metody zewnętrznej można używać tylko wtedy, gdy są final lub „efektywnie final” (nie zmieniają się po inicjalizacji). Ale jest niuans z nazwami:

  • w klasie anonimowej można zadeklarować zmienną o tej samej nazwie co w zewnętrznym zasięgu („przesłanianie”);
  • w lambdzie — nie: nazwa nie może kolidować z nazwą zmiennej zewnętrznej.

Przykład:

int x = 10;
Runnable r = new Runnable() {
    @Override
    public void run() {
        int x = 20; // OK: przesłania zmienną zewnętrzną
        System.out.println(x); // 20
    }
};
r.run();

Runnable l = () -> {
    // int x = 30; // Błąd kompilacji: zmienna jest już zdefiniowana
    System.out.println(x); // 10
};
l.run();

7. Kiedy lambda — lepsza, a kiedy klasa anonimowa — niezastąpiona?

Wyrażenia lambda — to Twój wybór, jeśli:

  • trzeba zaimplementować krótką funkcję dla interfejsu funkcyjnego;
  • nie trzeba przechowywać stanu;
  • nie trzeba nadpisywać metod Object;
  • implementacja jest „tu i teraz” i jest prosta.

Klasa anonimowa — jest konieczna, jeśli:

  • trzeba zaimplementować interfejs z kilkoma metodami lub klasę abstrakcyjną;
  • trzeba zadeklarować pola lub dodatkowe metody;
  • trzeba nadpisać toString, equals, hashCode;
  • potrzebny jest dostęp do chronionych składowych nadklasy.

8. Praktyka: porównanie na przykładach

Zadanie 1: Filtrowanie listy przez Predicate

Klasa anonimowa:

List<String> animals = Arrays.asList("kot", "słoń", "mysz", "tygrys");
animals.removeIf(new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.length() < 4;
    }
});
System.out.println(animals); // [słoń, mysz, tygrys]

Wyrażenie lambda:

List<String> animals = Arrays.asList("kot", "słoń", "mysz", "tygrys");
animals.removeIf(s -> s.length() < 4);
System.out.println(animals); // [słoń, mysz, tygrys]

Zadanie 2: Porównanie zasięgu this

public class Demo {
    String name = "Demo";

    void check() {
        Runnable anon = new Runnable() {
            String name = "Anon";
            @Override
            public void run() {
                System.out.println(this.name); // "Anon"
            }
        };

        Runnable lambda = () -> System.out.println(this.name); // "Demo"

        anon.run();
        lambda.run();
    }

    public static void main(String[] args) {
        new Demo().check();
    }
}

9. Typowe błędy podczas pracy z klasami anonimowymi i wyrażeniami lambda

Błąd nr 1: Oczekiwanie, że lambda może zaimplementować kilka metod. Lambda działa tylko z interfejsami funkcyjnymi (jedna metoda abstrakcyjna). Jeśli metod jest więcej — użyj klasy anonimowej.

Błąd nr 2: Zamieszanie z zasięgiem this. W lambdzie this — to klasa zewnętrzna, w klasie anonimowej — sama klasa anonimowa. Przez to łatwo odwołać się do „nie tych” pól i wartości.

Błąd nr 3: Próba zadeklarowania pól w lambdzie. W lambdzie nie można deklarować własnych pól — można tylko używać zmiennych kontekstu zewnętrznego (final/„efektywnie final”). Do stanu użyj klasy anonimowej.

Błąd nr 4: Przesłanianie zmiennych. W klasie anonimowej można zadeklarować lokalną zmienną o tej samej nazwie co zewnętrzna — to przesłanianie. W lambdzie tak nie można: kompilator zgłosi błąd.

Błąd nr 5: Zbyt złożona logika w lambdzie. Jeśli ciało lambdy staje się dłuższe niż 35 wierszy, czytelność cierpi. Lepiej wydzielić kod do osobnej metody albo użyć klasy anonimowej (jeśli potrzebny jest stan/kilka metod).

1
Zadanie
JAVA 25 SELF, poziom 48, lekcja 4
Niedostępne
Cenzura w cyfrowym zoo 🦒
Cenzura w cyfrowym zoo 🦒
1
Zadanie
JAVA 25 SELF, poziom 48, lekcja 4
Niedostępne
Głosy wewnętrznego świata 🎭
Głosy wewnętrznego świata 🎭
1
Ankieta/quiz
Wyrażenia lambda, poziom 48, lekcja 4
Niedostępny
Wyrażenia lambda
Wyrażenia lambda
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION