Что такое параметрический полиморфизм в Java (на примере)?

Толкование

Полиморфизм (программирование)

Полиморфи́зм (от греч.πολὺ- — много, и μορφή — форма) в языках программирования — возможность объектов с одинаковой спецификацией иметь различную реализацию.

Язык программирования поддерживает полиморфизм, если классы с одинаковой спецификацией могут иметь различную реализацию — например, реализация класса может быть изменена в процессе наследования[1].

Кратко смысл полиморфизма можно выразить фразой: «Один интерфейс, множество реализаций».

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

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

  • внешняя общность проявляется как одинаковый набор методов с одинаковыми именами и сигнатурами (именами методов, типами аргументов и их количеством);
  • внутренняя общность — одинаковая функциональность методов. Её можно описать интуитивно или выразить в виде строгих законов, правил, которым должны подчиняться методы. Возможность приписывать разную функциональность одному методу (функции, операции) называется перегрузкой метода (перегрузкой функций, перегрузкой операций).

Примеры

Класс геометрических фигур (эллипс, многоугольник) может иметь методы для геометрических трансформаций (смещение, поворот, масштабирование).

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

В объектно-ориентированных языках

В объектно-ориентированных языках класс является абстрактным типом данных.[Прим. 1] Полиморфизм реализуется с помощью наследования классов и виртуальных функций. Класс-потомок наследует сигнатуры методов класса-родителя, а реализация, в результате переопределения метода, этих методов может быть другой, соответствующей специфике класса-потомка. Другие функции могут работать с объектом как с экземпляром класса-родителя, но если при этом объект на самом деле является экземпляром класса-потомка, то во время исполнения будет вызван метод, переопределенный в классе-потомке. Это называется поздним связыванием. [Примером использования может служить обработка массива, содержащего экземпляры как класса-родителя, так и класса-потомка: очевидно, что такой массив может быть объявлен только как массив типа класса-родителя и у объектов массива могут вызываться только методы этого класса, но если в классе-потомке какие-то методы были переопределены, то в режиме исполнения для экземпляров этого класса будут вызваны именно они, а не методы класса-родителя.]

Класс-потомок сам может быть родителем. Это позволяет строить сложные схемы наследования — древовидные или сетевидные.

Абстрактные (или чисто виртуальные) методы не имеют реализации вообще (на самом деле некоторые языки, например C++, допускают реализацию абстрактных методов в родительском классе). Они специально предназначены для наследования. Их реализация должна быть определена в классах-потомках.

Класс может наследовать функциональность от нескольких классов. Это называется множественным наследованием. Множественное наследование создаёт известную проблему (в C++), когда класс наследуется от нескольких классов-посредников, которые в свою очередь наследуются от одного класса (так называемая «Проблема ромба»): если метод общего предка был переопределён в посредниках, неизвестно, какую реализацию метода должен наследовать общий потомок. Решается эта проблема путём отказа от множественного наследования для классов и разрешением множественного наследования для полностью абстрактных классов (то есть интерфейсов) (C#, Delphi, Java), либо через виртуальное наследование (C++).

В функциональных языках

Полиморфизм в функциональных языках будет рассмотрен на примере языка Haskell.

В Haskell существует два вида полиморфизма — параметрический (чистый) и специальный, (на основе классов[Прим. 2]). Специальный называют еще ad hoc (от лат. ad hoc — специально). Их можно отличить следующим образом:

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

В случае параметрического полиморфизма функция реализована для всех классов одинаково, и, таким образом, реализована вообще для произвольного типа данных. Например, функция сортировки одинакова для данных любого типа, если функция сравнения данных задана отдельно. См. также Метапрограммирование.

Специальный полиморфизм

Специальный (или лат. ad hoc) полиморфизм допускает специальную реализацию для данных каждого типа. Например, используемая в нашем примере функцией сортировки функция сравнения должна быть определена по-разному для чисел, кортежей, списков, т. е. она является специально полиморфной.

В Haskell есть деление на классы и экземпляры (instance), которого нет в ООП. Класс определяет набор и сигнатуры методов (возможно, задавая для некоторых или всех из них реализации по умолчанию), а экземпляры реализуют их. Таким образом, автоматически отпадает проблема множественного наследования. Классы не наследуют и не переопределяют методы других классов — каждый метод принадлежит только одному классу. Такой подход проще, чем сложная схема взаимоотношений классов в ООП. Некоторый тип данных может принадлежать нескольким классам; класс может требовать, чтобы каждый его тип обязательно принадлежал к другому классу, или даже нескольким; такое же требование может выдвигать экземпляр. Это аналоги множественного наследования. Есть и некоторые свойства, не имеющие аналогов в ООП. Например, реализация списка, как экземпляра класса сравнимых величин, требует, чтобы элементы списка также принадлежали к классу сравнимых величин.

Программистам, переходящим от ООП к ФП, следует знать важное отличие их системы классов. Если в ООП класс «привязан» к объекту, т. е. к данным, то в ФП — к функции. В ФП сведения о принадлежности к классу передаются при вызове функции, а не хранятся в полях объекта. Такой подход, в частности, позволяет решить проблему метода нескольких объектов (в ООП метод вызывается у одного объекта). Пример: метод сложения (чисел, строк) требует двух аргументов, причем одного типа.

Неявная типизация

В некоторых языках программирования (например, в Python и Ruby) применяется так называемая утиная типизация[2] (другие названия: латентная, неявная), которая представляет собой разновидность сигнатурного полиморфизма. Таким образом, например, в языке Python полиморфизм не обязательно связан с наследованием.

Формы полиморфизма

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

(упоминается в классической книге Саттера и Александреску, которая является источником).

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

В одном случае конкретный смысл фрагмента зависит от того, в каком окружении код был построен. Это т.н. статический полиморфизм. Перегрузка функций, шаблоны в Си++ реализуют именно статический полиморфизм. Если в коде шаблонного класса вызвана, например, std::sort, то реальный смысл вызова зависит от того, для каких именно типовых параметров будет развернут данный шаблон — вызовется одна из std::sort.

В другом случае конкретный смысл фрагмента определяется только на этапе исполнения и зависит от того, как именно и где именно был построен данный объект. Это обычный, динамический полиморфизм, реализуется через виртуальные методы.

Полиморфизм включения

Этот полиморфизм называют чистым полиморфизмом. Применяя такую форму полиморфизма, родственные объекты можно использовать обобщенно. С помощью замещения и полиморфизма включения можно написать один метод для работы со всеми типами объектов TPerson. Используя полиморфизм включения и замещения можно работать с любым объектом, который проходит тест «is-A». Полиморфизм включения упрощает работу по добавлению к программе новых подтипов, так как не нужно добавлять конкретный метод для каждого нового типа, можно использовать уже существующий, только изменив в нем поведение системы. С помощью полиморфизма можно повторно использовать базовый класс; использовать любого потомка или методы, которые использует базовый класс.

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

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

Параметрические методы

Если полиморфизм включения влияет на наше восприятие объекта, то параметрические полиморфизм влияет на используемые методы, так как можно создавать методы родственных классов, откладывая объявление типов до времени выполнения. Для избежания написания отдельного метода каждого типа применяется параметрический полиморфизм, при этом тип параметров будет являться таким же параметром, как и операнды.

Параметрические типы

Вместо того, чтобы писать класс для каждого конкретного типа следует создать типы, которые будут реализованы во время выполнения программы то есть мы создаем параметрический тип.

Полиморфизм переопределения

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

Полиморфизм-перегрузка

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

Сравнение полиморфизма в функциональном и объектно-ориентированном программировании

Система классов в ФП и в ООП устроены по-разному, поэтому к их сравнению следует подходить очень осторожно.

Полиморфизм является довольно обособленным свойством языка программирования. Например, классы в C++ изначально были реализованы как препроцессор для C. Для Haskell же существует алгоритм трансляции программ, использующих специальный полиморфизм, в программы с исключительно параметрическим полиморфизмом.

Несмотря на концептуальные различия систем классов в ФП и ООП, реализуются они примерно одинаково — с помощью таблиц виртуальных методов.Используется часто в Java.

См. также

Другие книги по запросу «Полиморфизм (программирование)» >>

Приветствую всех! Сегодня поговорим о полиморфизме и рассмотрим ряд примеров и определений. А так же отличия полиморфизма от AdHoc полиморфизма.

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

  • Во время выполнения объекты производного класса могут рассматриваться как объекты базового класса в таких местах как параметры метода и коллекции массивов. При этом объявленный тип объекта больше не идентичен его типу времени выполнения.
  • Базовые классы могут определять и реализовывать виртуальные методы, а производные классы могут переопределять их. Это означает, что они предоставляют свои собственные определение и реализацию. Во время выполнения, когда клиентский код вызывает метод, среда CLR ищет тип времени выполнения объекта и вызывает это переопределение виртуального метода. Таким образом, в исходном коде можно вызвать метод в базовом классе и вызвать выполнение метода с версией производного класса.

Если производный класс наследует от базового класса, то он приобретает все методы, поля, свойства и события базового класса. Проектировщик производного класса может выбирать из следующих возможностей:

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

Производный класс может переопределить член базового класса, если только член базового класса объявлен как виртуальный или абстрактный. Производный член должен использовать ключевое слово override, чтобы явно указать, что метод должен участвовать в виртуальном вызове.

В C# производный класс может включать методы с теми же именами, что и у методов базового класса:

Метод базового класса может быть определен как виртуальный.

  • Если перед методом в производном классе не указано ключевое слово new или override, компилятор выдаст предупреждение, и обработка метода будет производиться как в случае наличия ключевого слова new.
  • Если перед методом в производном классе указано ключевое слово new, то этот метод определен как независимый от метода в базовом классе.
  • Если перед методом в производном классе указано ключевое слово override, то объекты производного класса будут вызывать этот метод вместо метода базового класса.
  • Базовый метод можно вызвать из производного класса с помощью ключевого слова base.
  • Ключевые слова override, virtual и new могут также применяться к свойствам, индексам и событиям.

Содержание

Использование ранее созданных классов в Java

Полями создаваемого класса могут быть не только переменные встроенных типов, но и объекты любых других доступных классов.

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

Рассмотрим пример, в котором класс окружностей создаётся с использованием класса точек (одним из полей класса окружностей является объект-точка):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 import java.util.Scanner; classPoint{   publicdoublex;// абсцисса точки   publicdoubley;// ордината точки   // возвращает строку с описанием точки   publicStringtoString(){     return«(«+x+«;»+y+«)»;   }   // выводит на экран описание точки   publicvoidprint(){     System.out.print(this.toString());   }   // метод перемещает точку на указанный вектор   publicvoidmove(doublea,doubleb){     x=x+a;     y=y+b;   }   // метод изменяет координаты точки на указанные   publicvoidset(doublea,doubleb){     x=a;     y=b;   }   // конструктор по умолчанию, создающий точку с указанными пользователем координатами   publicPoint(){     booleanerr;     do{       err=false;       System.out.print(«Введите абсциссу точки: «);       Scanner scan=newScanner(System.in);       if(scan.hasNextDouble()){         x=scan.nextDouble();       }else{         System.out.println(«Вы ввели не число, попробуйте снова»);         err=true;       }     }while(err);     do{       err=false;       Scanner scan=newScanner(System.in);       System.out.print(«Введите ординату точки: «);       if(scan.hasNextDouble()){         y=scan.nextDouble();       }else{         System.out.println(«Вы ввели не число, попробуйте снова»);         err=true;       }     }while(err);       }   // конструктор, создающий точку с указанными координатами   publicPoint(doublea,doubleb){     x=a;     y=b;   }    // метод вычисляющий расстояние между точками   publicdoublelength(Pointp){     returnMath.sqrt(Math.pow(p.xx,2)+Math.pow(p.yy,2));   }   // метод проверяющий совпадают ли точки   publicbooleanequalsPoint(Pointp){     if(this.x==p.x&&this.y==p.y){       returntrue;     }else{       returnfalse;     }   }  } classCircle{   publicdoubler;// радиус   publicPointc;// центр   // возвращает строку с описанием окружности   publicStringtoString(){     return«Окружность с центром в точке «+c+» и радиусом «+r;   }    // выводит на экран описание окружности   publicvoidprint(){     System.out.print(this.toString());   }     // метод перемещает центр окружности на указанный вектор   publicvoidmove(doublea,doubleb){     c.move(a,b);   }   // метод изменяет окружность, перемещая центр в указанные координаты и меняя радиус   publicvoidset(doublea,doubleb,doublem){     c.set(a,b);     r=m;   }     // метод изменяет окружность, перемещая центр в указанную точку и меняя радиус   publicvoidset(Pointp,doublem){     c.set(p.x,p.y);     r=m;   }    // конструктор по умолчанию, создающий окружность с указанными пользователем параметрами   Circle(){     System.out.println(«Задайте центр окружности:»);     c=newPoint();     booleanerr;     do{       err=false;       Scanner scan=newScanner(System.in);       System.out.print(«Задайте радиус: «);       if(scan.hasNextDouble()){         r=scan.nextDouble();         if(r<=</span>){           System.out.println(«Радиус окружности должен быть положительным»);           err=true;         }       }else{         System.out.println(«Вы ввели не число, попробуйте снова»);         err=true;       }     }while(err);       }   Circle(doublea,doubleb,doublem){     c.set(a,b);     r=m;   }      // метод вычисляющий длину окружности   publicdoublelength(Pointp){     return2*Math.PI*r;   }   // метод проверяющий, совпадают ли две окружности   publicbooleanequalsCircle(Circleo){     if(this.r==o.r&&c.equalsPoint(o.c)){       returntrue;     }else{       returnfalse;     }   }    } publicclassMain{   publicstaticvoidmain(String[]args){     Circle o1=newCircle();     o1.print();   } }

Полиморфизм это один из китов объектно-ориентированного программирования. Язык Java использует полиморфизм повсеместно и не создает программистам преграды для его использования в своем коде. Там есть два типа полиморфных вызовов — интерфейсный вызов и виртуальный вызов. Как и многие другие возможности языков программирования высокого уровня, они имеют свою цену с точки зрения производительности кода. Это значит, что если вы пишете код ориентированный на достижение максимальной производительности, то вам необходимо понимать что это за цена, в каких каких случаях её приходится платить, а в каких нет, где можно себе её позволить, а где нет.

Язык Javа в настоящее время имеет весьма продвинутую реализацию виртуальной машины и очень хороший оптимизирующий компилятор. Сам факт использования интерфейсных или виртуальных вызовов не означает какое-либо замедление работы программы, если фактически используется только одна реализация вызываемого метода. Я наглядно это показал в заметке «Java vs C++ на целых числах». Напомню, что там замерялось время работы такого цикла:

private int runIteration() {          int sum = ;          for (int i = , n = list.size(); i < n; i++)              sum += list.getInt(i);          return sum;      }

Здесь в цикле идут интерфейсные вызовы метода getInt через простой интерфейс IntList, который я сделал для этих заметок. А из заметки «Смотрим на ассемблерный код работающего Java приложения» видно, что этот вызов в данном случае вообще ничего не стоит — его просто нет в результирующем машинном коде. Все измерения я производил каждый раз запуская тестовую программу так, чтобы замерялось время работы одной конкретной реализации, и HotSpot компилятор не только встроил реализацию метода getInt прямо в тело цикла, но и развернул цикл.

Однако, я предусмотрел возможность и запуска нескольких реализаций (передавая список их имен в командной строке), которой сейчас и воспользуюсь. При этом в каждом проходе тестирования последовательно замеряется время работы каждой из реализаций. Я возьму самую быструю реализацию интерфейса IntList через int[] и буду замерять время работы метода runIteration в расчете на одну итерацию как отдельно, так и при наличии еще одной, двух или трех других реализаций. Для этих целей я взял ViaByteBuffer1 и ViaByteBuffer2 из этой заметки и ViaByteBuffer3 отсюда. Получаются вот такие результаты (в наносекундах затрачиваемых в среднем на одну итерацию цикла для реализации через int[]):

Размер 1 2 3 4
1 000 0.46 3.44 8.93 8.90
10 000 0.48 3.34 8.76 8.80
100 000 0.50 3.42 9.00 8.89
1 000 000 0.74 3.46 9.04 8.99
10 000 000 0.78 3.42 8.98 9.04

При двух различных реализациях метода getInt внутри JVM скорость выполнения снижается в несколько раз, при трех реализациях снижается еще более чем в два раза, но при четырех дальнейшего замедления нет.

Дело в том, что в дополнение к случаю мономорфного вызова, когда реализация всего одна и её можно встроить прямо в точку вызова, HotSpot распознает специально случай биморфного вызова, когда есть две фактических реализации, и генерирует код для быстрой проверки класса объекта (в нашем случае переменной list) и условного перехода к нужной реализации. Три и более реализаций считаются HotSpot-ом полиморфным вызовом для которого создается код полноценного интерфейсного или виртуального вызова, который, как мы видим на примере выше, относительно дорого обходится.

Полная картина, на самом деле, несколько сложней, ибо HotSpot учитывает не только количество загруженных в JVM классов, которые реализуют тот или иной метод, но и собирает динамический профиль исполнения для анализа того, какие реализации фактически используются в той или иной точке кода. Если загрузить, сконструировать и даже заполнить данными все четыре реализации IntList, но прогонять тесты только для первой, то результаты будут такие же, как и в случае единственной загруженной реализации.

Более того, если переделать IntList из интерфейса в абстрактный класс и провести такой же тест, то будет видно, что скорость работы мономорфного и биморфного вызова не изменится, а вот полиморфный виртуальный вызов будет работать несколько быстрей чем полиморфный интерфейсный вызов (в общем случае, полиморфный интерфейсный вызов будет работать тем медленней, чем больше интерфейсов реализует данный класс):

Размер 1 2 3 4
1 000 0.47 3.24 7.52 7.54
10 000 0.49 3.30 7.59 7.58
100 000 0.51 3.30 7.54 7.60
1 000 000 0.76 3.40 7.80 7.77
10 000 000 0.78 3.39 7.71 7.66

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

В мире программирования полиморфизм относится к способности функции с одним и тем же именем выполнять разные функции. Он создает структуру, которая может использовать множество форм объектов.

Это позволяет функциям или аргументам использовать сущности разных типов в разное время.

В объектно-ориентированном программировании полиморфизм позволяет использовать конкретный объект, ссылающийся на конкретный класс, таким же образом, как если бы это был другой объект, ссылающийся на совершенно другой класс.

image

Реализация полиморфизма в Python с помощью класса

Python может использовать разные типы классов таким же образом, используя полиморфизм. Для этой цели можно создать цикл, который выполняет итерацию по кортежу объектов. Опубликовать который, можно вызывать методы, не глядя на тип класса, к которому принадлежит объект.

Пример с классами и объектами

class Rabbit():       def age(self):           print("This function determines the age of Rabbit.")           def color(self):           print("This function determines the color of Rabbit.")       class Horse():       def age(self):           print("This function determines the age of Horse.")           def color(self):           print("This function determines the color of Horse.")       obj1 = Rabbit()   obj2 = Horse()   for type in (obj1, obj2): # creating a loop to iterate through the obj1, obj2      type.age()       type.color()         

Вывод:

Эта функция определяет возраст Кролика. Эта функция определяет цвет Кролика. Эта функция определяет возраст Лошади. Эта функция определяет цвет Лошади.

Реализация с наследованием

Мы будем определять функции в производном классе, который имеет то же имя, что и функции в базовом классе. Здесь мы повторно реализуем функции в производном классе. Явление повторной реализации функции в производном классе известно как переопределение метода.

Пример

class Animal:     def type(self):       print("Various types of animals")             def age(self):       print("Age of the animal.")         class Rabbit(Animal):     def age(self):       print("Age of rabbit.")           class Horse(Animal):     def age(self):       print("Age of horse.")           obj_animal = Animal()   obj_rabbit = Rabbit()   obj_horse = Horse()       obj_animal.type()   obj_animal.age()       obj_rabbit.type()   obj_rabbit.age()       obj_horse.type()   obj_horse.age()   

Вывод:

Различные виды животных Возраст животного. Различные виды животных Возраст кролика. Различные виды животных Возраст лошади.

Полиморфизм времени компиляции или перегрузка метода?

В отличие от многих других популярных объектно-ориентированных языков программирования, таких как Java, Python не поддерживает полиморфизм во время компиляции или перегрузку методов. Если в классе или скрипте Python есть несколько методов с одинаковым именем, метод, определенный в последнем, переопределит предыдущий.

Python не использует аргументы функции для подписи метода, поэтому перегрузка метода не поддерживается.

Перегрузка оператора в Python

Python поддерживает перегрузку операторов. Это еще один тип полиморфизма, при котором оператор ведет себя по-разному в зависимости от типа операндов.

  • + оператор складывает два числа и объединяет две строки
  • Оператор * умножает два числа и при использовании со строкой и int повторяет строку, указанную в int, раз и объединяет их.

Преимущества

  • Коды и классы, написанные один раз, можно повторно использовать и реализовывать несколько раз.
  • Это помогает уменьшить взаимосвязь между различными функциями и поведением объектов.

Оцените статью
Рейтинг автора
5
Материал подготовил
Илья Коршунов
Наш эксперт
Написано статей
134
А как считаете Вы?
Напишите в комментариях, что вы думаете – согласны
ли со статьей или есть что добавить?
Добавить комментарий