Анонимные внутренние классы Java
Создавайте одноразовые реализации интерфейсов и абстрактных классов в Java с помощью анонимных внутренних классов.
Анонимный класс — это одноразовый подкласс или реализация интерфейса, которые вы определяете и создаёте в одном выражении — без имени, без отдельного файла, без заголовка класса. Именно так Java обрабатывала обратные вызовы, слушатели и небольшие адаптеры до появления лямбда-выражений в Java 8. Лямбды заменили большинство их вариантов использования, но анонимные классы по-прежнему допустимы, по-прежнему полезны в ряде конкретных ситуаций и до сих пор встречаются в старых кодовых базах.
Синтаксис
Форма записи: new SomeType() { ... body ... }. Тело является определением класса; окружающее выражение также создаёт экземпляр:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("hi");
}
};
r.run();Этот единственный оператор (1) определяет новый класс, реализующий Runnable, (2) создаёт один его экземпляр, (3) присваивает экземпляр переменной r. Класс не имеет имени в исходном коде; компилятор присваивает ему сгенерированное имя вида Outer$1 в файле .class.
То же самое можно сделать с абстрактным классом:
Shape s = new Shape() {
@Override double area() { return 42; }
};…или даже с конкретным классом, если вы хотите переопределить один из его методов встроенно:
ArrayList<String> chatty = new ArrayList<>() {
@Override
public boolean add(String s) {
System.out.println("added " + s);
return super.add(s);
}
};Когда их используют
Классический случай: обратный вызов, реализация которого невелика и используется ровно в одном месте:
button.addClickListener(new ClickListener() {
@Override
public void onClick() {
System.out.println("clicked");
}
});В современном Java это обычно записывается как лямбда:
button.addClickListener(() -> System.out.println("clicked"));…и лямбда короче, легче читается и не удерживает ссылку на внешний класс. Когда же анонимная форма всё ещё оправдана?
- Несколько методов. Лямбда реализует один абстрактный метод. Если нужно переопределить два метода абстрактного класса или реализовать два метода интерфейса, только анонимный класс справится с задачей.
- Наследование от конкретного класса. Лямбды нацелены только на функциональные интерфейсы. Чтобы на лету переопределить метод у
ArrayList,HashMapили вашего собственного конкретного класса, нужен анонимный класс. - Вызов
super.method(...)в переопределении. У лямбд нетsuper. У анонимных классов есть. - Блоки инициализации. Анонимные классы могут содержать блоки инициализации экземпляра (
{ ... }); лямбды — нет.
На практике это небольшой набор случаев. Большинство современных обратных вызовов используют лямбды.
Захват переменных
Анонимный класс, объявленный внутри метода, может читать локальные переменные этого метода — но только если они объявлены как final или являются фактически финальными (не переприсваиваются после инициализации):
void schedule() {
String msg = "hello";
Runnable r = new Runnable() {
@Override public void run() {
System.out.println(msg); // captures msg
}
};
r.run();
// msg = "bye"; // would make msg no longer effectively final → ERROR above
}То же правило применяется к лямбдам — это свойство окружающего кода, а не синтаксической формы. Причина в том, что захваченное значение копируется в поля синтетического класса; если бы msg могло измениться позже, захваченная копия незаметно устарела бы.
Внешний this
Как и внутренние классы, анонимные классы, объявленные в нестатическом контексте, хранят ссылку на экземпляр внешнего класса. Это означает, что this внутри анонимного класса ссылается на анонимный экземпляр, а не на внешний. Чтобы обратиться к внешнему экземпляру, используйте квалифицированную форму Outer.this:
public class Server {
String name = "outer";
Runnable handler() {
return new Runnable() {
String name = "inner";
public void run() {
System.out.println(name); // "inner"
System.out.println(Server.this.name);// "outer"
}
};
}
}И то же предостережение насчёт внешней ссылки, что и у внутренних классов: возврат анонимного экземпляра в долгоживущий код удерживает внешний экземпляр в памяти.
Ограничения
- Анонимный класс может расширять один суперкласс или реализовывать один интерфейс — но не то и другое одновременно и не более одного из них.
- У него не может быть собственного конструктора — нет имени, которое можно дать конструктору. Для инициализации можно использовать блок инициализации экземпляра (
{ ... }). - Он не может быть
static.
Сравнение с лямбдами — пример в коде
Одна задача, два способа:
// Anonymous class — older style
Comparator<String> byLength = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
// Lambda — modern equivalent
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());Оба варианта создают Comparator<String>. Лямбда короче и имеет несколько иную семантику this и областей видимости (нет ссылки на внешний класс в контексте экземпляра; this внутри лямбды — это экземпляр-замыкание, а не новый объект). Если оба варианта подходят для вашего случая, предпочтите лямбду.
Рабочий пример
Что дальше
Оставшаяся разновидность вложенных классов — локальный класс: класс, объявленный внутри тела метода, с настоящим именем, но очень узкой областью видимости. Они пересекаются с анонимными классами (и с лямбдами), но иногда являются наиболее чистым выбором. Продолжайте изучение с локальных классов.