Анонимные внутренние классы

В языке Java есть возможность создавать анонимные классы, то есть, классы без имени. На этом занятии мы увидим как они объявляются и для чего используются.

Итак, анонимные внутренние классы всегда используются как расширения обычных классов или интерфейсов. То есть, изначально нам нужно объявить какой-нибудь обычный класс (или интерфейс), например:

class Button {}

а, затем, в процессе создания экземпляра этого класса, мы можем расширить его функционал через анонимный внутренний класс. Это делается по следующему синтаксису:

<имя класса или интерфейса> btn = new <имя класса или интерфейса>() {
         // данные и методы
         // анонимного внутреннего класса
}

Смотрите, вот эти фигурные скобки, что идут после создания экземпляра класса (или интерфейса) и есть объявление внутреннего анонимного класса, так как у него мы явно не указываем никакого имени. Хотя имя у него все-таки есть. Виртуальная машина Java автоматически присвоит ему имя по правилу

Class$X

где X – целое число (порядковый номер анонимного класса).

То есть, в нашем примере с классом Button, мы можем объявить вложенный анонимный класс, следующим образом:

        Button btn = new Button() {
        };

Правда функционала здесь пока еще никакого нет. Давайте его добавим, а заодно увидим для чего все это нужно. Предположим, что при нажатии на кнопку должен вызываться метод click() и этот метод мы пропишем внутри класса Button:

class Button {
    public void click() {
        System.out.println("Нажатие на кнопку");
    }
}

Но далее, по программе собираемся использовать этот класс кнопки для разных задач: открытия файла, сохранения проекта, копирования данных и т.п. Используя наши текущие знания, мы, конечно же создали бы несколько дочерних классов и переопределили в них метод click():

class ButtonOpen extends Button {
    public void click() {
        System.out.println("Открытие файла");
    }
}

И так для каждой операции. В результате, у нас в программе появится большое число разных дочерних классов и их количество будет постоянно расти по мере развития проекта. Это не очень удобно. Вот здесь, как раз, нам на помощь и приходят внутренние анонимные классы. Вместо создания дочерних классов, мы при создании объекта Button, сразу переопределим его метод click с помощью анонимного класса:

        Button btnCopy = new Button() {
            public void click() {
                System.out.println("Копирование данных");
            }
        };

И, затем, вызывая этот метод:

btnCopy.click();

в консоли увидим сообщение:

Копирование данных

То есть, с помощью внутреннего анонимного класса мы «на лету» переопределили его метод click() и задали нужное нам действие. Никаких дочерних классов создавать не потребовалось. Мало того, текст программы стал более прозрачным и ясным. Программист непосредственно в момент создания объекта видит, что именно он будет выполнять при нажатии на кнопку. Все это и обусловливает удобство вложенных анонимных классов.

Давайте теперь внимательнее посмотрим, как работает эта конструкция. Проведем такой маленький эксперимент. Если в анонимный класс добавить некий метод:

        Button btnCopy = new Button() {
            public void click() {
                System.out.println("Копирование данных");
            }
 
            public void doSome() {
                System.out.println("do something...");
            }
        };

А, затем, вызвать его через ссылку btnCopy:

btnCopy.doSome();

то возникнет ошибка. Спрашивается: почему мы можем обращаться к методу click(), но не можем вызывать метод doSome()? Я думаю, вы уже догадались почему. Все дело в том, что анонимный класс фактически наследуется от базового класса Button. В этом смысл слова «внутренний» анонимный класс. Поэтому, мы можем вызывать метод click() благодаря механизму динамической диспетчеризации методов, так как такой же метод click() объявлен в классе Button. А вот «добраться» до метода doSome() уже нет никакой возможности. Но, если нам изменить тип ссылки btnCopy так, чтобы она имела тип анонимного вложенного класса:

var btnCopy = new Button() { ... }

то проблем с вызовом метода doSome() не возникнет, т.к. ссылка btnCopy теперь имеет тип дочернего анонимного класса (благодаря ключевому слову var, виртуальная машина Java в момент выполнения кода автоматически приводит ссылку к нужному типу данных).

Этот пример еще раз показывает, что здесь создается не объект класса Button, а объект дочернего анонимного класса.

Использование анонимных классов с интерфейсами

В нашей реализации с кнопкой есть один существенный недостаток: анонимный класс встраивается в цепочку наследования:

В результате, программист может случайно (или намеренно) переопределить методы и нарушить штатную работу объекта Button. Большую гибкость и безопасность работы можно достичь, используя интерфейсы. Например, можно определить интерфейс EventHandler, связанный с обработкой определенного типа события, в котором объявлен абстрактный метод execute(). Далее, мы можем при создании экземпляра класса Button создать класс, реализующий интерфейс EventHandler с помощью анонимного класса, в котором определим нужную нам реализацию метода execute:

На уровне языка Java все это можно реализовать так. Сначала запишем интерфейс EventHandler и класс Button:

interface EventHandler {
    void execute();
}
 
class Button {
    EventHandler handler;
 
    Button(EventHandler handler) {
        this.handler = handler;
    }
 
    public void click() {
        handler.execute();
    }
}

А, затем, создадим кнопку в методе main():

public class Main {
    public static void main(String[] args) {
        var btnCopy = new Button(new EventHandler() {
            public void execute() {
                System.out.println("Копирование данных");
            }
        });
 
        btnCopy.click();
    }
}

Смотрите, как элегантно и красиво выглядит реализация обработки события в классе Button. Сначала, при создании экземпляра кнопки мы тут же создаем объект анонимного вложенного класса, реализующий интерфейс EventHandler. Поэтому, внутри фигурных скобок обязаны определить метод execute() с конкретной реализацией. Далее, ссылку на созданный объект анонимного класса передаем как аргумент в конструктор класса Button и сохраняем ее, используя тип интерфейса EventHandler. Нам этого вполне достаточно, так как затем, в методе click() вызываем переопределенный метод execute() в анонимном классе через ссылку handler. Все, таким образом, мы отделили обработку события от реализации класса Button и, кроме того, можем разными интерфейсами описывать разные типы событий. Это обеспечивает дополнительную гибкость программного кода.

Вот так анонимные вложенные классы позволяют «на лету» создавать нужные нам объекты на основе других классов или интерфейсов.

Видео по теме