В чём различие между наследником, который фиксирует параметр обобщённого базового класса, и наследником, который передаёт этот параметр дальше?
Наследник, фиксирующий параметр базового класса, становится специализированным для одного типа и сам не обязан быть обобщённым. Наследник, передающий параметр дальше, сохраняет обобщённость и позволяет выбрать конкретный тип при создании его экземпляра.
Иными словами, первый вариант уменьшает универсальность и закрепляет контракт, второй — сохраняет возможность работать с разными типами.
Generics появились в Java для статической проверки типов коллекций и других API без постоянных приведений типов. При этом они должны были сохранить совместимость с существующим кодом, поэтому обобщённые типы реализованы через стирание типов.
Наследование стало важным местом применения параметризации: библиотека может определить общий алгоритм в базовом классе, а конкретный класс — либо оставить тип настраиваемым, либо закрепить его для специализированного API.
Рассмотрим базовый класс, который хранит или обрабатывает значения типа T. При объявлении наследника нужно решить, что означает T в этом наследнике: конкретный тип, например String, или параметр, который должен выбрать пользователь наследника.
Если тип зафиксирован, такой наследник нельзя использовать для других аргументов типа. Если параметр передан дальше, наследник остаётся универсальным, но его пользователям приходится работать с дополнительной параметризацией.
Неверный выбор может привести либо к потере повторного использования, либо к слишком общему API, в котором ожидался специализированный контракт.
В TextBox параметр базового класса зафиксирован как String. Поэтому TextBox не имеет собственного параметра типа: его методы работают со строками, а передача числа не допускается на этапе компиляции.
В TypedBox<T> параметр класса передан базовому классу как Box<T>. При использовании можно выбрать Integer, String или другой допустимый тип, и компилятор сохранит эту связь во всех унаследованных методах.
Важно отличать это от простого необобщённого наследования без параметризации. Если класс наследуется от обобщённого типа без указания аргумента, он использует сырой тип; это ослабляет статическую проверку и может привести к предупреждениям и небезопасным операциям.
Фиксация типа полезна, когда предметная область действительно допускает только один тип. Передача параметра дальше полезна для библиотечного или инфраструктурного класса, который должен поддерживать разные типы без дублирования реализации.
Ограничение можно сочетать с собственной границей: наследник может передать параметр дальше, но сузить допустимые типы через верхнюю границу. Однако такая граница не превращает параметр в конкретный тип — она лишь ограничивает множество возможных аргументов.
Команда разрабатывает базовый контейнер Box<T> и два прикладных слоя. Для модуля конфигурации нужны только строки, а для общего кэша требуется поддержка разных типов значений.
Вариант с фиксацией типа для обоих наследников прост: специализированный класс получает точный API и не требует параметризации от вызывающего кода. Минус — для каждого нового типа придётся создавать отдельный класс, а общая реализация будет концептуально дублироваться на уровне типов.
Вариант с передачей параметра дальше универсален: один наследник обслуживает разные типы, а компилятор сохраняет типобезопасность. Минус — пользователю нужно явно или через вывод типов выбрать аргумент типа, и такой класс не выражает ограничение «только строки».
Рациональное решение — зафиксировать String в классе конфигурации и передать T дальше в общем кэше. В результате специализированный API не принимает неподходящие значения, а универсальный код не размножается; обе гарантии проверяются компилятором.
Можно ли после фиксации типа снова сделать наследника обобщённым по другому параметру?
Да, но новый параметр не изменит уже зафиксированный параметр базового класса. Например, наследник может быть обобщён по метаданным, сохраняя String как тип значения. Это будут независимые параметры, а не способ «переоткрыть» зафиксированный T.
Сохраняет ли наследник, передающий параметр дальше, тот же тип в унаследованных методах?
Да. Если наследник объявлен как TypedBox<T> и расширяет Box<T>, его методы используют один и тот же параметр T. При выборе TypedBox<Integer> метод записи принимает Integer, а метод чтения возвращает Integer; связь не теряется из-за наследования.
Что произойдёт, если наследник использует базовый обобщённый класс как сырой тип?
Параметризация базового класса будет стёрта для использования через этот путь. Методы обычно видны с типами до стирания, например как принимающие или возвращающие Object, а операции могут сопровождаться предупреждениями о непроверяемом преобразовании. Такой наследник теряет часть преимуществ Generics и может скрыть ошибку типов до момента выполнения.