ООП и полиморфизм в Java
Запустил четыре строки: ссылка типа Parent указывает на объект Child. Вызов метода дал «child», а обращение к полю с тем же именем дало «parent-field». Один и тот же объект, одна и та же ссылка - и разные ответы. Вот с этой развилки и начинается любой Java-собес.
Зазубренные «инкапсуляция-наследование-полиморфизм» никого не впечатляют. Проверяют, понимаешь ли ты механику: кто выбирает метод при вызове, когда наследование ломает код и почему композиция чаще спасает.
// Формулировки: «чем перегрузка отличается от переопределения?», «интерфейс или абстрактный класс?», «почему наследование считают опасным?»
Кто выбирает метод: два разных момента
Разберём тот замер по частям. Метод name() переопределён в наследнике, и вызов ушёл в версию Child - потому что переопределение разрешается в рантайме, по фактическому классу объекта. А поле field и статический метод разрешились по типу ссылки, ещё при компиляции: они не переопределяются, а заслоняются.
Интуиция такая. Ссылка - окно, через которое ты смотришь на объект. Обычные методы видят сам объект, поэтому окно им не важно. Поля и статика видят только окно.
Перегрузка (несколько методов с одним именем) - тоже решение компилятора, по статическим типам аргументов. Проверил классику: при методах m(String) и m(Object) вызов m(null) уходит в m(String), потому что компилятор берёт самый специфичный подходящий тип.
// Отсюда привычка ставить @Override на каждый переопределяемый метод. Опечатался в сигнатуре - без аннотации получишь новый метод, который просто никогда не вызовется, и компилятор промолчит.
Parent p = new Child();
p.name(); // "child" - по объекту, рантайм
p.field; // "parent-field" - по ссылке, компиляция
Parent.who(); // "parent-static"- по классу- переопределение
- замена метода в наследнике, выбор в рантайме
- сокрытие
- поле или static заслоняет родительское по типу ссылки
Наследование это обязательство, композиция - свобода
Наследование в Java означает не «переиспользовать код», а отношение «является» с подстановкой. Формально это LSP (Liskov substitution principle): подкласс обязан корректно работать всюду, где ожидается родитель. Унаследовался ради пары методов - получил хрупкую связку, где любое изменение родителя бьёт по детям.
Классический анти-пример: Квадрат наследует Прямоугольник. Математически спорить не о чем: квадрат - частный случай прямоугольника. А в коде метод setWidth у квадрата обязан менять и высоту тоже - иначе объект перестанет быть квадратом. И любой код, который написан под прямоугольник и делает setWidth(5), потом setHeight(3), ждёт площадь 15, а получает 9. Подстановка нарушена, значит наследование неправомерно.
Композиция - это когда объект держит другой внутри и делегирует ему работу. Связь слабее, внутренности можно менять свободно. Правило выбора короткое: «является» с честной подстановкой - наследуй; «использует» - компонуй.
- LSP
- подкласс валиден везде, где ожидается родитель
- композиция
- объект содержит другой и делегирует ему работу
Инкапсуляция, абстракция и три смысла final
Инкапсуляция - не «поля private плюс геттеры и сеттеры». Смысл в защите инвариантов: снаружи нельзя привести объект в кривое состояние, потому что доступ идёт через методы с логикой. Класс, где на каждое поле механически сгенерирован сеттер, инкапсуляцию имитирует - состояние всё так же торчит наружу, просто через две скобки.
Абстракция в Java двулика. Интерфейс - контракт возможностей, их можно реализовать хоть пять штук. Абстрактный класс - общая база с состоянием, и родитель может быть только один. С появлением default-методов граница стала тоньше, но состояние по-прежнему бывает только у класса, и это и есть критерий выбора.
// Слово final значит три разных вещи в зависимости от места: класс не наследуется, метод не переопределяется, переменная не переприсваивается. И третье не делает объект неизменяемым: final-ссылка на список запрещает подсунуть другой список, а добавлять в текущий никто не мешает.
- инвариант
- условие, которое объект обязан соблюдать всегда
- default-метод
- реализация прямо в интерфейсе, но без состояния
Как отвечать: «Чем перегрузка отличается от переопределения?»
Разница в том, когда принимается решение. Переопределение это полиморфизм в рантайме: подкласс заменяет реализацию метода, и вызов уходит по фактическому типу объекта, каким бы ни был тип ссылки. Перегрузка это выбор на компиляции: несколько методов с одним именем, и компилятор подбирает вариант по статическим типам аргументов. Отсюда известный подвох с перегрузкой: при методах для String и для Object вызов с null уходит в String, потому что берётся самый специфичный подходящий тип. Второй подвох важнее: статические методы и поля не переопределяются, а скрываются. Я это проверял - через ссылку типа родителя метод даёт значение наследника, а одноимённое поле даёт родительское, потому что поля разрешаются по типу ссылки. Полиморфизма там нет вовсе. Поэтому на всех переопределяемых методах ставлю @Override: без неё опечатка в сигнатуре тихо создаёт новый метод, который никогда не вызовется.
Сильный ответ: разведены времена принятия решения, назван неочевидный случай со статикой и полями, показана практика с @Override. Видно человека, который видел эти грабли, а не читал определения.
На чём валят
- −Ждут полиморфизма от статических методов и полей. Они разрешаются по типу ссылки, и спрашивают это обычно кодом «что напечатает».
- −Перегрузка с null: вызов уходит в самый специфичный тип, то есть в m(String), а не в m(Object).
- −Считают final-ссылку неизменяемым объектом. Переприсвоить нельзя, а мутировать содержимое можно свободно.
- −Наследуют класс ради его методов. Ждут ответа про композицию: делегирование без хрупкой иерархии.
- −Приводят квадрат и прямоугольник как пример «является» и не могут объяснить, почему он ломает подстановку.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 22, остальные разбираются в тренажёре.
- Почему совет «предпочитай композицию наследованию» так распространён?A)Композиция работает заметно быстрее в рантайме, ведь ей не нужно тратить время на поиск метода в цепочке родителейB)Наследование в Java запрещает переопределять методы, а композиция это позволяет делатьC)Наследование жёстко привязывает к реализации родителя (fragile base class); композиция гибчеD)Композиция позволяет наследоваться сразу от нескольких классов, обходя запрет Java на это
показать ответ и разбор
+C)Наследование жёстко привязывает к реализации родителя (fragile base class); композиция гибче// разбор: Наследование раскрывает подклассу детали родителя и связывает их намертво: изменение внутренней реализации базового класса может незаметно сломать наследников (проблема fragile base class), а иерархия навязывает отношение «is-a» даже там, где нужно лишь переиспользовать поведение. Композиция (объект держит другой объект и делегирует ему) связывает через узкий контракт, заменяема в рантайме и не тащит лишнее из родителя. Наследование остаётся для настоящего «is-a» и полиморфизма. Скорость тут ни при чём.
- Чем override отличается от overload?A)Override меняет тело метода прямо в самом классе-родителе, а overload лишь копирует этот метод в класс-потомок без правокB)Override и overload — синонимы; разница лишь в том, статический метод или обычныйC)Override работает только для интерфейсов, а overload — только для абстрактных классовD)Override — та же сигнатура в подклассе (рантайм-выбор), overload — иные параметры в классе (компайл-тайм)
показать ответ и разбор
+D)Override — та же сигнатура в подклассе (рантайм-выбор), overload — иные параметры в классе (компайл-тайм)// разбор: Overload (перегрузка) — несколько методов с одним именем, но разными параметрами в пределах класса; какой вызвать, решает компилятор по типам аргументов (статически). Override (переопределение) — подкласс даёт свою реализацию метода с ТОЙ ЖЕ сигнатурой, что у родителя; выбор происходит в рантайме по типу объекта (динамически). Перегрузка — про удобство API, переопределение — про полиморфизм. @Override — подсказка компилятору проверить, что метод действительно переопределяет родительский.
- Когда выбрать абстрактный класс, а когда интерфейс (Java 8+)?A)Абстрактный класс — общее состояние и один родитель; интерфейс — контракт и много реализацийB)Интерфейс с default-методами вытеснил абстрактные классы — после Java 8 применять их незачемC)Абстрактный класс нужен ради полей, а интерфейс не умеет содержать методы с реализациейD)Разницы нет: оба задают только сигнатуры методов и не хранят реализацию и данные
показать ответ и разбор
+A)Абстрактный класс — общее состояние и один родитель; интерфейс — контракт и много реализаций// разбор: Абстрактный класс уместен, когда у наследников есть общее СОСТОЯНИЕ (поля) и частично общая реализация; но класс можно унаследовать лишь один. Интерфейс задаёт контракт, класс реализует сколько угодно интерфейсов — это про способности/роли. После Java 8 интерфейсы умеют default- и static-методы (реализация есть), но не хранят состояние экземпляра. Правило: «is-a» с общим состоянием → абстрактный класс; «can-do»/множественные роли → интерфейс.
- Какие методы в Java связываются статически (на этапе компиляции), а не по типу объекта?A)Все методы, объявленные внутри абстрактного класса, независимо от их модификаторовB)Только методы, унаследованные от класса Object; прочие вызовы разрешаются динамическиC)private, static и final — их нельзя переопределить, вызов резолвится статическиD)Методы, возвращающие примитив, — компилятор знает тип результата заранее и вызывает напрямую
показать ответ и разбор
+C)private, static и final — их нельзя переопределить, вызов резолвится статически// разбор: Динамическая диспетчеризация нужна там, где метод можно переопределить. Если переопределить нельзя — вызов резолвится статически: private-методы не видны подклассу, static принадлежат классу (а не объекту), final запрещён к переопределению. Для них компилятор знает точную цель вызова. Обычные (public/protected нефинальные) методы связываются динамически — по фактическому классу объекта. Тип возвращаемого значения на способ связывания не влияет.
- Чем абстракция отличается от инкапсуляции?A)Это одно и то же понятие: оба означают сокрытие приватных полей класса за геттерамиB)Абстракция запрещает создавать объекты класса, а инкапсуляция разрешает, в этом вся разницаC)Абстракция относится к интерфейсам и абстрактным классам, а инкапсуляция — к полям примитивных типовD)Абстракция — выделить существенное и спрятать сложность за контрактом (что); инкапсуляция — сокрытие данных (как)
показать ответ и разбор
+D)Абстракция — выделить существенное и спрятать сложность за контрактом (что); инкапсуляция — сокрытие данных (как)// разбор: Понятия близкие, но про разное. Абстракция — про уровень модели: выделяем существенные для задачи свойства и прячем сложность за понятным контрактом (интерфейс, абстрактный класс), думаем в терминах «ЧТО делает», а не «как». Инкапсуляция — про механизм: держим данные и логику вместе и закрываем внутреннее состояние (private), отдавая доступ через методы, чтобы защитить инварианты («КАК устроено» — скрыто). Одно — про дизайн интерфейсов, другое — про защиту состояния; часто работают вместе.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.