Внутренние классы Java
Объявляйте нестатические внутренние классы Java, которые хранят неявную ссылку на экземпляр внешнего класса.
Внутренний класс — это нестатический вложенный класс, объявленный внутри другого класса без модификатора static. Главная особенность: каждый экземпляр внутреннего класса привязан к экземпляру внешнего класса и хранит неявную ссылку на него. Из внутреннего класса можно читать и записывать поля внешнего экземпляра и вызывать его методы, как если бы они были собственными.
Это делает внутренние классы подходящим инструментом, когда небольшой вспомогательный класс должен тесно работать с состоянием другого класса — классический пример: итераторы, обходящие внутренние данные своего контейнера.
На этой странице рассматривается: как объявить внутренний класс, как его создать (всегда нужен экземпляр внешнего класса), квалификатор Outer.this, канонический сценарий использования с итераторами, проблема утечки памяти из-за неявной ссылки, и как выбирать между внутренним классом и static-вложенным классом.
Объявление внутреннего класса
Уберите static из объявления вложенного класса:
public class Outer {
private int x = 1;
class Inner { // no static — inner class
int get() { return x; } // reads Outer's x directly
}
}У Inner нет собственных полей, однако get() возвращает 1. Простое x разрешается в Outer.this.x через неявную ссылку.
Создание экземпляра
Поскольку каждый экземпляр внутреннего класса привязан к экземпляру внешнего, для создания первого нужен второй. Есть два способа:
Outer o = new Outer();
Outer.Inner i = o.new Inner(); // bind explicitly to o…или из нестатического метода Outer:
public class Outer {
void demo() {
Inner i = new Inner(); // implicitly bound to this
}
}Синтаксис o.new Inner() встречается редко и выглядит необычно — большинство кода создаёт экземпляры внутреннего класса из собственных методов внешнего класса, где привязка выполняется неявно.
Outer.this — разрешение конфликта имён
Когда внутренний класс объявляет поле с тем же именем, что и поле внешнего класса, внутреннее поле его затеняет. Чтобы обратиться к внешнему, используйте квалификатор Outer.this:
public class Outer {
int x = 1;
class Inner {
int x = 2;
void demo() {
System.out.println(x); // 2 — Inner's x
System.out.println(this.x); // 2 — Inner's x
System.out.println(Outer.this.x); // 1 — Outer's x
}
}
}this во внутреннем классе указывает на внутренний экземпляр; Outer.this — на внешний экземпляр.
Канонический сценарий — итераторы
Классическая причина использовать внутренний класс — реализация итератора над приватными данными контейнера:
public class IntList {
private int[] data;
private int size;
// ... constructors, add, ...
public Iterator<Integer> iterator() {
return new InnerIterator();
}
private class InnerIterator implements Iterator<Integer> {
private int i = 0;
public boolean hasNext() { return i < size; }
public Integer next() { return data[i++]; }
}
}InnerIterator обращается к data и size напрямую через неявную ссылку на внешний класс. Никаких сеттеров и геттеров не нужно — внутренний класс является частью реализации IntList.
Обратите внимание на private class InnerIterator. Снаружи вызывающий код видит только публичный интерфейс Iterator<Integer>; он не знает о существовании InnerIterator. В этом заключается преимущество инкапсуляции при вложении.
Внутренние классы хранят ссылку — и удерживают внешний класс в памяти
Тонкий подводный камень. Пока экземпляр внутреннего класса доступен, JVM не может собрать мусором внешний экземпляр, к которому он привязан. Передача экземпляра внутреннего класса в долгоживущий код (например, установка слушателя) может удерживать целые графы объектов в памяти дольше, чем ожидалось.
public class Window {
Listener installListener() {
return new Listener(); // returned to whoever calls this
}
class Listener { ... } // holds a Window reference forever
}Если installListener() сохраняется в статическом реестре, Window будет жить до очистки этого реестра. Обычное решение — сделать вложенный класс static и явно передавать необходимые данные, разрывая неявную ссылку.
Именно по этой причине многие команды по умолчанию используют static-вложенные классы и переходят к внутренним только тогда, когда явно нужна привязка.
Статические члены во внутренних классах
Большую часть истории Java внутренним классам запрещалось объявлять static-члены (статические поля, методы или вложенные классы). Java 16 смягчила это ограничение — внутренние классы теперь могут иметь статические члены. Тем не менее, если возникает желание их добавить, это часто сигнал о том, что класс лучше сделать static.
static против внутреннего — выбор
Полезное правило: делайте static, если не нужна явная привязка к внешнему экземпляру.
- Статический вложенный класс: проще, легче, не удерживает внешний экземпляр в памяти.
- Внутренний класс: удобный сахар для доступа к
outerInstance.field, когда связь действительно необходима.
Если единственное, что вы делаете, — это Outer.this.field, просто передайте Outer в конструктор и сделайте класс статическим.
Рабочий пример
Что дальше
Следующий вид вложенных классов — встроенный одноразовый вариант: анонимные классы, используемые для быстрого создания подкласса с одновременным его инстанциированием. Продолжите чтение в разделе анонимные классы.