W3docs

Внутренние классы 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 в конструктор и сделайте класс статическим.

Рабочий пример

java— editable, runs on the server

Что дальше

Следующий вид вложенных классов — встроенный одноразовый вариант: анонимные классы, используемые для быстрого создания подкласса с одновременным его инстанциированием. Продолжите чтение в разделе анонимные классы.

Практика

Практика
Почему многие команды по умолчанию используют статические вложенные классы, даже когда нестатический внутренний класс тоже подошёл бы?
Почему многие команды по умолчанию используют статические вложенные классы, даже когда нестатический внутренний класс тоже подошёл бы?
Was this page helpful?