Menu

Полиморфизм в Java: один интерфейс, множество форм

Как полиморфизм в Java позволяет одной переменной ссылаться на множество типов, почему переопределённые методы выбираются во время выполнения и как безопасно использовать upcasting, downcasting и instanceof.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Одна ссылка, множество типов

Полиморфизм — это плата за наследование и интерфейсы. Он означает, что одна переменная, объявленная как родитель (или интерфейс), может хранить объект любого подтипа, и когда вы вызываете на ней метод, Java выполняет версию, принадлежащую фактическому классу объекта, а не версию, подразумеваемую объявленным типом переменной.

Основу вы уже видели на странице о наследовании: подкласс переопределяет метод своего родителя. Полиморфизм — это то, что делает такое переопределение оправданным: он позволяет писать код на основе общего типа и давать каждому конкретному объекту вести себя по-своему.

И a, и b объявлены как Animal, однако каждый выводит свой собственный звук. Этот выбор происходит во время выполнения на основе реального объекта.

Динамическая диспетчеризация методов

Механизм, стоящий за этим, — динамическая диспетчеризация методов (dynamic method dispatch): для переопределённого метода экземпляра JVM смотрит на класс объекта во время выполнения, чтобы решить, какую реализацию вызвать. Компилятор лишь проверяет, что метод существует у объявленного типа; фактический выбор откладывается до запуска программы.

Именно это позволяет одному циклу обрабатывать целую смесь типов, ни разу не спрашивая, что собой представляет каждый из них:

Цикл знает только о Shape. Добавьте позже Triangle extends Shape, и этот код продолжит работать без изменений — в этом весь смысл. Код зависит от абстракции, а не от конкретного списка типов.

Upcasting и downcasting

Хранение Dog в переменной Animal — это upcasting: подъём вверх по иерархии к более общему типу. Он всегда безопасен, и Java делает его неявно, потому что каждый Dog является Animal.

Движение в обратном направлении — это downcasting: взять родительскую ссылку и рассматривать её как конкретный подтип. Это допустимо только если объект действительно является этим подтипом, поэтому приведение нужно записывать явно, и вы рискуете получить ClassCastException, если ошибётесь:

Последнее приведение прекрасно компилируется — компилятор не может доказать, что оно неверно, — но падает при запуске, потому что Cat не является Dog. Никогда не выполняйте downcast вслепую.

Защищайте downcast'ы с помощью instanceof

Перед downcasting проверьте реальный тип через instanceof. Современная Java позволяет привязать результат прямо в том же выражении (сопоставление с образцом для instanceof), так что отдельное приведение не нужно:

instanceof возвращает false для null, поэтому эта проверка также защищает вас от NullPointerException. При этом, если вы замечаете, что пишете длинные цепочки instanceof, это часто признак того, что поведение должно находиться внутри классов в виде переопределённого метода — пусть полиморфизм сам выполняет ветвление за вас.

Overriding против overloading

Эти два понятия звучат похоже, но не связаны между собой, и их путаница — классический источник недоразумений.

Overriding (переопределение) — это когда подкласс заменяет метод родителя с идентичной сигнатурой. Оно разрешается во время выполнения по типу объекта — это тот полиморфизм, который мы использовали.

Overloading (перегрузка) — это когда у одного класса есть несколько методов с одинаковым именем, но разными списками параметров. Оно разрешается во время компиляции по типам аргументов, без какой-либо диспетчеризации во время выполнения:

Компилятор выбирает подходящий describe исключительно по статическому типу аргумента. Здесь не участвует ни родительский, ни дочерний объект, поэтому это не полиморфизм времени выполнения — это просто повторное использование имени метода.

Распространённая ловушка: поля не полиморфны

Только методы экземпляра диспетчеризуются во время выполнения. Поля и статические методы разрешаются по объявленному типу, на чём многие спотыкаются:

p.name() выполняет версию Child (полиморфизм), но p.label читает поле Parent, потому что поля скрываются, а не переопределяются. Решение простое: держите поля private и обращайтесь к ним только через методы, чтобы полиморфный вызов всегда побеждал.

Далее: модификаторы доступа

Полиморфизм работает чисто только тогда, когда подклассы могут видеть и переопределять нужные члены, а остальной ваш код не может вмешаться и нарушить инварианты. Этот баланс контролируется доступом public, protected, private и пакетным — модификаторами доступа, о которых пойдёт речь далее.

Часто задаваемые вопросы

Что такое полиморфизм в Java?

Полиморфизм означает, что один ссылочный тип может указывать на объекты множества разных классов, а метод, который действительно выполняется, выбирается по реальному типу объекта во время выполнения, а не по объявленному типу переменной. Так, переменная Shape shape может хранить Circle или Square, и вызов shape.area() автоматически выполнит нужную версию.

В чём разница между overriding и overloading в Java?

Overriding (переопределение) — это когда подкласс заменяет метод суперкласса с той же сигнатурой; именно это и обеспечивает полиморфизм времени выполнения. Overloading (перегрузка) — это когда у одного класса есть несколько методов с одинаковым именем, но разными списками параметров; компилятор выбирает один из них во время компиляции по аргументам. Переопределение разрешается во время выполнения по типу объекта; перегрузка разрешается во время компиляции по типам аргументов.

В чём разница между upcasting и downcasting в Java?

Upcasting рассматривает дочерний объект как его родительский тип (Animal a = new Dog();) — всегда безопасен и обычно неявный. Downcasting идёт в обратном направлении (Dog d = (Dog) a;) и безопасен только если объект действительно является этим подтипом; иначе он бросает ClassCastException. Защищайте каждый downcast предварительной проверкой instanceof.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ