Вложенные классы

Базовый Java
≈ 4 мин чтения

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

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

Статический вложенный класс

Если вложенный класс объявлен с static, ему не нужен объект внешнего класса. Он связан с внешним классом прежде всего логически: имя внешнего класса становится частью его полного имени.

class Order {
    private final int id;

    Order(int id) {
        this.id = id;
    }

    static class Validator {
        boolean isValid(Order order) {
            return order.id > 0;
        }
    }
}

Создать такой объект можно через имя внешнего класса:

Order.Validator validator = new Order.Validator();
boolean valid = validator.isValid(new Order(10));

У Validator нет неявной ссылки на конкретный Order, поэтому к нестатическому состоянию внешнего класса он обращается только через явно переданный объект. При этом как вложенный класс он может обращаться даже к private-членам Order.

Внутренний класс

Нестатический вложенный класс называют внутренним классом (inner class). Его объект связан с конкретным объектом внешнего класса и может напрямую использовать его поля и методы.

class Counter {
    private int value;

    class Incrementer {
        void increment() {
            value++;
        }
    }
}

Поэтому сначала нужен внешний объект, а уже через него создаётся внутренний:

Counter counter = new Counter();
Counter.Incrementer incrementer = counter.new Incrementer();
incrementer.increment();

Здесь incrementer связан именно с объектом counter. Если создать второй Counter, его внутренний объект будет работать уже с состоянием второго экземпляра.

static здесь определяет не возможность иметь статические члены. Современный Java разрешает внутренним классам объявлять статические члены. Главное различие в другом: внутренний класс связан с экземпляром внешнего класса, а статический вложенный — нет.

Локальный класс

Класс можно объявить ещё уже — прямо внутри метода или другого блока. Такой локальный класс виден только в той области, где объявлен, поэтому подходит для небольшой вспомогательной логики, которая нигде больше не нужна.

void printMessage(String prefix) {
    class Printer {
        void print(String text) {
            System.out.println(prefix + text);
        }
    }

    new Printer().print("Ready");
}

Локальный класс может использовать переменные окружающего метода, если они объявлены final или являются effectively final — то есть после присваивания больше не изменяются. В примере параметр prefix не меняется, поэтому доступен внутри Printer.

Анонимный класс

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

interface Greeter {
    void greet();
}

Greeter greeter = new Greeter() {
    @Override
    public void greet() {
        System.out.println("Hello");
    }
};

greeter.greet();

После new Greeter() идёт тело нового безымянного класса, который реализует Greeter. Собственного конструктора у анонимного класса быть не может, потому что у него нет имени. Как и локальный класс, он может использовать только final или effectively final локальные переменные окружающей области.

Как выбрать подходящий вариант

  • Статический вложенный класс — когда тип логически относится к внешнему классу, но не должен быть связан с его конкретным объектом.
  • Внутренний класс — когда объекту вложенного класса нужен прямой доступ к состоянию конкретного внешнего объекта.
  • Локальный класс — когда именованный вспомогательный тип нужен только внутри одного метода или блока.
  • Анонимный класс — когда нужна одноразовая реализация или специализация и отдельное имя только мешало бы.

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