Перейти к содержимому

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

  • автор:

Полиморфизм: подавать холодным

Полиморфизм («истинный», параметрический) — свойство, позволяющее обрабатывать данные разных типов одним образом.

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

Реализуем функции print_static , print_dynamic и print_enum для демонстрации различных методов реализации полиморфизма.

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

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

Статическая диспетчеризация

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

Rust

В Rust статическая диспетчеризация реализуется с помощью типовых параметров или ключевого слова impl (по сути, являющего собой анонимные типовые параметры). Напишем функцию print_static :

fn print_static(to: T) < println!(""); > print_static(123); print_static("abc")

Типовый параметр объявляется внутри <> , названием его служит T , а Display — трейт, позволяющий форматировать значения. В Rust вы не сможете отформатировать значение с помощью <> , пока не укажете, что типовый параметр реализует Display .

C++

В C++ статическая диспетчеризация реализуется с помощью шаблонов, декларируемых с помощью ключевого слова template . Функция:

template auto print_static(T to) -> void < std::cout print_static(123); print_static("abc");

Типовый параметр T объявлен с помощью ключевого слова typename , обозначающего произвольный тип. В C++ вам не нужно ограничивать область типов для функции, ошибку вы получите только в случае передачи аргумента типа, который нельзя записать в std::ostream . Стоит отметить, что в данном случае можно было обойтись без шаблона, вместо типового параметра T использовав auto .

Go

Go не поддерживает статический полиморфизм. В этом языке есть концепт, называемый типовыми параметрами, но на деле ими не являющийся, который я упомяну позже.

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

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

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

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

Rust

В Rust динамическая диспетчеризация реализуется с помощью ключевого слова dyn . Динамические типы считаются безразмерными, потому их нужно передавать посредством указателя, ссылки или умного указателя (всё далее — указатель). Указатели на динамические типы являются толстыми, на деле представляющими из себя два указателя - на значение и на динамическую таблицу.

fn print_dynamic(to: &dyn Display) < println!(""); > print_dynamic(&123); print_dynamic(&"abc");

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

C++

В C++ не существует идентичного dyn Display механизма, однако сам этот язык гораздо более приспособлен к использованию динамической диспетчеризации, так как является объектно‑ориентированным, потому мы реализуем такой механизм с помощью наследования и ключевых слов virtual и override :

class Display < public: virtual auto writeln(std::ostream& to) ->void <>; >; class DisplayInt: public Display < public: int self; DisplayInt(int val) < self = val; >; auto writeln(std::ostream& to) -> void override < to self ; >; class DisplayStr: public Display < public: char* self; DisplayStr(char* val) < self = val; >; auto writeln(std::ostream& to) -> void override < to self ; >; auto display(int val) < return DisplayInt(val); >auto display(char* val) < return DisplayStr(val); >auto print_dynamic(Display&& to) -> void < to.writeln(std::cout); >print_dynamic(display(123)); print_dynamic(display("abc"));

В данном случае мы реализуем метод writeln для наследников базового класса Display . Переопределяемая display сугубо для схожести инициализации.

Go

В Go за динамическую диспетчеризацию отвечает механизм интерфейсов, их и используем:

func print_dynamic(to any) < fmt.Println(to) >print_dynamic(123); print_dynamic("abc");

В данном случае Go не требует наложения дополнительных ограничений на принимаемый тип, потому мы можем использовать интерфейс any , являющийся менее строгим аналогом any в C++ и dyn Any в Rust.

"Энамная" диспетчеризация

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

Rust

В Rust нет встроенного механизма для реализации энамной диспетчеризации, однако существует замечательный крейт, предоставляющий подобный механизм — enum_dispatch. Пользоваться сторонними библиотеками мы, однако, конечно же не будем, потому реализуем тип Displayable сами:

enum Displayable < Str(&'static str), I32(i32), >impl From for Displayable < fn from(value: i32) ->Self < Self::I32(value) >> impl From for Displayable < fn from(value: &'static str) ->Self < Self::Str(value) >> impl Display for Displayable < fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result < match self < Self::Str(s) =>write!(f, ""), Self::I32(i) => write!(f, "") > > > fn print_enum(to: Displayable) < println!(""); > print_enum(123.into()); print_enum("abc".into());

На строках 1-4 реализуется сам тип, на строках 6-16 — конверсия в него (используется на строках 31 и 32 путём вызова метода into ), на строках 18-25 — возможность форматирования типа. В функции мы без проблем принимаем готовую инстанцию.

C++

Как и Rust, C++ не предоставляет встроенной энамной диспетчеризации, потому реализуем сами:

struct Displayable < enum < STR, I32 >tag; union < char* str; int i32; >; Displayable(int val) < tag = I32; i32 = val; >; Displayable(char* val) < tag = STR; str = val; >; auto writeln(std::ostream& to) -> void < switch (this->tag) < case STR: to str << std::endl; break; case I32: to i32 > >; auto print_enum(Displayable to) -> void < to.writeln(std::cout); >print_enum(Displayable(123)); print_enum(Displayable("abc"));

И так, на строках 1-9 мы описываем сам тип, на 11-19 — конверсию, а на 21-30 — метод writeln , идентичный методу fmt в версии на Rust. В функции print_enum мы также принимаем готовую инстанцию. Стоит отметить, что похожего эффекта можно добиться при помощи std::variant .

Go

В Go существует встроенный механизм реализации энамной диспетчеризации — т. н. словари, которые часто неверно называют дженериками даже сами разработчики языка (не делайте так, пожалуйста). Поглядим на них в деле:

type Displayable interface < int | string >func print_enum[T Displayable](to T) < fmt.Println(to) >print_enum(123); print_enum("abc");

Словарь требует интерфейса‑ограничителя, возможные типы для нашего интерфейса Displayable указаны на строке 2. В качестве ограничителя также может выступать интерфейс any , однако в данном случае я избежал его использования для более точного раскрытия этого механизма в сравнении с предыдущими вариантами и возможной оптимизации.

Стоит упомянуть - C

В стандарте C11 в систему препроцессорных констант ( #define ) была встроена директива _Generic , позволяющая вручную осуществлять статическую диспетчеризацию.

#define print_static(VAL) _Generic((VAL), int: print_int, default: print_str)(VAL) print_int(int val) < printf("%d\n", val); >print_str(char* val) < puts(val); >print_static(123); print_static("abc");

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

Заключения

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

Energy
education

C#

Объектно-ориентированный язык программирования. Разработан в 1998—2001 годах группой инженеров под руководством Андерса Хейлсберга в компании Microsoft как язык разработки приложений для платформы Microsoft .NET Framework и впоследствии был стандартизирован как ECMA-334 и ISO/IEC 23270.

19. Полиморфизм

Слово полиморфизм означает иметь много форм. В объектно-ориентированном программировании полиморфизмом часто понимается как "один интерфейс, множество функций".

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

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

Механизм связывания функции с объектом во время компиляции называется ранним связыванием. Он также называется статическое связывание. C# использует два механизма для реализации статического полиморфизма. Это:

  • Перегрузка метода
  • Перегрузка оператора
Перегрузка метода

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

Ниже приведен пример, в котором те же методы print() используются для печати различных типов данных:

 using System; namespace PolymorphismApplication < class Printdata < void print(int i) < Console.WriteLine("Вывод int: ", i ); > void print(double f) < Console.WriteLine("Вывод float: " , f); > void print(string s) < Console.WriteLine("Вывод string: ", s); > static void Main(string[] args) < Printdata p = new Printdata(); // вызов метода print для вывода integer p.print(5); // вызов метода print для вывода float p.print(500.263); // вызов метода print для вывода string p.print("Привет C#"); Console.ReadKey(); >> > 

Если приведенный выше код скомпилировать и выполнить, это приведет к следующему результату:

 Вывод int: 5 Вывод float: 500.263 Вывод string: Привет C# 
Динамический полиморфизм

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

Обратите внимание на следующие правила об абстрактных классах:

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

Следующая программа демонстрирует абстрактный класс:

 using System; namespace PolymorphismApplication < abstract class Shape < public abstract int area(); >class Rectangle: Shape < private int length; private int width; public Rectangle( int a=0, int b=0) < length = a; width = b; >public override int area () < Console.WriteLine("Площадь прямоугольника :"); return (width * length); >> class RectangleTester < static void Main(string[] args) < Rectangle r = new Rectangle(10, 7); double a = r.area(); Console.WriteLine("Площадь: ",a); Console.ReadKey(); > > > 

Если приведенный выше код скомпилировать и выполнить, это приведет к следующему результату:

 Площадь прямоугольника : Площадь: 70 

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

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

Следующая программа демонстрирует это:

 using System; namespace PolymorphismApplication < class Shape < protected int width, height; public Shape( int a=0, int b=0) < width = a; height = b; >public virtual int area() < Console.WriteLine("Площадь родительского класса :"); return 0; >> class Rectangle: Shape < public Rectangle( int a=0, int b=0): base(a, b) < >public override int area () < Console.WriteLine("Площадь класса Rectangle :"); return (width * height); >> class Triangle: Shape < public Triangle(int a = 0, int b = 0): base(a, b) < >public override int area() < Console.WriteLine("Площадь класса Triangle :"); return (width * height / 2); >> class Caller < public void CallArea(Shape sh) < int a; a = sh.area(); Console.WriteLine("Площадь: ", a); > > class Tester < static void Main(string[] args) < Caller c = new Caller(); Rectangle r = new Rectangle(10, 7); Triangle t = new Triangle(10, 5); c.CallArea(r); c.CallArea(t); Console.ReadKey(); >> > 

Если приведенный выше код скомпилировать и выполнить, это приведет к следующему результату:

 Площадь класса Rectangle: Площадь: 70 Площадь класса Triangle: Площадь: 25 

Администратор сайта: Колосов Михаил
email:
Copyright © 2011-2024. All rights reserved.

Полиморфизм и его друзья

Java-университет

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

Полиморфизм и его друзья - 1

Вступление

Думаю, все мы знаем, что язык программирования Java принадлежит компании Oracle. Поэтому, наш путь начинается с сайта: www.oracle.com. На главной странице есть "Menu". В нём в разделе "Documentation" есть подраздел "Java". Всё, что относится к базовым функциям языка относится к "Java SE documentation", поэтому выбираем этот раздел. Раздел документации откроется для последней версии, но пока что в "Looking for a different release?" выберем вариант: JDK8. На странице мы увидим много различных вариантов. Но нас интересует Learn the Language : "Java Tutorials Learning Paths". На этой странице мы найдём ещё один раздел: "Learning the Java Language". Это - святая из святых, tutorial по основам Java от Oracle. Java — объектно-ориентированный язык программирования (ООП), поэтому изучение языка даже на сайте Oracle начинается с обсуждения основных концепций "Object-Oriented Programming Concepts". Из самого названия понятно, что Java ориентирован на работу с объектами. Из подраздела "What Is an Object?" понятно, что объекты в Java состоят из состояния и поведения. Представьте, что у нас есть счёт в банке. Количество денег на счету - это состояние, а методы работы с этим состоянием - это поведение. Объекты надо как-то описывать (рассказывать, какое у них может быть состояние и поведение) и этим описанием является класс. Когда мы создаём объект какого-то класса, то мы указываем этот класс и это называется "типом объекта". Отсюда и говорится, что Java является строго типизированным языком, о чём сказано в спецификации яызка Java в разделе "Chapter 4. Types, Values, and Variables". Язык Java следует концепциям ООП и поддерживает наследование (Inheritance), используя ключевое слово extends (т.е. расширение типа). Почему расширение? Потому что при наследовании дочерний класс наследует поведение и состояние родительского класса и может их дополнить, т.е. расширить функциональность базового класса. Так же в описании класса может быть указан интерфейс (Interface) при помощи ключевого слова implements. Когда класс реализует интерфейс, это значит, что класс соответствует некоторому контракту - декларации программиста остальному окружению, что класс имеет определённое поведение. Например, у плеера есть различные кнопки. Эти кнопки - интерфейс для управления поведением плеера, а поведение будет изменять внутреннее состояние плеера (например, громкость). При этом состояние и поведение как описание дадут класс. Если класс реализует интерфейс, то объект созданный по этому классу может быть описан типом не только по классу, но и по интерфейсу. Давайте уже посмотрим на пример:

 public class MusicPlayer < public static interface Device < public void turnOn(); public void turnOff(); >public static class Mp3Player implements Device < public void turnOn() < System.out.println("On. Ready for mp3."); >public void turnOff() < System.out.println("Off"); >> public static class Mp4Player extends Mp3Player < @Override public void turnOn() < System.out.println("On. Ready for mp3/mp4."); >> public static void main(String []args) throws Exception < // Какое-то устройство (Тип = Device) Device mp3Player = new Mp3Player(); mp3Player.turnOn(); // У нас есть mp4 проигрыватель, но нам от него нужно только mp3 // Пользуемся им как mp3 проигрывателем (Тип = Mp3Player) Mp3Player mp4Player = new Mp4Player(); mp4Player.turnOn(); >> 

Тип — это очень важное описание. Оно рассказывает, как мы собираемся работать с объектом, т.е. какое поведение от объекта ожидаем. Поведение - это методы. Поэтому, давайте разбираться с методами. На сайте Oracle методам отведён свой раздел в Oracle Tutorial : "Defining Methods". Первое, что стоит вынести из статьи: Сигнатура метода — это название метода и типы параметров:

Полиморфизм и его друзья - 2

Например, объявляя метод public void method(Object o), сигнатурой будет название method и тип параметра Object. Тип возвращаемого значения НЕ входит в сигнатуру. Это важно! Далее выполним компиляцию нашего исходного кода. Как мы знаем, для этого код надо сохранить в файл с именем класса и с расширением java. Код на языке Java компилируется при помощи компилятора "javac" в некоторый промежуточный формат, который умеет выполнять виртуальная машина Java (JVM). Этот промежуточный формат называется байткодом и содержится в файлах с расширением .class. Выполним команду для компиляции: javac MusicPlayer.java После того, как java код скомпилирован, мы можем его выполнять. Используя утилиту "java" для запуска будет запущен процесс виртуальный машины java для выполнения переданного в class файле байткода. Выполним команду для запуска приложения: java MusicPlayer . Мы увидим на экране текст, указанный во входном параметре метода println. Интересно, что имея байткод в файле с расширением .class мы можем его посмотреть при помощи утилиты "javap". Выполним команду javap -c MusicPlayer:

Полиморфизм и его друзья - 3

Из байткода мы можем увидеть, что вызов метода через объект, типом которого был указан класс выполняется при помощи invokevirtual , а компилятор вычислил, какую сигнатуру метода надо использовать. Почему invokevirtual ? Потому что идёт вызов(invoke переводится как вызывать) виртуального метода. Что такое виртуальный метод? Это такой метод, тело которого может быть переопределено в момент выполнения программы. Представьте просто, что у вас есть некий список соответствий некоторого ключа (сигнатуры метода) и тела (кода) метода. И это соответствие ключа и тела метода во время выполнения программы может меняться. Поэтому метод виртуальный. По умолчанию в Java методы, которые НЕ static, НЕ final и НЕ private, являются виртуальными. Благодаря этому Java поддерживает такой принцип объектно-ориентированного программирования как полиморфизм. Как Вы уже могли понять, об этом наш сегодняшний обзор.

Полиморфизм

На сайте Oracle в их официальном Tutorial есть отдельный раздел: "Polymorphism". Воспользуемся Java Online Compiler'ом чтобы увидеть, как работает полиморфизм в Java. Например, у нас есть некоторый абстрактный класс Number, представляющий число в Java. Что он позволяет? У него есть некоторые базовые методы, которые будут у всех наследников. Тот кто наследуется от Number буквально говорит - "Я число, со мной можно работать как с числом". Например, для любого наследника можно при помощи метода intValue() получить его Integer значение. Если посмотреть java api для Number, то видно, что метод abstract, то есть данный метод каждый наследник Number должен реализовать сам. Но что нам это даёт? Посмотрим на пример:

 public class HelloWorld < public static int summ(Number first, Number second) < return first.intValue() + second.intValue(); >public static void main(String []args) < System.out.println(summ(1, 2)); System.out.println(summ(1L, 4L)); System.out.println(summ(1L, 5)); System.out.println(summ(1.0, 3)); >> 

Как видно из примера, благодаря полиморфизму, мы можем написать метод, который на вход будет принимать аргументы любого типа, который будет наследником Number (Number мы не можем получить, т.к. это абстрактный класс). Как было в примере с плеером, в данном случае мы говорим, что хотим работать с чем-то, как с Number. Мы знаем, что любой, кто является Number, обязан уметь предоставить своё integer значение. И нам этого достаточно. Мы не хотим вдаваться в подробности реализации конкретного объекта и хотим работать с этим объектом через общие для всех наследников Number методы. Список методов, которые нам будут доступны, будет определён по типу во время компиляции (как это мы видели ранее в байткоде). В данном случае у нас тип будет Number. Как видно из примера, мы передаём различные числа разного типа, то есть на вход метод summ будет получать и Integer, и Long, и Double. Но всех их объединяет то, что они наследники от абстрактного Number, а следовательно переопределили у себя поведение в методе intValue, т.к. каждый конкретный тип знает, как этот тип нужно приводить к Integer. Такой полиморфизм реализован через так называемое переопределение, по английски Overriding.

Полиморфизм и его друзья - 4

Переопределение (Overriding) или динамический полиморфизм. Итак, начнём с того, что сохраним файл HelloWorld.java со следующим содержанием:

 public class HelloWorld < public static class Parent < public void method() < System.out.println("Parent"); >> public static class Child extends Parent < public void method() < System.out.println("Child"); >> public static void main(String[] args) < Parent parent = new Parent(); Parent child = new Child(); parent.method(); child.method(); >> 

Выполним javac HelloWorld.java и javap -c HelloWorld :

Полиморфизм и его друзья - 5

Как видно, в байткоде для строчек с вызовом метода указана одинаковая ссылка на метод для вызова invokevirtual (#6) . Выполним java HelloWorld . Как мы видим, переменные parent и child объявлены c типом Parent, однако сама реализация вызвана согласно тому, какой объект был присвоен переменной (т.е. объект какого типа). Во время выполнения программы (ещё говорят в рантайме) JVM в зависимости от объекта при вызове методов по одной и той же сигнатуре выполняла разные методы. То есть по ключу соответствующей сигнатуры сначала получили одно тело метода, а потом получили другое. В зависимости от того, какой объект лежит в переменной. Такое вот определение в момент выполнения программы того, какой метод будет вызван, называется ещё поздним связыванием или Dynamic Binding. То есть соответствие сигнатуры и тела метода выполняется динамически, в зависимости от объекта, для которого вызывается метод. Естественно, нельзя переопределить статические члены класса (Class member), а так же члены класса с типом доступа private или final. На помощь разработчикам так же приходят аннотации @Override. Она помогает компилятору понять, что в этом месте мы собираемся переопределить поведение метод предка. Если мы ошиблись в сигнатуре метода, то компилятор нам сразу об этом скажет. Например:

 public static class Parent < public void method() < System.out.println("parent"); >> public static class Child extends Parent < @Override public void method(String text) < System.out.println("child"); >> 

Не скомпилируется с ошибкой: error: method does not override or implement a method from a supertype

Полиморфизм и его друзья - 6

С переопределением так же связано такое понятие, как "ковариантность" (Covariance). Рассмотрим пример:

 public class HelloWorld < public static class Parent < public Number method() < return 1; >> public static class Child extends Parent < @Override public Integer method() < return 2; >> public static void main(String[] args) < System.out.println(new Child().method()); >> 

Несмотря на внешнюю заумность смысл сводится к тому, что при переопределении мы можем вернуть не только тот тип, который был указан в предке, но и более конкретный тип. Например, предок возвращал Number, а мы можем вернуть Integer - наследника от Number. Тоже касается и исключений, объявленных в throws у метода. Наследники могут переопределить метод и уточнить бросаемое исключение. Но не могут расширить. То есть если родитель бросает IOException, то мы можем бросать более точное EOFException, но не можем бросать Exception. Аналогично, нельзя сужать область видимости и нельзя накладывать дополнительные ограничения. Например, нельзя добавлять static.

Полиморфизм и его друзья - 7

Сокрытие (Hiding)

Есть ещё такое понятие, как "сокрытие". Пример:

 public class HelloWorld < public static class Parent < public static void method() < System.out.println("Parent"); >> public static class Child extends Parent < public static void method() < System.out.println("Child"); >> public static void main(String[] args) < Parent parent = new Parent(); Parent child = new Child(); parent.method(); child.method(); >> 

Это довольно очевидная вещь, если подумать. Статические члены класса относятся к классу, т.е. к типу переменной. Поэтому, логично, что если child имеет тип Parent, то и метод будет вызван у Parent, а не у child. Если мы посмотрим байткод, как мы уже делали ранее, то увидим, что вызов статического метода осуществляется при помощи invokestatic. Это объясняет JVM, что надо смотреть на тип, а не по таблице методов, как это делал invokevirtual или invokeinterface.

Полиморфизм и его друзья - 8

Перегрузка методов (Overloading)

Что мы видим ещё видим в Java Oracle Tutorial? В ранее изученном разделе "Defining Methods" есть что-то про Overloading. Что это такое? По-русски это "перегрузка методов", а такие методы называются "перегруженными". Итак, перегрузка методов. На первый взгляд, всё просто. Откроем онлайн компилятор Java, например tutorialspoint online java compiler.

 public class HelloWorld < public static void main(String []args)< HelloWorld hw = new HelloWorld(); hw.say(1); hw.say("1"); >public static void say(Integer number) < System.out.println("Integer " + number); >public static void say(String number) < System.out.println("String " + number); >> 

Итак, тут всё кажется просто. Как и сказано в tutorial от Oracle, перегруженные методы (в данном случае это метод say) отличаются по количеству и типу аргументов, переданных в метод. Нельзя объявить одинаковые имя и одинаковое количество одинаковых типов аргументов, т.к. компилятор не сможет их отличить друг от друга. Тут стоит сразу отметить очень важную вещь:

Полиморфизм и его друзья - 9

То есть при перегрузке компилятор проверяет корректность. Это важно. Но как же на самом деле компилятор определяет, что нужно вызывать определённый метод? Он использует правило "the Most Specific Method", описанного в спецификации языка Java : "15.12.2.5. Choosing the Most Specific Method". Чтобы продемонстрировать его работу, возьмём пример из Oracle Certified Professional Java Programmer:

 public class Overload < public void method(Object o) < System.out.println("Object"); >public void method(java.io.FileNotFoundException f) < System.out.println("FileNotFoundException"); >public void method(java.io.IOException i) < System.out.println("IOException"); >public static void main(String args[]) < Overload test = new Overload(); test.method(null); >> 

Пример взять отсюда: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j. Как видно, мы передаём в метод null. Компилятор пытается определить наиболее специфичный тип. Object не подходит, т.к. от него наследуются все. Идём дальше. Есть 2 класса исключений. Посмотрим на java.io.IOException и увидим, что в "Direct Known Subclasses" есть FileNotFoundException. То есть выходит, что FileNotFoundException самый специфичный тип. Поэтому, результатом будет вывод строки "FileNotFoundException". А вот если заменить IOException на EOFException, то получится, что у нас два метода находятся на одном уровне иерархии по дереву типов, то есть для них обоих IOException является родителем. Компилятор не сможет выбрать, какой метод нужно будет вызывать и выдаст ошибку компиляции: reference to method is ambiguous . Ещё один пример:

 public class Overload < public static void method(int. array) < System.out.println("1"); >public static void main(String args[]) < method(1, 2); >> 

Выведет 1. Тут вопросов нет. Тип int. является vararg https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html и на самом деле является не более чем "синтаксическим сахаром" и на самом деле int. array можно читать как int[] array. Если мы теперь добавим метод:

 public static void method(long a, long b)

То станет выводится не 1, а 2, т.к. мы передаём 2 числа, и 2 аргумента более точное совпадение, чем один массив. Если мы добавим метод:

 public static void method(Integer a, Integer b)

То мы по прежнему будем видеть 2. Потому что в данном случае примитивы более точное совпадение, чем боксинг в Integer. Однако, если мы выполним method(new Integer(1), new Integer(2)); то будет выведено 3. Конструкторы в Java похожи на методы, а так как по ним тоже можно получить сигнатуру, то для них действуют те же правила "overloading resolution", что и перегруженные методы. Спецификация языка Java нам так и сообщает в "8.8.8. Constructor Overloading". Перегруз методов = Раннее связывание (оно же Static Binding) Часто можно услышать про раннее и позднее связывание, он же Static Binding или Dynamic Binding. Различие в них очень простое. Рано - это компиляция, поздно - это момент выполнения программы. Поэтому, раннее связывание (static binding) - определение того, какой метод у кого будет вызван в момент компиляции. Ну а позднее связывание (dynamic binding) - определение того, какой метод вызывать, непосредственно в момент выполнения программы. Как мы видели раньше (когда меняли IOException на EOFException), если мы перегрузим методы так, что компилятор не сможет понять, где какой вызов выполнять, то мы получим ошибку во время компиляции: reference to method is ambiguous. Слово ambiguous в переводе с английского - двусмысленный или неопределённый, неточный. Получается, что перегрузка - это раннее связывание, т.к. проверка выполняется в момент компиляции. Чтобы подтвердить свои умозаключения откроем Java Language Specification на главе "8.4.9. Overloading" :

Полиморфизм и его друзья - 10

Получается, во время компиляции будет использована информация о типах и количестве аргументах (которая доступна на момент компиляции), чтобы определить сигнатуру метода. Если метод относится к методам объекта (т.е. instance method), реальный вызов метода будет определён в runtime, используя dynamic method lookup (то есть динамическое связывание). Чтобы стало понятнее, возьмём пример, который похож на ранее рассмотренный:

 public class HelloWorld < public void method(int intNumber) < System.out.println("intNumber"); >public void method(Integer intNumber) < System.out.println("Integer"); >public void method(String intNumber) < System.out.println("Number is: " + intNumber); >public static void main(String args[]) < HelloWorld test = new HelloWorld(); test.method(2); >> 

Сохраним этот код в файл HelloWorld.java и скомпилируем его при помощи javac HelloWorld.java Теперь посмотрим, что там написал компилятор наш в байткоде, выполнив команду: javap -verbose HelloWorld .

Полиморфизм и его друзья - 11

Как указано, компилятор определил, что в будущем будет вызван некоторый виртуальный метод. То есть тело метода будет определено в runtime. Но на момент компиляции из всех трёх методов компилятор выбрал самый подходящий, поэтому указал номер: "invokevirtual #13"

Полиморфизм и его друзья - 12

А что это за methodref такой? Это ссылка на метод. Грубо говоря, это некоторы ключ, по которому во время выполнения виртуальная Java машина сможет действительно определить, какой метод нужно искать для выполнения. Подробнее можно ознакомиться в супер статье: "How Does JVM Handle Method Overloading And Overriding Internally".

Подведение итогов

  • How Does JVM Handle Method Overloading And Overriding Internally
  • Introduction to Compiler Construction in a Java World
  • Различия между ранним и поздним связыванием в Java
  • Static vs Dynamic Binding in Java
  • OCP question : Overload
  • Java Bytecode Fundamentals: Using Objects and Calling Methods

C # - Полиморфизм

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

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

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

Механизм связывания функции с объектом во время компиляции называется ранним связыванием. Он также называется статической привязкой. C # предоставляет два метода для реализации статического полиморфизма. Они -

  • Перегрузка функций
  • Перегрузка оператора

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

Перегрузка функции

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

В следующем примере показано использование функции print () для печати различных типов данных:

using System; namespace PolymorphismApplication < class Printdata < void print(int i) < Console.WriteLine("Printing int: ", i ); > void print(double f) < Console.WriteLine("Printing float: " , f); > void print(string s) < Console.WriteLine("Printing string: ", s); > static void Main(string[] args) < Printdata p = new Printdata(); // Call print to print integer p.print(5); // Call print to print float p.print(500.263); // Call print to print string p.print("Hello C++"); Console.ReadKey(); >> >

Когда приведенный выше код компилируется и выполняется, он производит следующий результат:

Printing int: 5 Printing float: 500.263 Printing string: Hello C++

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

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

Вот правила об абстрактных классах:

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

Следующая программа демонстрирует абстрактный класс:

using System; namespace PolymorphismApplication < abstract class Shape < public abstract int area(); >class Rectangle: Shape < private int length; private int width; public Rectangle( int a = 0, int b = 0) < length = a; width = b; >public override int area () < Console.WriteLine("Rectangle class area :"); return (width * length); >> class RectangleTester < static void Main(string[] args) < Rectangle r = new Rectangle(10, 7); double a = r.area(); Console.WriteLine("Area: ",a); Console.ReadKey(); > > >

Когда приведенный выше код компилируется и выполняется, он производит следующий результат:

Rectangle class area : Area: 70

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

Следующая программа демонстрирует это:

using System; namespace PolymorphismApplication < class Shape < protected int width, height; public Shape( int a = 0, int b = 0) < width = a; height = b; >public virtual int area() < Console.WriteLine("Parent class area :"); return 0; >> class Rectangle: Shape < public Rectangle( int a = 0, int b = 0): base(a, b) < >public override int area () < Console.WriteLine("Rectangle class area :"); return (width * height); >> class Triangle: Shape < public Triangle(int a = 0, int b = 0): base(a, b) < >public override int area() < Console.WriteLine("Triangle class area :"); return (width * height / 2); >> class Caller < public void CallArea(Shape sh) < int a; a = sh.area(); Console.WriteLine("Area: ", a); > > class Tester < static void Main(string[] args) < Caller c = new Caller(); Rectangle r = new Rectangle(10, 7); Triangle t = new Triangle(10, 5); c.CallArea(r); c.CallArea(t); Console.ReadKey(); >> >

Когда приведенный выше код компилируется и выполняется, он производит следующий результат:

Rectangle class area: Area: 70 Triangle class area: Area: 25

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

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