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ż 3–5 wierszy, czytelność cierpi. Lepiej wydzielić kod do osobnej metody albo użyć klasy anonimowej (jeśli potrzebny jest stan/kilka metod).
GO TO FULL VERSION