Перейти к основному содержимому

Анонимные классы в Java

· 6 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Введение

В этом уроке мы рассмотрим анонимные классы в Java.

Мы опишем, как мы можем объявлять и создавать их экземпляры. Мы также кратко обсудим их свойства и ограничения.

2. Объявление анонимного класса

Анонимные классы — это внутренние классы без имени. Поскольку у них нет имени, мы не можем использовать их для создания экземпляров анонимных классов. В результате нам приходится объявлять и создавать экземпляры анонимных классов в одном выражении в момент использования.

Мы можем либо расширить существующий класс, либо реализовать интерфейс.

2.1. Расширить класс

Когда мы создаем экземпляр анонимного класса из существующего, мы используем следующий синтаксис:

./8aa65fb8582567180572470779046bb8.png

В скобках указываем параметры, которые требуются конструктору класса, который мы расширяем:

new Book("Design Patterns") {
@Override
public String description() {
return "Famous GoF book.";
}
}

Естественно, если конструктор родительского класса не принимает аргументов, мы должны оставить круглые скобки пустыми.

2.2. Реализовать интерфейс

Мы также можем создать экземпляр анонимного класса из интерфейса:

./4d8da80b6c16580cf7ec16c2aaf964c4.png

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

new Runnable() {
@Override
public void run() {
...
}
}

Как только мы создали экземпляр анонимного класса, мы можем присвоить этот экземпляр переменной, чтобы иметь возможность сослаться на него где-нибудь позже.

Мы можем сделать это, используя стандартный синтаксис для выражений Java:

Runnable action = new Runnable() {
@Override
public void run() {
...
}
};

Как мы уже упоминали, объявление анонимного класса — это выражение, поэтому оно должно быть частью оператора . Это объясняет, почему мы поставили точку с запятой в конце оператора.

Очевидно, что мы можем избежать назначения экземпляра переменной, если создадим этот экземпляр в строке:

List<Runnable> actions = new ArrayList<Runnable>();
actions.add(new Runnable() {
@Override
public void run() {
...
}
});

Мы должны использовать этот синтаксис с большой осторожностью, так как он может легко ухудшить читабельность кода, особенно когда реализация метода run() занимает много места.

3. Свойства анонимного класса

Существуют определенные особенности использования анонимных классов по отношению к обычным классам верхнего уровня. Здесь мы кратко коснемся наиболее практических вопросов. Для получения наиболее точной и обновленной информации мы всегда можем обратиться к Спецификации языка Java .

3.1. Конструктор

Синтаксис анонимных классов не позволяет реализовать несколько интерфейсов. Во время построения может существовать ровно один экземпляр анонимного класса . Поэтому они никогда не могут быть абстрактными. Поскольку у них нет имени, мы не можем их расширить. По той же причине анонимные классы не могут иметь явно объявленных конструкторов.

На самом деле отсутствие конструктора не представляет для нас проблемы по следующим причинам:

  1. мы создаем экземпляры анонимных классов в тот же момент, когда мы их объявляем
  2. из экземпляров анонимного класса мы можем получить доступ к локальным переменным и членам окружающего класса

3.2. Статические члены

Анонимные классы не могут иметь никаких статических членов, кроме тех, которые являются постоянными.

Например, это не будет компилироваться:

new Runnable() {
static final int x = 0;
static int y = 0; // compilation error!

@Override
public void run() {...}
};

Вместо этого мы получим следующую ошибку:

The field y cannot be declared static in a non-static inner type, unless initialized with a constant expression

3.3. Объем переменных

Анонимные классы захватывают локальные переменные, находящиеся в области действия блока, в котором мы объявили класс:

int count = 1;
Runnable action = new Runnable() {
@Override
public void run() {
System.out.println("Runnable with captured variables: " + count);
}
};

Как видим, локальные переменные count и action определены в одном блоке. По этой причине мы можем получить доступ к count из объявления класса.

Обратите внимание, что для того, чтобы можно было использовать локальные переменные, они должны быть эффективно окончательными. Начиная с JDK 8 больше не требуется объявлять переменные с ключевым словом final . Тем не менее, эти переменные должны быть final . В противном случае получим ошибку компиляции:

[ERROR] local variables referenced from an inner class must be final or effectively final

Чтобы компилятор решил, что переменная на самом деле неизменяемая, в коде должно быть только одно место, в котором мы присваиваем ей значение. Мы могли бы найти больше информации об эффективно окончательных переменных в нашей статье « Почему локальные переменные, используемые в лямбда-выражениях, должны быть окончательными или фактически окончательными?

Отметим только, что, как и любой внутренний класс, анонимный класс может получить доступ ко всем членам окружающего его класса .

4. Варианты использования анонимного класса

Может быть большое разнообразие приложений анонимных классов. Давайте рассмотрим некоторые возможные варианты использования.

4.1. Иерархия классов и инкапсуляция

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

4.2. Более чистая структура проекта

Обычно мы используем анонимные классы, когда нам нужно на лету модифицировать реализацию методов некоторых классов. В этом случае мы можем избежать добавления в проект новых файлов *.java для определения классов верхнего уровня. Это особенно верно, если этот класс верхнего уровня будет использоваться только один раз.

4.3. Слушатели событий пользовательского интерфейса

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

button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
...
}
}

мы создаем экземпляр анонимного класса, реализующего интерфейс ActionListener. Его метод actionPerformed срабатывает, когда пользователь нажимает кнопку.

Однако , начиная с Java 8, лямбда-выражения кажутся более предпочтительными.

5. Общая картина

Анонимные классы, которые мы рассмотрели выше, являются лишь частным случаем вложенных классов . Как правило, вложенный класс — это класс, объявленный внутри другого класса или интерфейса :

./f7d723a8c1ce5df4c2f4b12cf809bd45.png

Глядя на схему, мы видим, что анонимные классы вместе с локальными и нестатическими членами образуют так называемые внутренние классы . Вместе со статическими классами-членами они образуют вложенные классы.

6. Заключение

В этой статье мы рассмотрели различные аспекты анонимных классов Java. Мы также описали общую иерархию вложенных классов.

Как всегда, полный код доступен в нашем репозитории GitHub .