Полиморфизм Java и его типы

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

Хотя в этом руководстве основное внимание уделяется полиморфизму подтипов, есть несколько других типов, о которых вам следует знать. Мы начнем с обзора всех четырех типов полиморфизма.

загрузить Получить код Загрузите исходный код для примеров приложений из этого руководства. Создано Джеффом Фризеном для JavaWorld.

Типы полиморфизма в Java

В Java есть четыре типа полиморфизма:

  1. Принуждение - это операция, которая обслуживает несколько типов посредством неявного преобразования типов. Например, вы делите целое число на другое целое число или значение с плавающей запятой на другое значение с плавающей запятой. Если один операнд является целым числом, а другой - значением с плавающей запятой, компилятор приводит (неявно преобразует) целое число в значение с плавающей запятой, чтобы предотвратить ошибку типа. (Не существует операции деления, поддерживающей целочисленный операнд и операнд с плавающей точкой.) Другой пример - передача ссылки на объект подкласса параметру суперкласса метода. Компилятор приводит тип подкласса к типу суперкласса, чтобы ограничить операции операциями суперкласса.
  2. Под перегрузкой подразумевается использование одного и того же символа оператора или имени метода в разных контекстах. Например, вы можете использовать +для выполнения сложения целых чисел, сложения с плавающей запятой или конкатенации строк, в зависимости от типов его операндов. Кроме того, в классе могут появляться несколько методов с одинаковым именем (посредством объявления и / или наследования).
  3. Параметрический полиморфизм предусматривает, что в объявлении класса имя поля может ассоциироваться с разными типами, а имя метода может ассоциироваться с разными параметрами и возвращаемыми типами. Затем поле и метод могут принимать разные типы в каждом экземпляре класса (объекте). Например, поле может иметь тип Double(член стандартной библиотеки классов Java, которая обертывает doubleзначение), и метод может возвращать a Doubleв одном объекте, и то же поле может иметь тип, Stringи тот же метод может возвращать a Stringв другом объекте. . Java поддерживает параметрический полиморфизм через дженерики, о которых я расскажу в следующей статье.
  4. Подтип означает, что тип может служить подтипом другого типа. Когда экземпляр подтипа появляется в контексте супертипа, выполнение операции супертипа над экземпляром подтипа приводит к выполнению версии подтипа этой операции. Например, рассмотрим фрагмент кода, который рисует произвольные фигуры. Вы можете выразить этот код рисования более кратко, представив Shapeкласс с draw()методом; путем введения Circle, Rectangleи других подклассов, которые переопределяют draw(); путем введения массива типа Shape, элементы которого хранят ссылки на Shapeэкземпляры подкласса; и по телефону Shape«s draw()метод на каждом конкретном случае. Когда вы звоните draw(), это Circle's, Rectangle' s или другой Shapeэкземплярdraw()вызываемый метод. Мы говорим, что есть много форм Shapeрусского draw()метода.

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

Ad-hoc vs универсальный полиморфизм

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

Полиморфизм подтипа: апкастинг и позднее связывание

Полиморфизм подтипа основан на восходящем и позднем связывании. Апкастинг - это форма приведения, при которой иерархия наследования преобразуется от подтипа к супертипу. Оператор приведения не используется, потому что подтип является специализацией супертипа. Например, Shape s = new Circle();преобразование из Circleв Shape. Это имеет смысл, потому что круг - это своего рода форма.

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

Предположим, что Shapeобъявляется draw()метод, его Circleподкласс переопределяет этот метод, Shape s = new Circle();только что выполнен, а следующая строка указывает s.draw();. Какой draw()метод называется: Shape«s draw()метод или Circle» s draw()метод? Компилятор не знает, какой draw()метод вызвать. Все, что он может сделать, это проверить, существует ли метод в суперклассе, и убедиться, что список аргументов вызова метода и тип возвращаемого значения соответствуют объявлению метода суперкласса. Однако компилятор также вставляет инструкцию в скомпилированный код, который во время выполнения выбирает и использует любую ссылку sдля вызова правильного draw()метода. Эта задача называется поздним связыванием .

Позднее связывание против раннего связывания

Позднее связывание используется для вызовов неэкземплярных finalметодов. Для всех остальных вызовов методов компилятор знает, какой метод вызывать. Он вставляет в скомпилированный код инструкцию, которая вызывает метод, связанный с типом переменной, а не ее значением. Этот метод известен как раннее связывание .

Я создал приложение, которое демонстрирует полиморфизм подтипа в терминах апкастинга и позднего связывания. Это приложение состоит из Shape, Circle, Rectangle, и Shapesклассов, где каждый класс хранятся в его собственном исходном файле. В листинге 1 представлены первые три класса.

Листинг 1. Объявление иерархии фигур

class Shape { void draw() { } } class Circle extends Shape { private int x, y, r; Circle(int x, int y, int r) { this.x = x; this.y = y; this.r = r; } // For brevity, I've omitted getX(), getY(), and getRadius() methods. @Override void draw() { System.out.println("Drawing circle (" + x + ", "+ y + ", " + r + ")"); } } class Rectangle extends Shape { private int x, y, w, h; Rectangle(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } // For brevity, I've omitted getX(), getY(), getWidth(), and getHeight() // methods. @Override void draw() { System.out.println("Drawing rectangle (" + x + ", "+ y + ", " + w + "," + h + ")"); } }

В листинге 2 представлен Shapesкласс приложения, main()метод которого управляет приложением.

Listing 2. Upcasting and late binding in subtype polymorphism

class Shapes { public static void main(String[] args) { Shape[] shapes = { new Circle(10, 20, 30), new Rectangle(20, 30, 40, 50) }; for (int i = 0; i < shapes.length; i++) shapes[i].draw(); } }

The declaration of the shapes array demonstrates upcasting. The Circle and Rectangle references are stored in shapes[0] and shapes[1] and are upcast to type Shape. Each of shapes[0] and shapes[1] is regarded as a Shape instance: shapes[0] isn't regarded as a Circle; shapes[1] isn't regarded as a Rectangle.

Late binding is demonstrated by the shapes[i].draw(); expression. When i equals 0, the compiler-generated instruction causes Circle's draw() method to be called. When i equals 1, however, this instruction causes Rectangle's draw() method to be called. This is the essence of subtype polymorphism.

Assuming that all four source files (Shapes.java, Shape.java, Rectangle.java, and Circle.java) are located in the current directory, compile them via either of the following command lines:

javac *.java javac Shapes.java

Run the resulting application:

java Shapes

You should observe the following output:

Drawing circle (10, 20, 30) Drawing rectangle (20, 30, 40, 50)

Abstract classes and methods

When designing class hierarchies, you'll find that classes nearer the top of these hierarchies are more generic than classes that are lower down. For example, a Vehicle superclass is more generic than a Truck subclass. Similarly, a Shape superclass is more generic than a Circle or a Rectangle subclass.

It doesn't make sense to instantiate a generic class. After all, what would a Vehicle object describe? Similarly, what kind of shape is represented by a Shape object? Rather than code an empty draw() method in Shape, we can prevent this method from being called and this class from being instantiated by declaring both entities to be abstract.

Java provides the abstract reserved word to declare a class that cannot be instantiated. The compiler reports an error when you try to instantiate this class. abstract is also used to declare a method without a body. The draw() method doesn't need a body because it is unable to draw an abstract shape. Listing 3 demonstrates.

Listing 3. Abstracting the Shape class and its draw() method

abstract class Shape { abstract void draw(); // semicolon is required }

Abstract cautions

The compiler reports an error when you attempt to declare a class abstract and final. For example, the compiler complains about abstract final class Shape because an abstract class cannot be instantiated and a final class cannot be extended. The compiler also reports an error when you declare a method abstract but don't declare its class abstract. Removing abstract from the Shape class's header in Listing 3 would result in an error, for instance. This would be an error because a non-abstract (concrete) class cannot be instantiated when it contains an abstract method. Finally, when you extend an abstract class, the extending class must override all of the abstract methods, or else the extending class must itself be declared to be abstract; otherwise, the compiler will report an error.

An abstract class can declare fields, constructors, and non-abstract methods in addition to or instead of abstract methods. For example, an abstract Vehicle class might declare fields describing its make, model, and year. Also, it might declare a constructor to initialize these fields and concrete methods to return their values. Check out Listing 4.

Listing 4. Abstracting a vehicle

abstract class Vehicle { private String make, model; private int year; Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } String getMake() { return make; } String getModel() { return model; } int getYear() { return year; } abstract void move(); }

You'll note that Vehicle declares an abstract move() method to describe the movement of a vehicle. For example, a car rolls down the road, a boat sails across the water, and a plane flies through the air. Vehicle's subclasses would override move() and provide an appropriate description. They would also inherit the methods and their constructors would call Vehicle's constructor.

Downcasting and RTTI

Moving up the class hierarchy, via upcasting, entails losing access to subtype features. For example, assigning a Circle object to Shape variable s means that you cannot use s to call Circle's getRadius() method. However, it's possible to once again access Circle's getRadius() method by performing an explicit cast operation like this one: Circle c = (Circle) s;.

This assignment is known as downcasting because you are casting down the inheritance hierarchy from a supertype to a subtype (from the Shape superclass to the Circle subclass). Although an upcast is always safe (the superclass's interface is a subset of the subclass's interface), a downcast isn't always safe. Listing 5 shows what kind of trouble could ensue if you use downcasting incorrectly.

Listing 5. The problem with downcasting

class Superclass { } class Subclass extends Superclass { void method() { } } public class BadDowncast { public static void main(String[] args) { Superclass superclass = new Superclass(); Subclass subclass = (Subclass) superclass; subclass.method(); } }

Listing 5 presents a class hierarchy consisting of Superclass and Subclass, which extends Superclass. Furthermore, Subclass declares method(). A third class named BadDowncast provides a main() method that instantiates Superclass. BadDowncast then tries to downcast this object to Subclass and assign the result to variable subclass.

В этом случае компилятор не будет жаловаться, потому что понижающее преобразование от суперкласса к подклассу в той же иерархии типов допустимо. Тем не менее, если присвоение было разрешено, приложение вылетало бы при попытке выполнения subclass.method();. В этом случае JVM будет пытаться вызвать несуществующий метод, потому Superclassчто не объявляет method(). К счастью, JVM проверяет допустимость приведения перед выполнением операции приведения. Обнаружение того, что Superclassне объявляется method(), выбросит ClassCastExceptionобъект. (Я расскажу об исключениях в следующей статье.)

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

javac BadDowncast.java

Запускаем получившееся приложение:

java BadDowncast