🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!
Вопросы, связанные с ООП, являются неотъемлемой частью технического собеседования на позицию Java-разработчика в IT-компании. В этой статье мы рассмотрим один из принципов ООП – полиморфизм. Мы сосредоточимся на аспектах, которые часто затрагиваются во время собеседований, а также приведем несколько примеров для наглядности.

Что такое полиморфизм в Java?
Полиморфизм — это способность программы обрабатывать объекты с одинаковым интерфейсом единообразно, без информации о конкретном типе объекта. Чтобы избежать дополнительных вопросов по этой теме на собеседовании, лучше всего стараться сразу дать исчерпывающий ответ.
Можно начать с того, что в основе ООП лежит механизм создания объектов на основе классов, таких своеобразных “чертежей”. Стоит также отметить, что Java является строго типизированным языком, поэтому в коде всегда необходимо указывать тип объекта при объявлении переменных. Это повышает безопасность и надежность кода, а также позволяет на этапе компиляции предотвратить ошибки, связанные с несовместимостью типов (например, попытку разделить строку на число).
Обратите внимание интервьюера на то, что если у нас есть переменная, объявленная как базовый класс, то в неё можно присвоить не только объект этого базового класса, но и объект любого класса-наследника. Это важный момент: мы можем обрабатывать объекты разных классов-наследников как объекты базового типа, если они происходят от общего родительского класса. Это возможно благодаря такому принципу ООП, как полиморфизм.
Интервьюеру понравится, если вы приведете пример. Выберите класс, который может быть общим для нескольких классов (базовым), и создайте несколько наследников.
Пример
Базовый класс:
public class Dancer {
private String name;
private int age;
public Dancer(String name, int age) {
this.name = name;
this.age = age;
}
public void dance() {
System.out.println(toString() + " I dance like everyone else.");
}
@Override
public String toString() {
Return "I'm " + name + ". I'm " + age + " years old.";
}
}
Run CodeПодклассы с переопределением метода:
public class ElectricBoogieDancer extends Dancer {
public ElectricBoogieDancer(String name, int age) {
super(name, age);
}
// Переопределение метода базового класса
@Override
public void dance() {
System.out.println(toString () + " I dance the electric boogie!");
}
}
public class Breakdancer extends Dancer {
public Breakdancer(String name, int age) {
super(name, age);
}
// Переопределение метода базового класса
@Override
public void dance() {
System.out.println(toString() + " I breakdance!");
}
}
Run CodeПример использования полиморфизма:
public class Main {
public static void main(String[] args) {
Dancer dancer = new Dancer("Fred", 18);
Dancer breakdancer = new Breakdancer("Jay", 19); // Расширяющее преобразование к базовому типу
Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20); // Расширяющее преобразование к базовому типу
List<dancer> disco = Arrays.asList(dancer, breakdancer, electricBoogieDancer);
for (Dancer d : disco) {
d.dance(); // Вызов полиморфного метода
}
}
}
Run CodeВ методе main
обратите внимание на строки:
Dancer breakdancer = new Breakdancer("Jay", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20);
Run CodeЗдесь объявляется переменная суперкласса, и ей присваивается объект, являющийся экземпляром одного из его подклассов. Возможно, вас спросят, почему компилятор не выдает ошибку из-за несоответствия типов слева и справа от оператора присваивания, ведь язык Java строго типизирован. Объясните, что в этом случает работает расширяющее преобразование типов: ссылка на объект рассматривается как ссылка на его базовый класс. Компилятор Java автоматически и без явного указания программиста выполняет преобразование типа подкласса к типу его суперкласса.
В примере видно, что тип, указанный слева от оператора присваивания (Dancer
), может принимать различные формы (типы), указанные справа (Breakdancer
, ElectricBoogieDancer
). Каждая такая “форма” может иметь свое уникальное поведение относительно общей функциональности, определенной в суперклассе (метод dance
). Иными словами, метод, объявленный в суперклассе, может быть по-разному реализован в его подклассах. В данном случае мы имеем дело с переопределением метода, благодаря которому возможны различные формы поведения. Для наглядного примера запустим код в методе main
:
I'm Fred. I'm 18 years old. I dance like everyone else.
I'm Jay. I'm 19 years old. I breakdance!
I'm Marcia. I'm 20 years old. I dance the electric boogie!
Если не переопределять метод в подклассах, то невозможно будет задать разное поведение. Например, если закомментировать метод dance
в классах Breakdancer
и ElectricBoogieDancer
, вывод программы станет таким:
I'm Fred. I'm 18 years old. I dance like everyone else.
I'm Jay. I'm 19 years old. I dance like everyone else.
I'm Marcia. I'm 20 years old. I dance like everyone else.
А это значит, что создавать классы Breakdancer
и ElectricBoogieDancer
просто не имеет смысла.
Принцип полиморфизма проявляется в тот момент, когда программа использует объект, не зная его конкретного типа. В нашем примере это происходит при вызове метода dance()
у объекта Dancer d
. Суть в том, что программе не важно, является ли объект экземпляром Breakdancer
или ElectricBoogieDancer
— важно лишь, что он наследуется от класса Dancer
.
Интерфейс в Java
В Java наследование работает двумя способами: через extends
для классов и implements
для интерфейсов. Стоит упомянуть, что Java не поддерживает множественное наследование: у класса может быть только один “родитель”. Тем не менее, класс может реализовывать несколько интерфейсов, которые используются для добавления разных методов.
В отличие от наследования (которое создает жесткую связь между классами), интерфейсы обеспечивают слабую связанность. Это значит, что класс зависит только от обязательных методов интерфейса, а не от конкретной реализации родительского класса. В Java интерфейс является ссылочным типом, поэтому в программе можно объявить переменную интерфейсного типа.
Настало время привести пример. Вот как выглядит создание интерфейса:
public interface CanSwim {
void swim();
}
Run CodeДля наглядности мы возьмём несколько совершенно разных классов и заставим их реализовать один и тот же интерфейс:
public class Human implements CanSwim {
private String name;
private int age;
public Human(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void swim() {
System.out.println(toString()+" I swim with an inflated tube.");
}
@Override
public String toString() {
return "I'm " + name + ". I'm " + age + " years old.";
}
}
public class Fish implements CanSwim {
private String name;
public Fish(String name) {
this.name = name;
}
@Override
public void swim() {
System.out.println("I'm a fish. My name is " + name + ". I swim by moving my fins.");
}
public class UBoat implements CanSwim {
private int speed;
public UBoat(int speed) {
this.speed = speed;
}
@Override
public void swim() {
System.out.println("I'm a submarine that swims through the water by rotating screw propellers. My speed is " + speed + " knots.");
}
}
Run CodeМетод main
:
public class Main {
public static void main(String[] args) {
CanSwim human = new Human("John", 6);
CanSwim fish = new Fish("Whale");
CanSwim boat = new UBoat(25);
List<swim> swimmers = Arrays.asList(human, fish, boat);
for (Swim s : swimmers) {
s.swim();
}
}
}
Run CodeРезультаты вызова полиморфного метода, определённого в интерфейсе, демонстрируют различия в поведении типов, реализующих этот интерфейс. В нашем случае это разные строки, выводимые методом swim()
.
Изучив наш пример, интервьюер может спросить, почему при выполнении этого кода в методе main
for (Swim s : swimmers) {
s.swim();
}
Run Codeвызываются именно переопределённые методы из подклассов? Как во время работы программы выбирается нужная реализация метода?
Связывание в Java
Чтобы ответить на эти вопросы, необходимо объяснить механизм позднего (динамического) связывания (late/dynamic binding).
Связывание — это процесс сопоставления вызова метода с его конкретной реализацией в классе. Проще говоря, код определяет, какая из трёх возможных версий метода swim()
(из Human
, Fish
или Uboat
) будет выполнена.
В Java по умолчанию используется позднее (динамическое) связывание, в связи с чем решение о выборе метода принимается во время выполнения программы, а не на этапе компиляции (как при раннем связывании).
Когда компилятор обрабатывает этот код…
for (Swim s : swimmers) {
s.swim();
}
Run Code… он не знает, реализация какого именно класса (Human
, Fish
или Uboat
) будет вызвана при выполнении swim()
. Это определяется только во время выполнения программы благодаря механизму динамического связывания.
При загрузке и инициализации объектов JVM создает в памяти специальные таблицы, связывающие переменные с их значениями, а объекты – с их методами. В случае, когда класс наследуется от другого класса или реализует интерфейс, JVM сначала проверяет фактический (реальный) тип объекта. Если для этого типа существует переопределённая версия метода, будет вызвана именно она. Иначе поиск метода продолжится в родительском классе и далее вверх по иерархии наследования, пока не будет найдена подходящая реализация.
Начиная с Java 8 при создании абстрактных классов и интерфейсов можно использовать ключевое слово default
:
public interface CanSwim {
default void swim() {
System.out.println("I just swim");
}
}
Run CodeИногда интервьюеры спрашивают, как должны быть объявлены методы в базовых классах, чтобы не нарушался принцип полиморфизма. Ответ прост: эти методы не должны иметь модификаторов static
, private
или final
. Модификатор private
делает метод доступным только внутри класса, поэтому его нельзя переопределить в подклассе. Модификатор static
связывает метод с классом, а не с отдельными объектами, поэтому будет вызываться именно метод суперкласса, а не переопределённая версия. Модификатор final
запрещает переопределение метода в подклассах, делая его реализацию фиксированной.
Применение
Полиморфизм:
1. Позволяет подменять реальные классы их наследниками или реализациями интерфейсов, что применяется в тестировании.
2. Обеспечивает расширяемость, упрощая создание базовой структуры, которую можно дорабатывать в будущем. Добавление новых типов на основе существующих — самый распространенный способ расширения функциональности в ООП.
3. Позволяет объединять объекты с общим поведением в одну коллекцию и обрабатывать их единообразно (как в наших с dance()
и swim()
)
4. Обеспечивает гибкость при создании новых типов: можно использовать реализацию метода из родительского класса или переопределить его в подклассе.
Перевод статьи «Java Polymorphism».
Если чувствуете, что нужно повторить основы, вам поможет статья «Классы и объекты в Java»