Как параметризация внешнего класса влияет на тип его нестатического внутреннего класса?
Нестатический внутренний класс связан с конкретной параметризацией внешнего класса. Поэтому внутренний класс, принадлежащий Контейнер<String>, является другим типом на уровне компиляции, чем внутренний класс, принадлежащий Контейнер<Integer>. Он может напрямую использовать параметр типа внешнего класса, тогда как статический вложенный класс такой связи не имеет.
Обобщения появились в Java, чтобы добавить статическую проверку типов и убрать необходимость массовых небезопасных приведений при работе с коллекциями, сохранив совместимость с существующим кодом. Внутренние классы существовали независимо от Generics, поэтому для них сохранилась модель связи с экземпляром внешнего объекта.
Нестатический внутренний класс концептуально имеет скрытую ссылку на экземпляр внешнего класса. Параметризация внешнего типа определяет, какой именно тип имеет эта связь и какие значения параметра доступны внутри внутреннего класса.
Если не учитывать параметризацию внешнего класса, можно ошибочно считать внутренние классы одинаковыми и передать объект с несовместимым параметром типа. Это нарушило бы типобезопасность: внутренний объект, ожидающий String, мог бы оказаться связанным с контейнером Integer.
Дополнительная сложность возникает из-за различия между нестатическим внутренним и статическим вложенным классом. Первый зависит от параметров внешнего класса, а второй не связан с конкретным экземпляром внешнего типа и не может напрямую обращаться к его параметру типа.
Рассмотрим минимальный пример:
Запись Container<String>.Entry означает внутренний класс Entry, связанный с экземпляром Container<String>. Запись Container<Integer>.Entry обозначает другой параметризованный вид этого внутреннего класса; присваивание между ними запрещено.
Внутри Entry имя T разрешается через внешний класс. Поэтому поле value в первом случае имеет тип String, а во втором — Integer. Сам Entry при этом не обязан объявлять собственный параметр типа.
Статический вложенный класс Helper не имеет скрытой ссылки на экземпляр Container<T>. Поэтому параметр T внешнего класса ему недоступен. Если ему нужна параметризация, он должен объявить собственный параметр типа.
На уровне байткода действует стирание типов: различие между Container<String> и Container<Integer> не представлено отдельными runtime-классами. Однако компилятор проверяет параметризации до стирания и вставляет необходимые приведения типов, поэтому ошибка выявляется на этапе компиляции, а не случайно при выполнении.
Существенный компромисс такой: нестатический внутренний класс удобно использует состояние и тип внешнего объекта, но сильнее связан с ним и не может существовать независимо. Статический вложенный класс слабее связан с внешним классом, зато лучше подходит для самостоятельных вспомогательных типов.
В библиотеке есть обобщённый Repository<T> и внутренний класс Transaction, который хранит операции над объектами T. Для Repository<User> транзакция должна работать только с User, а для Repository<Order> — только с Order.
Вариант с нестатическим внутренним классом естественно выражает это ограничение: тип транзакции связан с конкретным репозиторием, а компилятор запрещает смешивать транзакции разных параметризаций. Минус — транзакция не может существовать без экземпляра репозитория и неявно удерживает его ссылку.
Вариант со статическим вложенным классом не удерживает внешний объект, что лучше для независимых DTO или утилит. Но тогда параметр типа нужно передавать самому классу, например концептуально моделировать Transaction<T>, а связь с конкретным Repository<T> придётся контролировать дополнительно.
Если транзакция действительно зависит от состояния репозитория, выбирают нестатический внутренний класс. Если ей нужен только общий алгоритм и данные можно передавать явно, предпочтительнее статический вложенный класс или отдельный обобщённый класс.
Нет. Entry в примере не объявляет собственного параметра типа. Он использует параметр T внешнего класса, а конкретный тип получает через параметризацию внешнего типа. Это отличается от объявления class Entry<U>, где появился бы независимый параметр U.
В корректном параметризованном коде внешнее употребление должно иметь конкретную параметризацию либо использовать raw-тип. Raw-тип технически допускается ради обратной совместимости, но теряет статическую проверку: тип членов, зависящих от T, становится менее точным и может привести к предупреждениям или небезопасным операциям.
Нет, стирание типов не создаёт отдельные runtime-классы для Container<String>.Entry и Container<Integer>.Entry. Их различие в основном существует в типовой модели компилятора и в generic-метаданных class-файла. Поэтому параметризацию нельзя надёжно проверить обычным instanceof во время выполнения, хотя компилятор использует её для проверки присваиваний и вставки приведений.