Полиморфизмът се отнася до способността на някои образувания да се срещат в различни форми. Популярно е представено от пеперудата, която се превръща от ларва в какавида в имаго. Полиморфизмът съществува и в езиците за програмиране, като техника за моделиране, която ви позволява да създадете един интерфейс към различни операнди, аргументи и обекти. Полиморфизмът на Java води до код, който е по-кратък и по-лесен за поддръжка.
Докато този урок се фокусира върху полиморфизма на подтипа, има няколко други типа, за които трябва да знаете. Ще започнем с общ преглед на четирите вида полиморфизъм.
изтегляне Вземете кода Изтеглете изходния код, например приложения в този урок. Създадено от Jeff Friesen за JavaWorld.Видове полиморфизъм в Java
В Java има четири вида полиморфизъм:
- Принудата е операция, която обслужва множество типове чрез имплицитно преобразуване на типа. Например, разделяте цяло число на друго цяло число или стойност с плаваща запетая с друга стойност с плаваща запетая. Ако единият операнд е цяло число, а другият операнд е стойност с плаваща запетая, компилаторът принуждава (имплицитно преобразува) цялото число към стойност с плаваща запетая, за да предотврати грешка в типа. (Няма операция за разделяне, която поддържа операнд с цяло число и операнд с плаваща запетая.) Друг пример е предаването на препратка към обект на подклас към параметъра на суперкласа на метода. Компилаторът принуждава типа подклас към типа суперклас, за да ограничи операциите до тези на суперкласа.
- Претоварването се отнася до използването на един и същ символ на оператор или име на метод в различен контекст. Например можете да използвате
+
за извършване на цяло число, добавяне с плаваща запетая или конкатенация на низове, в зависимост от видовете операнди. Също така, множество методи с едно и също име могат да се появят в клас (чрез декларация и / или наследяване). - Параметричният полиморфизъм предвижда, че в рамките на декларация за клас име на поле може да се асоциира с различни типове, а името на метода може да се асоциира с различни типове параметри и връщане. След това полето и методът могат да приемат различни типове във всеки екземпляр на клас (обект). Например, поле може да е от тип
Double
(член на стандартната библиотека на клас Java, която обгръщаdouble
стойност) и метод може да върне aDouble
в един обект, а същото поле може да е от типString
и същият метод да върне aString
в друг обект . Java поддържа параметричен полиморфизъм чрез генерици, което ще обсъдя в следваща статия. - Подтип означава, че даден тип може да служи като подтип на друг тип. Когато екземпляр на подтип се появява в контекст на супертип, изпълнението на операция на супертип на екземпляра на подтипа води до изпълнение на версията на подтипа на тази операция. Например, помислете за фрагмент от код, който рисува произволни фигури. Можете да изразите този код за рисуване по-кратко, като въведете
Shape
клас сdraw()
метод; чрез въвежданеCircle
,Rectangle
и други подкласове, които отменятdraw()
; чрез въвеждане на масив от тип,Shape
чиито елементи съхраняват препратки къмShape
екземпляри на подклас; и чрез извикванеShape
наdraw()
метода на всеки екземпляр. Когато се обадитеdraw()
, това еCircle
'sRectangle
' , 's или другShape
екземплярdraw()
метод, който се извиква. Ние казваме, че има много форми наShape
еdraw()
метод.
Този урок въвежда подтип полиморфизъм. Ще научите за актуализиране и късно свързване, абстрактни класове (които не могат да бъдат създадени) и абстрактни методи (които не могат да се извикват). Също така ще научите за низходящото предаване и идентификацията на типа на изпълнение и ще разгледате за първи път ковариантните типове връщане. Ще запазя параметричния полиморфизъм за бъдещ урок.
Ad-hoc срещу универсален полиморфизъм
Подобно на много разработчици, класифицирам принудата и претоварването като ad-hoc полиморфизъм, а параметричните и подтипа като универсален полиморфизъм. Макар и ценни техники, не вярвам, че принудата и претоварването са истински полиморфизъм; те са по-скоро преобразувания на типове и синтактична захар.
Подтип полиморфизъм: Актуализиране и късно свързване
Полиморфизмът на подтипа разчита на актуализиране и късно свързване. Upcasting е форма на кастинг, при която прехвърляте йерархията на наследяването от подтип към супертип. Не участва оператор на гласове, тъй като подтипът е специализация на супертипа. Например Shape s = new Circle();
актуализации от Circle
до Shape
. Това има смисъл, защото кръгът е вид форма.
След обновяването Circle
до Shape
, не можете да извикате Circle
-специфични методи, като getRadius()
метод, който връща радиуса на кръга, тъй като Circle
-специфичните методи не са част от Shape
интерфейса на. Загубата на достъп до функции на подтипа след стесняване на подклас до неговия суперклас изглежда безсмислено, но е необходимо за постигане на полиморфизъм на подтипа.
Да предположим, че Shape
декларира draw()
метод, неговият Circle
подклас заменя този метод, Shape s = new Circle();
току-що е изпълнен и следващият ред указва s.draw();
. Кой draw()
метод се нарича: Shape
е draw()
метод или Circle
е 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()
метод управлява приложението.
Листинг 2. Актуализиране и късно свързване при политип на подтипа
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(); } }
Декларацията на shapes
масива демонстрира обновяване. В Circle
и Rectangle
справки се съхраняват в shapes[0]
и shapes[1]
и са се наследи да пишете Shape
. Всеки от shapes[0]
и shapes[1]
се разглежда като Shape
екземпляр: shapes[0]
не се разглежда като a Circle
; shapes[1]
не се разглежда като a Rectangle
.
Късното свързване се демонстрира от shapes[i].draw();
израза. Когато i
се равнява 0
, съставител генерирани инструкции причините Circle
е draw()
метод, за да се нарече. Когато i
се равнява 1
, обаче, тази инструкция причини Rectangle
е draw()
метод, за да се нарече. Това е същността на подтипа полиморфизъм.
Ако приемем, че всичките четири изходни файлове ( Shapes.java
, Shape.java
, Rectangle.java
, и Circle.java
) се намира в текущата директория, да ги събира чрез един от следните командни редове:
javac *.java javac Shapes.java
Стартирайте полученото приложение:
java Shapes
Трябва да наблюдавате следния изход:
Drawing circle (10, 20, 30) Drawing rectangle (20, 30, 40, 50)
Абстрактни класове и методи
Когато проектирате йерархии на класове, ще откриете, че класовете по-близо до върха на тези йерархии са по-общи от класовете, които са по-ниско долу. Например Vehicle
суперкласът е по-общ от Truck
подклас. По същия начин Shape
суперкласът е по-общ от a Circle
или Rectangle
подклас.
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