Полиморфизм в Java

🔥 🚀 Важно для всех, кто работает с 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), может принимать различные формы (типы), указанные справа (BreakdancerElectricBoogieDancer). Каждая такая “форма” может иметь свое уникальное поведение относительно общей функциональности, определенной в суперклассе (метод 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() (из HumanFish или Uboat) будет выполнена.

В Java по умолчанию используется позднее (динамическое) связывание, в связи с чем решение о выборе метода принимается во время выполнения программы, а не на этапе компиляции (как при раннем связывании).

Когда компилятор обрабатывает этот код…

for (Swim s : swimmers) {
            s.swim();
}
Run Code

… он не знает, реализация какого именно класса (HumanFish или Uboat) будет вызвана при выполнении swim(). Это определяется только во время выполнения программы благодаря механизму динамического связывания.

При загрузке и инициализации объектов JVM создает в памяти специальные таблицы, связывающие переменные с их значениями, а объекты – с их методами. В случае, когда класс наследуется от другого класса или реализует интерфейс, JVM сначала проверяет фактический (реальный) тип объекта. Если для этого типа существует переопределённая версия метода, будет вызвана именно она. Иначе поиск метода продолжится в родительском классе и далее вверх по иерархии наследования, пока не будет найдена подходящая реализация.

Начиная с Java 8 при создании абстрактных классов и интерфейсов можно использовать ключевое слово default:

public interface CanSwim {
    default void swim() {
        System.out.println("I just swim");
    }
}
Run Code

Иногда интервьюеры спрашивают, как должны быть объявлены методы в базовых классах, чтобы не нарушался принцип полиморфизма. Ответ прост: эти методы не должны иметь модификаторов staticprivate или final. Модификатор private делает метод доступным только внутри класса, поэтому его нельзя переопределить в подклассе. Модификатор static связывает метод с классом, а не с отдельными объектами, поэтому будет вызываться именно метод суперкласса, а не переопределённая версия. Модификатор final запрещает переопределение метода в подклассах, делая его реализацию фиксированной.

Применение

Полиморфизм:

1. Позволяет подменять реальные классы их наследниками или реализациями интерфейсов, что применяется в тестировании.

2. Обеспечивает расширяемость, упрощая создание базовой структуры, которую можно дорабатывать в будущем. Добавление новых типов на основе существующих — самый распространенный способ расширения функциональности в ООП.

3. Позволяет объединять объекты с общим поведением в одну коллекцию и обрабатывать их единообразно (как в наших с dance() и swim())

4. Обеспечивает гибкость при создании новых типов: можно использовать реализацию метода из родительского класса или переопределить его в подклассе.

Перевод статьи «Java Polymorphism».

Если чувствуете, что нужно повторить основы, вам поможет статья «Классы и объекты в Java»

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх