1. Заглиблюємося в анонімні класи
Анонімний клас — це безіменний підклас або реалізація інтерфейсу, що створюється прямо в місці використання. До появи лямбд (Java 8) це був найзручніший спосіб «одноразової» реалізації інтерфейсу або абстрактного класу.
Класика жанру:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Привіт з анонімного класу!");
}
};
r.run();
Тут ми оголосили й одразу реалізували інтерфейс Runnable — без окремого файлу та імені класу. Такі реалізації часто використовували для обробників подій, компараторів, потоків та інших завдань, де потрібно швидко «підкинути» поведінку.
Якщо лямбда — це «вираз на льоту», то анонімний клас — «маленький актор без імені», який зіграв епізодичну роль і зник.
2. Порівняння з лямбда-виразами
Синтаксис
Анонімний клас:
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
Лямбда-вираз:
Comparator<String> comp = (a, b) -> a.length() - b.length();
Різниця очевидна: лямбда компактніша — не потрібно явно прописувати типи, ім’я методу й зайві фігурні дужки, якщо дія проста.
Функціональність
- Анонімний клас — повноцінний об’єкт. Можна оголошувати поля, додаткові методи, перевизначати методи Object (toString, equals тощо).
- Лямбда-вираз — реалізація одного абстрактного методу функціонального інтерфейсу. Усередині не можна оголошувати власні поля або додаткові методи.
Коли що обирати?
- Лямбда — коли потрібно коротко реалізувати один метод функціонального інтерфейсу.
- Анонімний клас — коли потрібно:
- реалізувати кілька методів (наприклад, абстрактного класу);
- оголосити поля для стану;
- перевизначити методи Object (наприклад, toString);
- використовувати особливості успадкування/доступу (наприклад, до захищених членів суперкласу).
3. Область видимості та ключове слово this
Тут криється часта пастка:
- в анонімному класі this посилається на екземпляр анонімного класу;
- у лямбда-виразі this посилається на зовнішній клас, у якому оголошено лямбду.
Приклад: порівняймо поведінку
public class Outer {
String name = "Зовнішній клас";
void test() {
Runnable anon = new Runnable() {
String name = "Анонімний клас";
@Override
public void run() {
System.out.println(this.name); // "Анонімний клас"
}
};
Runnable lambda = () -> System.out.println(this.name); // "Зовнішній клас"
anon.run();
lambda.run();
}
}
Виведення:
Анонімний клас
Зовнішній клас
В анонімному класі this вказує на сам анонімний клас (зчитується його поле name). У лямбді this — це Outer.
4. Коли використовувати анонімні класи?
Якщо потрібно реалізувати більше ніж один метод
Лямбда працює тільки з функціональними інтерфейсами (рівно один абстрактний метод). Якщо інтерфейс/абстрактний клас вимагає реалізувати кілька методів — потрібен анонімний клас.
abstract class Animal {
abstract void say();
abstract void jump();
}
Animal cat = new Animal() {
@Override
void say() {
System.out.println("Няв!");
}
@Override
void jump() {
System.out.println("Стриб!");
}
};
Якщо потрібно зберігати стан (поля)
Runnable r = new Runnable() {
int counter = 0;
@Override
public void run() {
counter++;
System.out.println("Викликано " + counter + " раз(и)");
}
};
r.run(); // Викликано 1 раз(и)
r.run(); // Викликано 2 раз(и)
Якщо потрібно перевизначити методи 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 "Компаратор за довжиною рядка";
}
};
System.out.println(comp); // Компаратор за довжиною рядка
5. Приклади: Comparator і Runnable — лямбда vs анонімний клас
Сортування рядків за довжиною
Анонімний клас:
List<String> words = Arrays.asList("кіт", "слон", "миша", "тигр");
words.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
System.out.println(words);
Лямбда-вираз:
List<String> words = Arrays.asList("кіт", "слон", "миша", "тигр");
words.sort((a, b) -> a.length() - b.length());
System.out.println(words);
Результат однаковий, але код із лямбдою коротший і легше читається.
Runnable: запуск потоку
Анонімний клас:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Потік через анонімний клас");
}
});
t1.start();
Лямбда-вираз:
Thread t2 = new Thread(() -> System.out.println("Потік через лямбду"));
t2.start();
Анонімний клас із полями
Runnable r = new Runnable() {
int count = 0;
@Override
public void run() {
count++;
System.out.println("Викликано " + count + " раз(и)");
}
};
r.run(); // Викликано 1 раз(и)
r.run(); // Викликано 2 раз(и)
У лямбді так не можна — немає можливості оголосити поле.
6. Особливості: область видимості, змінні та final
І в анонімних класах, і в лямбда-виразах локальні змінні зовнішнього методу можна використовувати лише якщо вони final або «ефективно final» (не змінюються після ініціалізації). Але є нюанс із іменами:
- в анонімному класі можна оголосити змінну з тим самим іменем, що й у зовнішній області («затінення»);
- у лямбді — не можна: ім’я не повинно конфліктувати з іменем зовнішньої змінної.
Приклад:
int x = 10;
Runnable r = new Runnable() {
@Override
public void run() {
int x = 20; // ОК: затінює зовнішню змінну
System.out.println(x); // 20
}
};
r.run();
Runnable l = () -> {
// int x = 30; // Помилка компіляції: змінну вже визначено
System.out.println(x); // 10
};
l.run();
7. Коли лямбда — кращий вибір, а коли анонімний клас — незамінний?
Лямбда-вирази — ваш вибір, якщо:
- потрібно реалізувати коротку функцію для функціонального інтерфейсу;
- не потрібно зберігати стан;
- не потрібно перевизначати методи Object;
- реалізація використовується «тут і зараз» і є простою.
Анонімний клас — необхідний, якщо:
- треба реалізувати інтерфейс із кількома методами або абстрактний клас;
- потрібно оголосити поля або додаткові методи;
- потрібно перевизначити toString, equals, hashCode;
- потрібен доступ до захищених членів суперкласу.
8. Практика: порівняння на прикладах
Завдання 1: Фільтрація списку через Predicate
Анонімний клас:
List<String> animals = Arrays.asList("кіт", "слон", "миша", "тигр");
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 4;
}
});
System.out.println(animals); // [слон, миша, тигр]
Лямбда-вираз:
List<String> animals = Arrays.asList("кіт", "слон", "миша", "тигр");
animals.removeIf(s -> s.length() < 4);
System.out.println(animals); // [слон, миша, тигр]
Завдання 2: Порівняння області видимості 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. Типові помилки при роботі з анонімними класами та лямбда-виразами
Помилка № 1: Очікування, що лямбда може реалізувати кілька методів. Лямбда працює тільки з функціональними інтерфейсами (один абстрактний метод). Якщо методів більше — використовуйте анонімний клас.
Помилка № 2: Плутанина з областю видимості this. У лямбді this — це зовнішній клас, в анонімному класі — сам анонімний клас. Через це легко отримати «не ті» поля та значення.
Помилка № 3: Спроба оголосити поля в лямбді. У лямбді не можна оголошувати власні поля — лише використовувати змінні зовнішнього контексту (final/«ефективно final»). Для стану використовуйте анонімний клас.
Помилка № 4: Затінення змінних. В анонімному класі можна оголосити локальну змінну з тим самим іменем, що у зовнішньої — це затінення. У лямбді так не можна: компілятор видасть помилку.
Помилка № 5: Занадто складна логіка в лямбді. Якщо тіло лямбди стає довшим за 3–5 рядків, читабельність страждає. Краще винести код в окремий метод або застосувати анонімний клас (якщо потрібен стан/кілька методів).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ