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 строк, читаемость страдает. Лучше вынести код в отдельный метод или применить анонимный класс (если нужно состояние/несколько методов).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ