сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Java: ядро языка

ООП и полиморфизм в 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, остальные разбираются в тренажёре.

  1. #oop_principles1 / 5
    Почему совет «предпочитай композицию наследованию» так распространён?
    A)Композиция работает заметно быстрее в рантайме, ведь ей не нужно тратить время на поиск метода в цепочке родителей
    B)Наследование в Java запрещает переопределять методы, а композиция это позволяет делать
    C)Наследование жёстко привязывает к реализации родителя (fragile base class); композиция гибче
    D)Композиция позволяет наследоваться сразу от нескольких классов, обходя запрет Java на это
    показать ответ и разбор
    +C)Наследование жёстко привязывает к реализации родителя (fragile base class); композиция гибче

    // разбор: Наследование раскрывает подклассу детали родителя и связывает их намертво: изменение внутренней реализации базового класса может незаметно сломать наследников (проблема fragile base class), а иерархия навязывает отношение «is-a» даже там, где нужно лишь переиспользовать поведение. Композиция (объект держит другой объект и делегирует ему) связывает через узкий контракт, заменяема в рантайме и не тащит лишнее из родителя. Наследование остаётся для настоящего «is-a» и полиморфизма. Скорость тут ни при чём.

  2. #oop_principles2 / 5
    Чем override отличается от overload?
    A)Override меняет тело метода прямо в самом классе-родителе, а overload лишь копирует этот метод в класс-потомок без правок
    B)Override и overload — синонимы; разница лишь в том, статический метод или обычный
    C)Override работает только для интерфейсов, а overload — только для абстрактных классов
    D)Override — та же сигнатура в подклассе (рантайм-выбор), overload — иные параметры в классе (компайл-тайм)
    показать ответ и разбор
    +D)Override — та же сигнатура в подклассе (рантайм-выбор), overload — иные параметры в классе (компайл-тайм)

    // разбор: Overload (перегрузка) — несколько методов с одним именем, но разными параметрами в пределах класса; какой вызвать, решает компилятор по типам аргументов (статически). Override (переопределение) — подкласс даёт свою реализацию метода с ТОЙ ЖЕ сигнатурой, что у родителя; выбор происходит в рантайме по типу объекта (динамически). Перегрузка — про удобство API, переопределение — про полиморфизм. @Override — подсказка компилятору проверить, что метод действительно переопределяет родительский.

  3. #oop_principles3 / 5
    Когда выбрать абстрактный класс, а когда интерфейс (Java 8+)?
    A)Абстрактный класс — общее состояние и один родитель; интерфейс — контракт и много реализаций
    B)Интерфейс с default-методами вытеснил абстрактные классы — после Java 8 применять их незачем
    C)Абстрактный класс нужен ради полей, а интерфейс не умеет содержать методы с реализацией
    D)Разницы нет: оба задают только сигнатуры методов и не хранят реализацию и данные
    показать ответ и разбор
    +A)Абстрактный класс — общее состояние и один родитель; интерфейс — контракт и много реализаций

    // разбор: Абстрактный класс уместен, когда у наследников есть общее СОСТОЯНИЕ (поля) и частично общая реализация; но класс можно унаследовать лишь один. Интерфейс задаёт контракт, класс реализует сколько угодно интерфейсов — это про способности/роли. После Java 8 интерфейсы умеют default- и static-методы (реализация есть), но не хранят состояние экземпляра. Правило: «is-a» с общим состоянием → абстрактный класс; «can-do»/множественные роли → интерфейс.

  4. #oop_principles4 / 5
    Какие методы в Java связываются статически (на этапе компиляции), а не по типу объекта?
    A)Все методы, объявленные внутри абстрактного класса, независимо от их модификаторов
    B)Только методы, унаследованные от класса Object; прочие вызовы разрешаются динамически
    C)private, static и final — их нельзя переопределить, вызов резолвится статически
    D)Методы, возвращающие примитив, — компилятор знает тип результата заранее и вызывает напрямую
    показать ответ и разбор
    +C)private, static и final — их нельзя переопределить, вызов резолвится статически

    // разбор: Динамическая диспетчеризация нужна там, где метод можно переопределить. Если переопределить нельзя — вызов резолвится статически: private-методы не видны подклассу, static принадлежат классу (а не объекту), final запрещён к переопределению. Для них компилятор знает точную цель вызова. Обычные (public/protected нефинальные) методы связываются динамически — по фактическому классу объекта. Тип возвращаемого значения на способ связывания не влияет.

  5. #oop_principles5 / 5
    Чем абстракция отличается от инкапсуляции?
    A)Это одно и то же понятие: оба означают сокрытие приватных полей класса за геттерами
    B)Абстракция запрещает создавать объекты класса, а инкапсуляция разрешает, в этом вся разница
    C)Абстракция относится к интерфейсам и абстрактным классам, а инкапсуляция — к полям примитивных типов
    D)Абстракция — выделить существенное и спрятать сложность за контрактом (что); инкапсуляция — сокрытие данных (как)
    показать ответ и разбор
    +D)Абстракция — выделить существенное и спрятать сложность за контрактом (что); инкапсуляция — сокрытие данных (как)

    // разбор: Понятия близкие, но про разное. Абстракция — про уровень модели: выделяем существенные для задачи свойства и прячем сложность за понятным контрактом (интерфейс, абстрактный класс), думаем в терминах «ЧТО делает», а не «как». Инкапсуляция — про механизм: держим данные и логику вместе и закрываем внутреннее состояние (private), отдавая доступ через методы, чтобы защитить инварианты («КАК устроено» — скрыто). Одно — про дизайн интерфейсов, другое — про защиту состояния; часто работают вместе.

дальше

Теорию прочитали. Навык ставится повторением

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