Полиморфизм

  • Рубрика записи:ООП

Полиморфизм – это не то, что вы подумали. На самом деле, полиморфизм встречается далеко не только в ООП. Да, он – один из принципов ООП, помимо абстракции, инкапсуляции и наследования. Но полиморфизм не ограничивается этой парадигмой.

Итак, после такого загадочного вступления, приступим к сути!

Полиморфизм шире, чем кажется

Полиморфизм появился задолго до первых языков с поддержкой объектно-ориентированного подхода.

Чаще всего под понятием полиморфизма понимают именно ООП-шную часть – когда мы берём класс или интерфейс, наследуем от него разные классы и путём переопределения методов базового класса меняем поведение в производных классах. А потом можем со всеми объектами работать одинаковым образом и в переменную с типом базового класса на самом деле класть объект производного класса.

Однако это далеко не всё.

Полиморфизм в программировании — способность использовать один и тот же код с разными типами переменных. Да, никакого наследования, объектов или интерфейсов в определении!

Если кратко, то полиморфизм можно условно поделить так:

Схема видов полиморфизма: статический (обобщённые типы, неявное приведение, перегрузка, операторы) и динамический (переопределение)

Давайте по очереди разберёмся со всеми видами полиморфизма. И в первую очередь – с самым популярным.

Динамический полиморфизм

Динамический полиморфизм подразумевает, что выбор конкретной операции происходит во время выполнения программы – в зависимости от предоставленного типа переменной.

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

Мы можем работать со многими классами одинаково, если они все – наследники одного и того же класса (или интерфейса). Получается, ООП-шный полиморфизм без наследования не существует. В переменную с типом класса-родителя мы можем положить объект класса-наследника:

Очень важная ремарка – при преобразовании ссылочных типов (когда объект типа Phone мы кладём в переменную с типом Device, например) сам объект физически ни во что не преобразовывается. Однако после преобразования с ним можно работать как с объектом типа Device.

Благодаря полиморфизму классов можно работать с объектами разных классов-наследников Device как с объектами Device. Например, добавить их в один массив:

Давайте рассмотрим пример с переопределением методов. Он позволит изменять поведение классов-наследников. Допустим, мы пишем калькулятор. Он будет уметь делать бинарные операции, где требуются 2 операнда: +, -, * и /. Ну ещё давайте степень ^ добавим. И мы хотим, чтобы вся работа с операциями была однородной. Чтобы нам не нужно было знать, какая конкретная операция выполняется – чтобы мы со всеми этими операциями могли работать одинаковым образом.

Операция – это абстрактное понятие, которое ничего не говорит о реализации. Это будет у нас абстрактный класс Operation:

Этот класс имеет 2 операнда типа double (причём protected, чтобы классы-наследники имели доступ к операндам для расчётов). Также он самостоятельно определяет метод toString(), переопределяя его реализацию из класса Object. Помимо этого, абстрактный класс Operation имеет 2 абстрактных метода. Метод calculate() будет рассчитывать результат выражения. В свою очередь, метод getToken() возвращает строковое представление символа операции (например, “+”). Это понадобилось как раз для метода toString() – чтобы строковое представление операции включало её символ. Ведь у абстрактной операции нет символа.

Экземпляр абстрактного класса нельзя создать. Зато от него можно наследовать другие классы. Они будут конкретными реализациями операций +, -, *, / и ^:

Думаю, насчёт остальных операций вы уже догадались. Что нам вся эта иерархия даёт? Так мы теперь можем со всеми операциями работать, как с Operation! Более того, благодаря полиморфизму мы в любой момент сможем расширить количество возможных операций, просто дописав новых классов-наследников от класса Operation. И для этого не придётся изменять код ни Operation, ни имеющихся классов-наследников, ни тот код, что обращается к классу Operation и его методам.

Например, внутри функции processOperation() абсолютно нет необходимости знать, какая именно операция была ей передана. Ей вообще всё равно. Без разницы! Ведь со всеми операциями, наследуемыми от Operation, можно работать, как с Operation:

Да, и например, можно сложить всё в один список, всех самых разных операций напихать, а потом куда-нибудь передать и в цикле всё вывести в консоль:

Вот такие чудеса. Полиморфизм в ООП дарит взаимозаменяемость. Один компонент можно вставить на место другого и менять их как перчатки. Всё равно один и тот же код сможет обрабатывать объекты разных классов. Это потому, что они имеют единое определение в Operation. Они имеют единые методы для доступа к ним. При этом, если объявить какой-нибудь дополнительный метод в PlusOperation, например, то вызвать его не получится, если у вас будет Operation operation = new PlusOperation(). Получится только при PlusOperation operation = new PlusOperation(). То есть при контейнере этого класса, а не родительского.

Статический полиморфизм

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

Перегрузка

Перегрузка – это полиморфизм. Одно имя функциинесколько реализаций. А какую применить в каждом конкретном случае – зависит от типов переменных. Только это уже не динамический полиморфизм, как переопределение, а статический. Потому что в случае перегрузки во время компиляции компилятор знает, какую функцию связать с вызовом для упавшего в неё типа аргумента. Посмотрите на это:

f(Class1 obj1), f(Class2 obj2) чем-то похоже на obj1.f(), obj2.f().

В коде это сравнение бы выглядело примерно так:

Аргумент у перегрузки функции может быть не один, а несколько. Перегруженные функции с одинаковыми именами всё равно представляют полиморфизм – разные типы аргументов, разное количество или их порядок. Но – одно имя (функции).

Обобщённые типы (generic)

Обобщённые типы это не то же самое, что множество наследников одного класса. Ведь generic-и имеют кучу ограничений. И есть базовое различие:

  • разные классы-наследники реализуют разную логику методов с одним и тем же названием,
  • разные обобщённые типы реализуют одну и ту же логику, абстрагируясь от разницы в типах.

Получается, параметризация, обобщение типов – синоним абстракции типа. Мы не обращаем внимание на тип и проделываем одни и те же операции со всеми. То есть логика одинакова.

За горизонт для примеров плыть не надо. Список List – хороший пример. Это класс, напрямую использующий обобщение типов. В треугольных скобочках вы можете указать, какой тип будет складываться в список. При этом, абсолютно неважно, какой это тип. Операции с ним будут производиться те же самые: складывание в список. То есть абстрагируемся от разницы в типах.

Попробуем реализовать свой собственный простенький класс, использующий обобщение типов:

Это коробка с массивом элементов внутри. Нам вообще без разницы, какого типа элементы добавлять в массив. А также – какого типа элементы возвращать из массива по индексу (а если индекс за пределами диапазона, порядочно бросаем исключение). Так и работают generic-и.

Затем можно работать с ArrayBox, указывая при создании тип в треугольных скобках:

Кстати, можно так не один тип задавать, а несколько. Как, например, бывает в словарях, например, HashMap<String, Integer>. Два обобщённых типа:

Неявное приведение типов

Неявное приведение типов относится к статическому полиморфизму. Да, это тоже полиморфизм. Всё потому, что вы можете с разными типами данных работать одинаково. Ставить +, – и так далее. А компилятор сам уже порешает, что и куда неявно привести. Приведение типов происходит на этапе компиляции программы, когда компилятор автоматически преобразует один тип данных в другой без явного указания программистом.

Когда в вычислениях int приводится к double, компилятор Java автоматически выполняет преобразование типа данных, так как double имеет больший диапазон значений, чем int, и такое преобразование не приводит к потере данных:

Здесь сложение отрабатывает точно так же, как если бы int-овая переменная имела тип double. Если бы здесь не было статического полиморфизма, компилятор бы ругался на такую операцию, и надо было бы приводить типы вручную:

Теперь насчёт приведения в параметрах функций. Если у функции записан параметр double в сигнатуре, однако она может принимать и аргумент int, и аргумент double и неявно их переводить друг в друга, то она считается полиморфной. Компилятор обнаруживает несоответствие типов и самостоятельно приводит их к нужному типу. Например, int преобразуется в double перед передачей его в функцию:

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

Занимательный факт: Из-за того, что в Python и JavaScript типизация динамическая, все переменные изначально обладают полиморфизмом. Они все не имеют строгого типа – следовательно, могут содержать значение любого типа. То есть даже не нужно прописывать типы переменных или типы параметров функции. Но в книжке по теории и практике языков программирования сказано, что в языках программирования с динамической типизацией статический полиморфизм не несёт смысловой нагрузки, так как для переменных не объявляются статические типы.

Операторы

Не ожидали, что такой обычный оператор, как, например, + тоже относится к полиморфизму? А вы посмотрите, какой он многоликий и как подстраивается под ситуацию:

  • для числовых типов данных (int, float, double) оператор + выполняет арифметическую операцию – сложение,
  • для строк оператор + выполняет конкатенацию, то есть объединение строк в одну строку,
  • для объектов оператор + пытается выполнить конкатенацию их строковых представлений, то есть вызывается метод toString().

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

Вот мы и разобрали целую кучу видов полиморфизма. Да-да, он не ограничивается одним лишь ООП, как оказалось. Но вы всё равно знайте, что, когда люди говорят волшебное слово “полиморфизм”, они с вероятностью в 98% имеют в виду именно объектно-ориентированный полиморфизм.

Добавить комментарий