Вложенные классы в Java
Вложенные классы в Java: статические вложенные классы, внутренние классы, локальные и анонимные классы.
Java позволяет объявлять класс внутри другого класса. Общий термин для этого — вложенный класс. Java предлагает четыре разновидности, различающиеся по месту объявления и наличию ссылки на экземпляр объемлющего класса:
| Разновидность | Где объявляется | Имеет ссылку на this объемлющего? | Глава |
|---|---|---|---|
| Статический вложенный класс | Внутри тела класса, с static | Нет | эта глава |
| Внутренний класс | Внутри тела класса, без static | Да | внутренние классы |
| Локальный класс | Внутри тела метода | Да (если не-статический метод) | локальные классы |
| Анонимный класс | Подкласс/реализация интерфейса «на месте» | Да (если не-статический контекст) | анонимные классы |
Причина выбора вложенного класса вместо отдельного класса верхнего уровня — область видимости: вложенный класс полезен только в контексте объемлющего класса и не должен быть доступен остальному коду. Эта глава является обзорной; каждая разновидность подробно рассматривается в последующих главах.
Зачем вообще вкладывать классы?
Три основные причины:
- Логическое группирование.
Map.Entryимеет смысл только внутриMap. Вложение делает эту связь очевидной в коде. - Инкапсуляция. Вложенный класс можно сделать
private, и ничто за пределами объемлющего класса не сможет на него ссылаться. - Замыкание над состоянием объемлющего класса. Внутренний, локальный или анонимный класс может читать поля объемлющего экземпляра и локальные переменные метода — это основа обработчиков событий, итераторов и многих небольших адаптерных шаблонов.
Если ни одно из этих условий не применимо, пишите класс верхнего уровня.
Статические вложенные классы
Класс, отмеченный static внутри другого класса, — это статический вложенный класс:
public class Outer {
static class Inner {
void hi() { System.out.println("hi"); }
}
}
Outer.Inner i = new Outer.Inner(); // instantiate directly
i.hi();Статический вложенный класс — это просто класс верхнего уровня, который находится в пространстве имён Outer. Он не имеет неявной ссылки на экземпляр Outer — его можно использовать без создания такого экземпляра. Единственное отличие от класса верхнего уровня — область видимости: Outer.Inner является полным именем.
Именно эту разновидность вы уже встречали на протяжении частей 5 и 6 — каждый static class Foo {} в примерах RunnableJava является таким классом. Ключевое слово static использовалось для того, чтобы можно было создавать экземпляры из статического метода main без необходимости создавать экземпляр Outer.
Внутренние классы (нестатические вложенные)
Уберите static — и получите внутренний класс. Экземпляр внутреннего класса привязан к экземпляру внешнего класса и несёт неявную ссылку на него:
public class Outer {
int x = 1;
class Inner { // no static
int get() { return x; } // reads Outer's x through the implicit reference
}
}
Outer o = new Outer();
Outer.Inner i = o.new Inner(); // unusual syntax — bind to o
System.out.println(i.get()); // 1Синтаксис o.new Inner() используется для создания экземпляра внутреннего класса, привязанного к конкретному внешнему экземпляру. Внутренние классы не могут иметь static-членов (старое правило; ослаблено в Java 16+, где static-члены во внутренних классах допускаются). Подробно рассматривается в главе внутренние классы.
Локальные классы
Класс, объявленный внутри тела метода, является локальным — его область видимости ограничена этим методом:
public void run() {
class Step { int n; Step(int n) { this.n = n; } } // visible only in run()
Step s = new Step(5);
}Полезно для небольших вспомогательных конструкций, не заслуживающих отдельного класса верхнего уровня или даже вложенного на уровне класса. Они имеют доступ к final (или фактически финальным) переменным объемлющего метода. Полная информация — в главе локальные классы.
Анонимные классы
Анонимный класс — это одноразовый подкласс или реализация интерфейса, определённая прямо в месте использования:
Runnable r = new Runnable() {
@Override
public void run() { System.out.println("hi"); }
};Это единственное выражение, которое (1) определяет новый класс, реализующий Runnable, (2) создаёт его экземпляр, (3) присваивает его переменной r. Класс не имеет имени — он существует только здесь. Почти все случаи применения анонимных классов в современной Java заменены лямбда-выражениями, однако они по-прежнему допустимы и иногда полезны. Подробнее — в главе анонимные классы.
Выбор подходящего вида
Краткое дерево решений:
- Нужна ли вложенному классу ссылка на внешний экземпляр? Нет → статический вложенный класс. Да → нестатическая разновидность.
- Используется ли он внутри одного метода? Да → локальный или анонимный. Нет → внутренний.
- Это одноразовый подкласс/реализация интерфейса с одним-двумя методами? Да → лямбда (предпочтительно) или анонимный класс (устаревший подход).
Статические вложенные классы — наиболее распространённая разновидность. Внутренние классы встречаются в адаптерах в стиле итератора. Локальные и анонимные классы в современном коде встречаются реже — лямбды покрывают большинство их сценариев использования.
Именование и доступ
Вложенные классы могут иметь любой модификатор доступа — public, private, protected, пакетный — и правила для модификаторов те же, что и для членов верхнего уровня. Map.Entry является public; private static class Node внутри реализации LinkedList невидим снаружи.
Скомпилированные вложенные классы получают имена с разделителем $ в файлах .class: Outer$Inner, Outer$1. Вы будете иногда видеть их в стек-трейсах и отладчиках.
Рабочий пример
Что дальше
В следующих трёх главах подробно рассматривается каждая нестатическая разновидность. Первая: внутренние классы — наиболее общая разновидность, класс, привязанный к экземпляру объемлющего класса.