25. Java – Полиморфизм
Полиморфизм – способность объекта принимать множество различных форм. Наиболее распространенное использование полиморфизма в ООП происходит, когда ссылка на родительский класс используется для ссылки на объект дочернего класса. Постараемся разобраться с понятием полиморфизма в Java простыми словами, так сказать для чайников.
Любой объект в Java, который может пройти более одного теста IS-A считается полиморфным. В Java все объекты полиморфны, так как любой объект пройдёт тест IS-A для своего собственного типа и для класса Object.
Важно знать, что получить доступ к объекту можно только через ссылочную переменную. Ссылочная переменная может быть только одного типа. Будучи объявленной, тип ссылочной переменной изменить нельзя.
Ссылочную переменную можно переназначить к другим объектам, которые не объявлены как final. Тип ссылочной переменной определяет методы, которые она может вызвать на объекте.
Ссылочная переменная может обратиться к любому объекту своего объявленного типа или любому подтипу своего объявленного типа. Ссылочную переменную можно объявить как класс или тип интерфейса.
Пример 1
Рассмотрим пример наследования полиморфизм в Java.
public interface Vegetarian<> public class Animal<> public class Deer extends Animal implements Vegetarian<>
Теперь класс Deer (Олень) считается полиморфным, так как он имеет множественное наследование. Следующие утверждения верны для примера выше:
- A Deer IS-A Animal (олень — это животное);
- A Deer IS-A Vegetarian (олень — это вегетарианец);
- A Deer IS-A Deer (олень — это олень);
- A Deer IS-A Object (олень — это объект).
Когда мы применяем факты ссылочной переменной к ссылке на объект Deer (Олень), следующие утверждения верны:
Пример 2
Deer d = new Deer(); Animal a = d; Vegetarian v = d; Object o = d;
Все переменные (d, a, v, o) ссылаются к тому же объекту Deer (Олень).
Виртуальные методы
В этом разделе рассмотрим, как поведение переопределённых методов в Java позволяет воспользоваться преимуществами полиморфизма при оформлении классов.
Мы уже рассмотрели переопредение методов, где дочерний класс может переопределить метод своего «родителя». Переопределённый метод же скрыт в родительском классе и не вызван, пока дочерний класс не использует ключевое слово super во время переопределения метода.
Пример
/* File name : Employee.java */ public class Employee < private String name; private String address; private int number; public Employee(String name, String address, int number) < System.out.println("Собираем данные о работнике"); this.name = name; this.address = address; this.number = number; >public void mailCheck() < System.out.println("Отправляем чек " + this.name + " " + this.address); >public String toString() < return name + " " + address + " " + number; >public String getName() < return name; >public String getAddress() < return address; >public void setAddress(String newAddress) < address = newAddress; >public int getNumber() < return number; >>
Теперь предположим, что мы наследуем класс Employee следующим образом:
/* File name : Salary.java */ public class Salary extends Employee < private double salary; // Годовая заработная плата public Salary(String name, String address, int number, double salary) < super(name, address, number); setSalary(salary); >public void mailCheck() < System.out.println("Внутри mailCheck класса Salary "); System.out.println("Отправляем чек " + getName() + " с зарплатой " + salary); >public double getSalary() < return salary; >public void setSalary(double newSalary) < if(newSalary >= 0.0) < salary = newSalary; >> public double computePay() < System.out.println("Вычисляем заработную плату для " + getName()); return salary/52; >>
Теперь, внимательно изучите программу и попытайтесь предугадать её вывод:
/* File name : VirtualDemo.java */ public class VirtualDemo < public static void main(String [] args) < Salary s = new Salary("Олег Петров", "Минск, Беларусь", 3, 3600.00); Employee e = new Salary("Иван Иванов", "Москва, Россия", 2, 2400.00); System.out.println("Вызываем mailCheck, используя ссылку Salary --"); s.mailCheck(); System.out.println("Вызываем mailCheck, используя ссылку Employee --"); e.mailCheck(); >>
После запуска программы будет выдан такой результат:
Собираем данные о работнике Собираем данные о работнике Вызываем mailCheck, используя ссылку Salary –– Внутри mailCheck класса Salary Отправляем чек Олег Петров с зарплатой 3600.0 Вызываем mailCheck, используя ссылку Employee –– Внутри mailCheck класса Salary Отправляем чек Иван Иванов с зарплатой 2400.0
Итак, мы создали два объекта Salary. Один использует ссылку Salary, то есть s, а другой использует ссылку Employee, то есть e.
Во время вызова s.mailCheck(), компилятор видит mailCheck() в классе Salary во время компиляции, а JVM вызывает mailCheck() в классе Salary при запуске программы.
mailCheck() в e совсем другое, потому что e является ссылкой Employee. Когда компилятор видит e.mailCheck(), компилятор видит метод mailCheck() в классе Employee.
Во время компиляции был использован mailCheck() в Employee, чтобы проверить это утверждение. Однако во время запуска программы JVM вызывает mailCheck() в классе Salary.
Это поведение называется вызовом виртуальных методов, а эти методы называются виртуальными. Переопределённый метод вызывается во время запуска программы вне зависимости от того, какой тип данных был использован в исходном коде во время компиляции.
Оглавление
- 1. Java – Самоучитель для начинающих
- 2. Java – Обзор языка
- 3. Java – Установка и настройка
- 4. Java – Синтаксис
- 5. Java – Классы и объекты
- 6. Java – Конструкторы
- 7. Java – Типы данных и литералы
- 8. Java – Типы переменных
- 9. Java – Модификаторы
- 10. Java – Операторы
- 11. Java – Циклы и операторы цикла
- 11.1. Java – Цикл while
- 11.2. Java – Цикл for
- 11.3. Java – Улучшенный цикл for
- 11.4. Java – Цикл do..while
- 11.5. Java – Оператор break
- 11.6. Java – Оператор continue
- 12. Java – Операторы принятия решений
- 12.1. Java – Оператор if
- 12.2. Java – Оператор if..else
- 12.3. Java – Вложенный оператор if
- 12.4. Java – Оператор switch..case
- 12.5. Java – Условный оператор (? 🙂
- 13. Java – Числа
- 13.1. Java – Методы byteValue(), shortValue(), intValue(), longValue(), floatValue(), doubleValue()
- 13.2. Java – Метод compareTo()
- 13.3. Java – Метод equals()
- 13.4. Java – Метод valueOf()
- 13.5. Java – Метод toString()
- 13.6. Java – Метод parseInt()
- 13.7. Java – Метод Math.abs()
- 13.8. Java – Метод Math.ceil()
- 13.9. Java – Метод Math.floor()
- 13.10. Java – Метод Math.rint()
- 13.11. Java – Метод Math.round()
- 13.12. Java – Метод Math.min()
- 13.13. Java – Метод Math.max()
- 13.14. Java – Метод Math.exp()
- 13.15. Java – Метод Math.log()
- 13.16. Java – Метод Math.pow()
- 13.17. Java – Метод Math.sqrt()
- 13.18. Java – Метод Math.sin()
- 13.19. Java – Метод Math.cos()
- 13.20. Java – Метод Math.tan()
- 13.21. Java – Метод Math.asin()
- 13.22. Java – Метод Math.acos()
- 13.23. Java – Метод Math.atan()
- 13.24. Java – Метод Math.atan2()
- 13.25. Java – Метод Math.toDegrees()
- 13.26. Java – Метод Math.toRadians()
- 13.27. Java – Метод Math.random()
- 14. Java – Символы
- 14.1. Java – Метод Character.isLetter()
- 14.2. Java – Метод Character.isDigit()
- 14.3. Java – Метод Character.isWhitespace()
- 14.4. Java – Метод Character.isUpperCase()
- 14.5. Java – Метод Character.isLowerCase()
- 14.6. Java – Метод Character.toUpperCase()
- 14.7. Java – Метод Character.toLowerCase()
- 14.8. Java – Метод Character.toString()
- 15. Java – Строки
- 15.1. Java – Метод charAt()
- 15.2. Java – Метод compareTo()
- 15.3. Java – Метод compareToIgnoreCase()
- 15.4. Java – Метод concat()
- 15.5. Java – Метод contentEquals()
- 15.6. Java – Метод copyValueOf()
- 15.7. Java – Метод endsWith()
- 15.8. Java – Метод equals()
- 15.9. Java – Метод equalsIgnoreCase()
- 15.10. Java – Метод getBytes()
- 15.11. Java – Метод getChars()
- 15.12. Java – Метод hashCode()
- 15.13. Java – Метод indexOf()
- 15.14. Java – Метод intern()
- 15.15. Java – Метод lastIndexOf()
- 15.16. Java – Метод length()
- 15.17. Java – Метод matches()
- 15.18. Java – Метод regionMatches()
- 15.19. Java – Метод replace()
- 15.20. Java – Метод replaceAll()
- 15.21. Java – Метод replaceFirst()
- 15.22. Java – Метод split()
- 15.23. Java – Метод startsWith()
- 15.24. Java – Метод subSequence()
- 15.25. Java – Метод substring()
- 15.26. Java – Метод toCharArray()
- 15.27. Java – Метод toLowerCase()
- 15.28. Java – Метод toString()
- 15.29. Java – Метод toUpperCase()
- 15.30. Java – Метод trim()
- 15.31. Java – Метод valueOf()
- 15.32. Java – Классы StringBuilder и StringBuffer
- 15.32.1. Java – Метод append()
- 15.32.2. Java – Метод reverse()
- 15.32.3. Java – Метод delete()
- 15.32.4. Java – Метод insert()
- 15.32.5. Java – Метод replace()
- 16. Java – Массивы
- 17. Java – Дата и время
- 18. Java – Регулярные выражения
- 19. Java – Методы
- 20. Java – Потоки ввода/вывода, файлы и каталоги
- 20.1. Java – Класс ByteArrayInputStream
- 20.2. Java – Класс DataInputStream
- 20.3. Java – Класс ByteArrayOutputStream
- 20.4. Java – Класс DataOutputStream
- 20.5. Java – Класс File
- 20.6. Java – Класс FileReader
- 20.7. Java – Класс FileWriter
- 21. Java – Исключения
- 21.1. Java – Встроенные исключения
- 22. Java – Вложенные и внутренние классы
- 23. Java – Наследование
- 24. Java – Переопределение
- 25. Java – Полиморфизм
- 26. Java – Абстракция
- 27. Java – Инкапсуляция
- 28. Java – Интерфейсы
- 29. Java – Пакеты
- 30. Java – Структуры данных
- 30.1. Java – Интерфейс Enumeration
- 30.2. Java – Класс BitSet
- 30.3. Java – Класс Vector
- 30.4. Java – Класс Stack
- 30.5. Java – Класс Dictionary
- 30.6. Java – Класс Hashtable
- 30.7. Java – Класс Properties
- 31. Java – Коллекции
- 31.1. Java – Интерфейс Collection
- 31.2. Java – Интерфейс List
- 31.3. Java – Интерфейс Set
- 31.4. Java – Интерфейс SortedSet
- 31.5. Java – Интерфейс Map
- 31.6. Java – Интерфейс Map.Entry
- 31.7. Java – Интерфейс SortedMap
- 31.8. Java – Класс LinkedList
- 31.9. Java – Класс ArrayList
- 31.10. Java – Класс HashSet
- 31.11. Java – Класс LinkedHashSet
- 31.12. Java – Класс TreeSet
- 31.13. Java – Класс HashMap
- 31.14. Java – Класс TreeMap
- 31.15. Java – Класс WeakHashMap
- 31.16. Java – Класс LinkedHashMap
- 31.17. Java – Класс IdentityHashMap
- 31.18. Java – Алгоритмы Collection
- 31.19. Java – Iterator и ListIterator
- 31.20. Java – Comparator
- 32. Java – Дженерики
- 33. Java – Сериализация
- 34. Java – Сеть
- 34.1. Java – Обработка URL
- 35. Java – Отправка Email
- 36. Java – Многопоточность
- 36.1. Java – Синхронизация потоков
- 36.2. Java – Межпоточная связь
- 36.3. Java – Взаимная блокировка потоков
- 36.4. Java – Управление потоками
- 37. Java – Основы работы с апплетами
- 38. Java – Javadoc
Что такое полиморфизм в java
Полиморфизм на первый взгляд кажется самой малоинтересной и малоперспективной парадигмой, но на самом деле это совсем не так. Полиморфизм удивительно мощная и востребованная парадигма. Давайте попробуем разобраться, что это такое.
Полиморфизмом назвается возможность работать с несколькими типами так, как будто это один и тот же тип и в то же время поведение каждого типа будет уникальным в зависимости от его реализации. Возможно, что вы ничего не поняли, поэтому попробую описать это иначе и на примере. Давайте так и сделаем.
В предыдущей части мы создавали класс RobotTotal, который наследовался от класса Robot. Если немного подумать, то по парадигме наследования будет интуитивно понятно, что класс RobotTotal является пусть и несколько измененным, но тем не менее классом Robot. Исходя из этого вполне непротиворечивого соображения мы можем написать несколько иную реализацию класса RobotManager
package edu . javacourse . robot ;
public class RobotManager
public static void main ( String [ ] args ) <
// Первое проявление полиморфизма - ссылке на класс-предок
// можно присвоить класс-потомок
Robot robot = new RobotTotal ( 0 , 0 ) ;
robot . forward ( 20 ) ;
robot . setCourse ( 90 ) ;
robot . forward ( 20 ) ;
robot . setCourse ( 90 ) ;
robot . forward ( 50 ) ;
// Напечатать координаты
robot . printCoordinates ( ) ;
// Напечатать общую дистанцию уже не получится
// компилятор выдает ошибку
//System.out.println(robot.getTotalDistance());
Как видим, создавая объект RobotTotal мы его «сужаем» до объекта класса Robot. С одной стороны это выглядит достаточно логично и непротиворечиво — RobotTotal является роботом. С другой стороны возникает вопрос — а метод forward будет вызываться какой ? Класса RobotTotal или Robot ? Думаю, что для вас ответ «Как RobotTotal» выглядит предпочтительнее — и это соврешенно правильный ответ. Можете в этом убедиться сами, добавив в методв forward какую-либо поясняющую печать. Например так:
public void forward ( int distance ) < super . forward ( distance ) ; totalDistance += distance ; System . out . println ( "RobotTotal" ) ;
В этом можно убедиться еще более удобным и практичным образом — спросить у объекта его класс и у класса спросить его имя. Этот механизм называется Reflection — он позволяет получить информацию об объекте прямо в момент выполнения программы. Мы будем его рассматривать несколько позже. Вот как может выглядеть такой вызов:
Расшифровывается это приблизительно так: сначала у объекта robot получаем его класс методом getClass(). Возвращается объект типа Class (есть такой стандартный класс — Class). И у этого класса есть метод, который возращает имя — getName(). Вот возможный полный код:
package edu . javacourse . robot ;
public class RobotManager
public static void main ( String [ ] args ) <
Robot robot = new RobotTotal ( 0 , 0 ) ;
System . out . println ( robot . getClass ( ) . getName ( ) ) ;
Как видим мы можем присвоить ссылке на объект класса-предка объект-потомок — и это работает. С одной стороны, мы работаем как-будто с классом Robot, с другой стороны — поведение нашего обхекта соответствует классу RobotTotal. ВАЖНО . А вот в обратную сторону присваивание НЕ работает. На запись вот такого вида
RobotTotal robot = new Robot(0, 0);
компилятор будет выдавать ошибку.
Думаю, что с технической точки зрения все достаточно понятно. Но возникает логичный вопрос — зачем это вообще надо ? С инкапсуляцией более-менее понятно, с наследованием — в принципе тоже. Но вот этот механизм зачем, какое преимущество мы получим при использовании этой парадигмы ? Для первого приближения рассмотрим наш пример графического приложения, в котором мы создавали свой компонент OvalComponent. Мы использовали модифицированный класс JFrame (OvalFrame) и что весьма важно, мы использовали уже готовый метод add для добавления нашего объекта на форму. Я бы хотел заострить ваше внимание на этом весьма тонком моменте — мы использовали УЖЕ существующий метод существующего класса JFrame. И этот метод (да и класс тоже) совершенно не осведомлен о нашем новом классе OvalComponent. И тем не менее он прекрасно с ним работает — мы сами это видели. Думаю, что вы уже догадались в чем фокус, но я тем не менее проговорю эту мысль — класс JFrame умеет работать с классами-потомками от класса JComponent и ему не важно, какой точно класс он получил — они для него все являются объектами класса JComponent. И это здорово нам помогает. Т.к. наш класс OvalComponent рисует себя сам путем вызова метода paintComponent где-то у себя, то ситуация еще более восхитительна — вызывается именно НАШ метод paintComponent. Значит мы можем написать много разных классов унаследованных от класса JComponent, положить их на форму и все они будут рисоваться так, как они сами это умеют.
Что еще интересно — для самого себя класс тоже может вести себя полиморфно. Если внимательно посмотреть на код класса OvalComponent метод paintComponent объявлен как protected и вызывается внутри класса JComponent и никак иначе. Снаружи другим классам он недоступен. Т.е. все наследники класса JComponent предоставляют свои реализации метода paintComponent и вызывают его внутри унаследованного метода paint, который уже объявлен как public. Иными словами — все наследники используют метод paintComponent из уже готового метода paint. Возможно, вы еще не совсем готовы оценить «красоту игры», но на мой взгляд полиморфизм является просто чудесной штукой. Мы еще вернемся к этой весьма увлекательной парадигме, ну а пока сделаем еще одно графическое приложение, которое позволит нам поместить разные типы компонентов на форму, о которых она не знает, но тем не менее сможет прекрасно ими управлять.
Графическое приложение
Данное приложение несколько сложнее предыдущего — здесь создаются разные компоненты для рисования разных фигур. Приложение содержит 5 классов: 3 класса являются компонентами, которые рисуют внутри себя три разных фигуры (овал, треугольник и прямоугольник), класс для отображения окна и класс для создания и отображения самой формы. Сначала рассмотрим три класса для рисования фигур. Они используют методы класса Graphics и вряд ли требуют каких-либо комментариев. Я их поместил в отдельный пакет.
Java Challengers #3: Полиморфизм и наследование
Мы продолжаем перевод серии статей с задачками по Java. Прошлый пост про строки вызвал на удивление бурную дискуссию. Надеемся, что мимо этой статьи вы тоже не пройдете мимо. И да — мы приглашаем теперь на юбилейный десятый поток нашего курса «Разработчик Java».
Согласно легендарному Венкату Субраманиам (Venkat Subramaniam) полиморфизм является самым важным понятием в объектно — ориентированном программировании. Полиморфизм — или способность объекта выполнять специализированные действия на основе его типа — это то, что делает Java — код гибким. Шаблоны проектирования, такие как Команда (Command), Наблюдатель (Observer), Декоратор (Decorator), Стратегия (Strategy), и многие другие, созданные бандой четырех (Gang Of Four), все используют ту или иную форму полиморфизма. Освоение этой концепции значительно улучшит вашу способность продумывать программные решения.
Вы можете взять исходный код для этой статьи и поэксперементировать здесь: https://github.com/rafadelnero/javaworld-challengers
Интерфейсы и наследование в полиморфизме
В этой статье мы сфокусируемся на связи между полиморфизмом и наследованием. Главное иметь в виду, что полиморфизм требует наследования или реализации интерфейса. Вы можете увидеть это на примере ниже с Дюком ( Duke ) и Джагги ( Juggy ):
public abstract class JavaMascot < public abstract void executeAction(); >public class Duke extends JavaMascot < @Override public void executeAction() < System.out.println("Punch!"); >> public class Juggy extends JavaMascot < @Override public void executeAction() < System.out.println("Fly!"); >> public class JavaMascotTest < public static void main(String. args) < JavaMascot dukeMascot = new Duke(); JavaMascot juggyMascot = new Juggy(); dukeMascot.executeAction(); juggyMascot.executeAction(); >>
Вывод этого кода будет таким:
Punch! Fly!
Так как определены конкретные реализации, то будут вызваны методы и Duke и Juggy .
Перегрузка (overloading) метода — это полиморфизм? Многие программисты путают отношение полиморфизма с переопределением методов (overriding) и перегрузкой методов (overloading). Фактически, только переопределение метода — это истинный полиморфизм. Перегрузка использует то же имя метода, но разные параметры. Полиморфизм — это широкий термин, поэтому всегда будут дискуссии на эту тему.
Какова цель полиморфизма
Большим преимуществом и целью использования полиморфизма является уменьшение связанности клиентского класса с реализацией. Вместо того чтобы хардкодить, клиентский класс получает реализацию зависимости для выполнения необходимого действия. Таким образом, клиентский класс знает минимум для выполнения своих действий, что является примером слабого связывания.
Чтобы лучше понять цель полиморфизма, взгляните на SweetCreator :
public abstract class SweetProducer < public abstract void produceSweet(); >public class CakeProducer extends SweetProducer < @Override public void produceSweet() < System.out.println("Cake produced"); >> public class ChocolateProducer extends SweetProducer < @Override public void produceSweet() < System.out.println("Chocolate produced"); >> public class CookieProducer extends SweetProducer < @Override public void produceSweet() < System.out.println("Cookie produced"); >> public class SweetCreator < private ListsweetProducer; public SweetCreator(List sweetProducer) < this.sweetProducer = sweetProducer; >public void createSweets() < sweetProducer.forEach(sweet ->sweet.produceSweet()); > > public class SweetCreatorTest < public static void main(String. args) < SweetCreator sweetCreator = new SweetCreator(Arrays.asList( new CakeProducer(), new ChocolateProducer(), new CookieProducer())); sweetCreator.createSweets(); >>
В этом примере вы можете видеть, что класс SweetCreator знает только о классе SweetProducer . Он не знает реализации каждого Sweet . Такое разделение дает нам гибкость для обновления и повторного использования наших классов, а это делает код намного проще в сопровождении. При проектировании кода всегда ищите способы сделать его максимально гибким и удобным. Полиморфизм — это очень мощный способ для использования в этих целях.
Аннотация @Override обязывает программиста использовать такую же сигнатуру метода, которая должна быть переопределена. Если метод не переопределен, будет ошибка компиляции.
Ковариантные возвращаемые типы при переопределении метода
Можно изменить тип возвращаемого значения переопределенного метода если это ковариантный тип. Ковариантный тип в основном является подклассом возвращаемого значения.
public abstract class JavaMascot < abstract JavaMascot getMascot(); >public class Duke extends JavaMascot < @Override Duke getMascot() < return new Duke(); >>
Поскольку Duke является JavaMascot , мы можем изменить тип возвращаемого значения при переопределении.
Полиморфизм в базовых классах Java
Мы постоянно используем полиморфизм в базовых классах Java. Один очень простой пример — создание экземпляра класса ArrayList с объявлением типа как интерфейс List .
List list = new ArrayList<>();
Рассмотрим пример кода, использующий Java Collections API без полиморфизма:
public class ListActionWithoutPolymorphism < // Пример без полиморфизма void executeVectorActions(Vectorvector)* Здесь повтор кода */> void executeArrayListActions(ArrayList arrayList)* Здесь повтор кода */> void executeLinkedListActions(LinkedList linkedList)* Здесь повтор кода */> void executeCopyOnWriteArrayListActions(CopyOnWriteArrayList copyOnWriteArrayList) < /* Здесь повтор кода */>> public class ListActionInvokerWithoutPolymorphism < listAction.executeVectorActions(new Vector<>()); listAction.executeArrayListActions(new ArrayList<>()); listAction.executeLinkedListActions(new LinkedList<>()); listAction.executeCopyOnWriteArrayListActions(new CopyOnWriteArrayList<>()); >
Отвратительный код, не так ли? Представьте себе, что вам нужно его сопровождать! Теперь рассмотрим тот же пример с полиморфизмом:
public static void main(String. polymorphism) < ListAction listAction = new ListAction(); listAction.executeListActions(); >public class ListAction < void executeListActions(Listlist) < // Выполнение действий с различными списками >> public class ListActionInvoker < public static void main(String. masterPolymorphism) < ListAction listAction = new ListAction(); listAction.executeListActions(new Vector<>()); listAction.executeListActions(new ArrayList<>()); listAction.executeListActions(new LinkedList<>()); listAction.executeListActions(new CopyOnWriteArrayList<>()); > >
Преимущество полиморфизма — гибкость и расширяемость. Вместо того чтобы создавать несколько различных методов, мы можем объявить один метод, который получает тип List .
Вызов конкретных методов для полиморфного метода
Можно вызвать конкретные методы при полиморфном вызове метода, это происходит за счет гибкости. Вот пример:
public abstract class MetalGearCharacter < abstract void useWeapon(String weapon); >public class BigBoss extends MetalGearCharacter < @Override void useWeapon(String weapon) < System.out.println("Big Boss is using a " + weapon); >void giveOrderToTheArmy(String orderMessage) < System.out.println(orderMessage); >> public class SolidSnake extends MetalGearCharacter < void useWeapon(String weapon) < System.out.println("Solid Snake is using a " + weapon); >> public class UseSpecificMethod < public static void executeActionWith(MetalGearCharacter metalGearCharacter) < metalGearCharacter.useWeapon("SOCOM"); // Следующая строка не будет работать // metalGearCharacter.giveOrderToTheArmy("Attack!"); if (metalGearCharacter instanceof BigBoss) < ((BigBoss) metalGearCharacter).giveOrderToTheArmy("Attack!"); >> public static void main(String. specificPolymorphismInvocation) < executeActionWith(new SolidSnake()); executeActionWith(new BigBoss()); >>
Техника, которую мы используем здесь — это приведение типов (casting) или сознательное изменение типа объекта во время выполнения.
Обратите внимание, что вызов определенного метода возможен только при приведении более общего типа к более специфичному типу. Хорошей аналогией было бы сказать явно компилятору: «Эй, я знаю, что я здесь делаю, поэтому я собираюсь привести объект к определенному типу и буду использовать этот метод.»
Ссылаясь на приведенный выше пример, у компилятора есть веская причина не принимать вызов определенных методов: класс, который передаётся должен быть SolidSnake . В этом случае, у компилятора нет никакого способа гарантировать, что каждый подкласс MetalGearCharacter имеет метод giveOrderToTheArmy .
Ключевое слово instanceof
Обратите внимание на зарезервированное слово instanceof . Перед вызовом конкретного метода мы спросили, является ли MetalGearCharacter экземпляром (instanceof) BigBoss . Если это не экземпляр BigBoss , мы получим следующее исключение:
Exception in thread `main" java.lang.ClassCastException: com.javaworld.javachallengers.polymorphism.specificinvocation.SolidSnake cannot be cast to com.javaworld.javachallengers.polymorphism.specificinvocation.BigBoss
Ключевое слово super
Что делать, если мы хотим сослаться на атрибут или метод из родительского класса? В этом случае мы можем использовать ключевое слово super .
Например:
public class JavaMascot < void executeAction() < System.out.println("The Java Mascot is about to execute an action!"); >> public class Duke extends JavaMascot < @Override void executeAction() < super.executeAction(); System.out.println("Duke is going to punch!"); >public static void main(String. superReservedWord) < new Duke().executeAction(); >>
Использование зарезервированного слова super в методе executeAction класса Duke вызывает метод родительского класса. Затем мы выполняем конкретное действие из класса Duke . Вот почему мы можем видеть оба сообщения в выводе:
The Java Mascot is about to execute an action! Duke is going to punch!
Решите задачку по полиморфизму
Давайте проверим, что вы узнали о полиморфизме и наследовании.
В этой задачке Вам дается несколько методов от Matt Groening’s The Simpsons, от вавам требуется разгадать, какой будет вывод для каждого класса. Для начала внимательно проанализируйте следующий код:
public class PolymorphismChallenge < static abstract class Simpson < void talk() < System.out.println("Simpson!"); >protected void prank(String prank) < System.out.println(prank); >> static class Bart extends Simpson < String prank; Bart(String prank) < this.prank = prank; >protected void talk() < System.out.println("Eat my shorts!"); >protected void prank() < super.prank(prank); System.out.println("Knock Homer down"); >> static class Lisa extends Simpson < void talk(String toMe) < System.out.println("I love Sax!"); >> public static void main(String. doYourBest) < new Lisa().talk("Sax :)"); Simpson simpson = new Bart("D'oh"); simpson.talk(); Lisa lisa = new Lisa(); lisa.talk(); ((Bart) simpson).prank(); >>
Как вы думаете? Каким будет результат? Не используйте IDE, чтобы выяснить это! Цель в том, чтобы улучшить ваши навыки анализа кода, поэтому постарайтесь решить самостоятельно.
Выберите ваш ответ (правильный ответ вы сможете найти в конце статьи).
A)
I love Sax!
D’oh
Simpson!
D’oh
B)
Sax 🙂
Eat my shorts!
I love Sax!
D’oh
Knock Homer down
C)
Sax 🙂
D’oh
Simpson!
Knock Homer down
D)
I love Sax!
Eat my shorts!
Simpson!
D’oh
Knock Homer down
Что случилось? Понимание полиморфизма
Для следующего вызова метода:
new Lisa().talk("Sax :)");
вывод будет «I love Sax!». Это потому, что мы передаём строку в метод и у класса Lisa есть такой метод.
Для следующего вызова:
Simpson simpson = new Bart("D'oh"); simpson.talk();
Вывод будет «Eat my shorts!». Это потому, что мы инициализируем тип Simpson с помощью Bart .
Теперь смотрите, это немного сложнее:
Lisa lisa = new Lisa(); lisa.talk();
Здесь мы используем перегрузку метода с наследованием. Мы ничего не передаем методу talk , поэтому вызывается метод talk из Simpson .
В этом случае на выходе будет «Simpson!».
((Bart) simpson).prank();
В этом случае строка prank была передана при создании экземпляра класса Bart через new Bart(«D’oh»); . В этом случае сначала вызывается метод super.prank() , а затем метод prank() из класса Bart . Вывод будет:
"D'oh" "Knock Homer down"
Распространенные ошибки с полиморфизмом
Распространенная ошибка думать, что можно вызвать конкретный метод без использования приведения типа.
Другой ошибкой является неуверенность в том, какой метод будет вызван при полиморфном создании экземпляра класса. Помните, что вызываемый метод является методом созданного экземпляра.
Также помните, что переопределение метода не является перегрузкой метода.
Невозможно переопределить метод, если параметры отличаются. Можно изменить тип возвращаемого значения переопределенного метода, если возвращаемый тип является подклассом.
Что нужно помнить о полиморфизме
- Созданный экземпляр определяет, какой метод будет вызван при использовании полиморфизма.
- Аннотация @Override обязывает программиста использовать переопределенный метод; в противном случае возникнет ошибка компилятора.
- Полиморфизм может использоваться с обычными классами, абстрактными классами и интерфейсами.
- Большинство шаблонов проектирования зависят от той или иной формы полиморфизма.
- Единственный способ вызвать нужный ваш метод в полиморфном подклассе — это использовать приведение типов.
- Можно создать мощную структуру кода, используя полиморфизм.
- Экспериментируйте. Через это, вы сможете овладеть этой мощной концепцией!
Ответ
I love Sax! Eat my shorts! Simpson! D'oh Knock Homer down
Как всегда приветствую ваши комментарии и вопросы. И ждём у Виталия на открытом уроке.
Полиморфизм
Полиморфизм (polymorphism) — это понятие из объектно-ориентированного программирования, которое позволяет разным сущностям выполнять одни и те же действия. При этом неважно, как эти сущности устроены внутри и чем они различаются.
С греческого языка слово «полиморфизм» означает «многообразие». Термин используют и в других сферах. Скажем, полиморфизм в биологии — способность какого-то вида существовать в разных формах.
Пример полиформности в природе — пчелы: есть рабочие пчелы, матка, трутни. Они разные, но все могут летать независимо от того, что это за пчела. По похожему принципу работает и полиморфизм в ООП.
Освойте профессию
«Fullstack-разработчик на Python»
Профессия / 12 месяцев
Fullstack-разработчик на Python
Создавайте веб-проекты самостоятельно
Fullstack-разработчик на Python
Fullstack-разработчики могут в одиночку сделать IT-проект от архитектуры до интерфейса. Их навыки востребованы у работодателей, особенно в стартапах. Научитесь программировать на Python и JavaScript и создавайте сервисы с нуля.
Например, есть две разных сущности: картинка и видео. И тем, и другим можно поделиться: отправить в личное сообщение другому человеку. Программист может сделать два разных метода — один для картинки, другой для видео. А может воспользоваться полиморфизмом и создать один метод «Отправить» для обеих сущностей.
Такой метод будет называться полиморфным. Плюс этого подхода — разработчик пишет меньше кода и не повторяется.
Чтобы лучше понять, что такое полиморфизм и как он работает, советуем прочитать статью про ООП. Полиморфизм — один из четырех основных принципов этого способа программирования
Для чего нужен полиморфизм
- Облегчает написание кода. Не нужно придумывать десять разных методов: отправить одно, другое, третье. Есть один метод, который можно применять к разным сущностям и не задумываться.
- Позволяет масштабировать решения. Если понадобится отправлять не только видео и картинки, но и текст, это можно будет сделать той же командой.
- Делает код более читаемым. Разработчику не нужно разбираться, что делает десяток методов с похожими названиями. Есть один метод, и по его названию все понятно.
- Помогает точно понимать, чего ждать от разных методов, то есть делает код более предсказуемым. Не может быть такого, что метод «Отправить» вдруг окажется методом не для картинки, а для видео.
С понятием должен быть знаком любой разработчик. Множество языков программирования используют полиморфизм: C, C++, Java, Python и другие. Не все эти языки чисто объектно-ориентированные: некоторые устроены так, что с ними можно работать в разных парадигмах программирования. Так что столкнуться с полиморфизмом может каждый.
Станьте Fullstack-разработчик на Python и найдите стабильную работу
на удаленке
Полиморфизм как принцип ООП
В объектно-ориентированном программировании четыре основных принципа: инкапсуляция, абстракция, наследование и полиморфизм. Это связанные между собой вещи — без одного не работало бы и другое.
Инкапсуляция — это создание сущностей как «вещей в себе». Классы должны работать независимо друг от друга: если какая-то сущность, например, удалится, это не повлияет на принцип работы остальных.
Абстракция — это принцип, когда какие-то общие вещи сводятся до набора абстрактных признаков. То есть мы имеем не абсолютно разные классы «картинка», «видео», «текст», а абстрактный класс «контент».
Наследование — это возможность делать на основе одних сущностей другие. Обычно в качестве «родителя» используются абстрактные сущности, а от них наследуются уже более конкретные. То есть если родитель — «контент», то дети — «картинка», «видео», «текст». Это все подвиды контента и его наследники.
Чтобы реализовать полиморфизм, нужны как минимум абстракция и наследование. Они помогают сделать абстрактный класс, в нем — абстрактный «общий» метод, а потом унаследовать разные реализации этого метода. В итоге название одно, а механика разная в зависимости от подтипа. Сейчас разберемся подробнее.
Что такое полиморфный метод и как его создают
Полиформный — это многообразный: формы различаются, а действие одно и то же. Тот же самый метод «Отправить» из примера выше может быть реализован по-разному для видео и картинки. Но вызывается он одинаково для всех видов контента и выполняет одну и ту же задачу. Вот как это работает.
Сначала программист создает общий класс. Например, «контент». В нем он описывает вещи, общие для всего контента: свойства и методы. Свойства — это признаки, то, что есть у контента: количество лайков, возможность комментирования и так далее. А методы — это действия, то есть команды: контент можно лайкнуть, открыть на отдельной вкладке, репостнуть или отправить в личное сообщение.
У общего класса — абстрактные, общие методы. Контент можно отправить, но как — пока непонятно. Зато уже можно описать, как будет выглядеть эта команда: как она называется, что в нее нужно передать, какое сообщение выдать после этого. Это своего рода каркас будущего конкретного действия.
Затем разработчик создает производные классы. Это наследники общего класса: они более конкретные, у них могут быть дополнительные свойства и методы. Например, видео можно поставить на паузу, а картинку — нет. А еще у них сохраняются абстрактные свойства и методы родителя: их переопределяют, чтобы они тоже работали конкретнее.
У производных классов — свои реализации общих методов. Например, в классе «контент» есть метод «отправить». Он же есть и в производных классах. В классе «картинка» может быть свой код для отправки, а в классе «видео» — свой. Они могут быть похожи или различаться. Но название у них окажется одинаковым, и делать они будут одно и то же.
Можно создавать объекты производных классов и пользоваться их методами. Абстрактные классы существуют как бы в вакууме: нельзя создать объект, который будет принадлежать такому классу. Среди реальных объектов не может быть «просто контента», который не является ни текстом, ни картинкой, ни видео, ни еще чем-то. Соответственно, абстрактные методы никто не будет вызывать. А вот переопределенные методы из производных классов — вполне реальные, ими можно пользоваться в разных ситуациях.
Формы полиморфизма
Существуют разные виды полиморфизма. Вообще-то классификация довольно широкая и начинающему легко в ней запутаться, поэтому мы решили ограничиться только основными формами.
Полиморфизм подтипов. Это полиморфность «по умолчанию»: когда в ООП говорят о полиморфизме, обычно имеют в виду его. Выше мы рассказывали именно про такой тип. Это возможность использовать одни и те же команды, или интерфейсы, для разных сущностей — подтипов.
Параметрический полиморфизм. Его еще называют обобщенным полиморфизмом. В нем для команды не имеет значения, какую сущность ей прислали: для всех возможных классов будет использоваться один код. Такой полиморфизм считается «истинным» и делает код универсальнее, но реализовать его сложнее.
Полиморфизм ad hoc. Этот вид полиморфизма еще называют специализированным. Его иногда противопоставляют параметрическому: идея ad hoc — разный код при одинаковом названии. Часто такой полиморфизм реализуют с помощью перегрузки методов: несколько раз пишут метод с одним и тем же названием, но разным кодом.
Статический и динамический полиморфизм
На более «глубоком», близком к машине уровне полиморфизм можно разделить на две группы — статический и динамический. Разница —в том, когда программа переходит от общего метода к одной из его вариаций.
- Статический — метод переопределяют при компиляции.
- Динамический — при выполнении программы.
Статический полиморфизм реализуют с помощью перегрузки методов, о которой мы рассказывали выше. Динамический — с помощью абстракций. Обычно в объектно-ориентированных языках есть возможность применить оба варианта.
Преимущества полиморфизма
- Код становится аккуратнее: не приходится множить сущности и создавать десяток команд, которые делают одно и то же. Нет «лапши» — бессвязного неструктурированного кода, в котором тяжело разобраться.
- Разработчику удобнее: не нужно каждый раз думать, что делает команда «Отправить» для конкретного вида контента, можно просто написать ее и получить результат.
- Работать с разными сущностями можно одинаково: не обязательно каждый раз узнавать, о каком именно производном классе речь идет на этот раз. Ведь общее полиморфное действие есть для всего.
- Код легче расширять, использовать заново и всячески модифицировать.
Недостатки полиморфизма
- Связь полиморфизма с наследованием порой расценивают как слабое место всей концепции. Если нужен полиформный класс, но для конкретной ситуации не подходит наследование, — реализация может усложниться.
- Не всегда полиморфизм легко реализовать на практике. Поэтому существует довольно много реализаций, которые работают плохо: с багами и необъяснимыми ошибками.
- Полиморфизм может ухудшать производительность кода, делать его более «тяжелым» и медленным. Но тут многое зависит от реализации: скажем, параметрический обычно быстрее, чем ad hoc.
- Новичкам бывает тяжело понять принцип — объяснить полиморфизм простыми словами можно только в связке с другими понятиями ООП. Так что человек на момент изучения уже должен понимать, что такое объектно-ориентированное программирование.
Как начать изучать полиморфизм
Мы советуем начать с основ: сначала разобраться, как работает программирование в целом, потом перейти к принципам ООП и полиморфизму. На всех этапах лучше практиковаться, чтобы закреплять знания, — к тому же на реальных примерах легче понять тот или иной концепт.
Вы можете записаться на наши курсы и начать учиться уже сейчас. Обещаем много реальных задач и интересной практики!