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

Виды

Класс, объявленный внутри другого типа, называется nested class. Есть четыре практических формы:

  1. статический вложенный класс;
  2. нестатический inner class;
  3. локальный класс внутри блока/метода;
  4. анонимный класс.

Вложенность уместна, когда вспомогательный тип тесно связан с внешним и не является самостоятельной частью API.

Static nested class

Статический вложенный класс не имеет неявной ссылки на экземпляр внешнего:

final class Request {
    private final String path;
 
    private Request(String path) {
        this.path = path;
    }
 
    static final class Builder {
        private String path;
 
        Builder path(String value) {
            path = value;
            return this;
        }
 
        Request build() {
            return new Request(path);
        }
    }
}
 
Request request = new Request.Builder()
        .path("/health")
        .build();

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

Inner class

Нестатический inner class связан с конкретным внешним объектом:

final class Bag<E> {
    private final List<E> values = new ArrayList<>();
 
    final class Cursor {
        private int index;
 
        boolean hasNext() {
            return index < values.size();
        }
 
        E next() {
            return values.get(index++);
        }
    }
}
 
Bag<String> bag = new Bag<>();
Bag<String>.Cursor cursor = bag.new Cursor();

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

Локальные и анонимные классы

Локальный класс виден только внутри блока. Анонимный создаёт единственный безымянный подтип:

Comparator<String> byLength = new Comparator<>() {
    @Override
    public int compare(String left, String right) {
        return Integer.compare(left.length(), right.length());
    }
};

Для функционального интерфейса это обычно короче записать лямбдой. Анонимный класс остаётся полезен, когда нужно переопределить несколько методов или иметь собственное состояние.

Локальные переменные, захваченные локальным/анонимным классом или лямбдой, должны быть final или effectively final.

Доступ и ограничения

Top-level класс может быть только public или package-private. Nested class может иметь private, protected, package-private или public.

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

class Box<T> {
    static class Entry<U> {
        U value;
    }
}

Ловушки

  • Называть любой nested class «inner»: термин inner относится только к нестатической форме.
  • Делать класс нестатическим, хотя ему не нужен внешний объект.
  • Не замечать, что inner class удерживает внешний объект.
  • Использовать анонимный класс вместо простой лямбды.
  • Захватывать большой mutable-контекст в долгоживущем callback.
  • Ожидать доступ к T внешнего класса из static nested class.

Вопросы на собеседовании

  1. Чем static nested class отличается от inner class?
  2. Как создать экземпляр нестатического inner class?
  3. Почему inner class может удерживать внешний объект в памяти?
  4. Какие локальные переменные можно захватывать?
  5. Чем анонимный класс отличается от лямбды?
  6. Какие модификаторы доступа допустимы для top-level и nested-классов?

Связанные заметки

Объекты Java · Классы обертки Java · Лямбда выражения · Функциональные интерфейсы · Модификаторы доступа