Java-polymorfisme en zijn typen

Polymorfisme verwijst naar het vermogen van sommige entiteiten om in verschillende vormen voor te komen. Het wordt in de volksmond vertegenwoordigd door de vlinder, die verandert van larve naar pop naar imago. Polymorfisme komt ook voor in programmeertalen, als een modelleertechniek waarmee u een enkele interface voor verschillende operanden, argumenten en objecten kunt maken. Java-polymorfisme resulteert in code die beknopter en gemakkelijker te onderhouden is.

Hoewel deze tutorial zich richt op subtype polymorfisme, zijn er verschillende andere typen die u moet kennen. We beginnen met een overzicht van alle vier soorten polymorfisme.

download Download de code Download de broncode voor voorbeeldtoepassingen in deze tutorial. Gemaakt door Jeff Friesen voor JavaWorld.

Soorten polymorfisme in Java

Er zijn vier soorten polymorfisme in Java:

  1. Dwang is een operatie die meerdere typen bedient door middel van impliciete type-conversie. U deelt bijvoorbeeld een geheel getal door een ander geheel getal of een drijvende-kommawaarde door een andere drijvende-kommawaarde. Als de ene operand een geheel getal is en de andere operand een drijvende- kommawaarde , zal de compiler het gehele getal coersen (impliciet converteren) naar een drijvende- kommawaarde om een typefout te voorkomen. (Er is geen deelbewerking die een operand met gehele getallen en een operand met drijvende komma ondersteunt.) Een ander voorbeeld is het doorgeven van een verwijzing naar een subklasseobject naar de superklasseparameter van een methode. De compiler dwingt het type subklasse naar het type superklasse om bewerkingen te beperken tot die van de superklasse.
  2. Overbelasting verwijst naar het gebruik van hetzelfde operatorsymbool of dezelfde methodenaam in verschillende contexten. U kunt bijvoorbeeld gebruiken +om gehele getallen toe te voegen, optellen met drijvende komma of tekenreeksen samen te voegen, afhankelijk van de typen operanden. Ook kunnen meerdere methoden met dezelfde naam in een klasse voorkomen (via declaratie en / of overerving).
  3. Parametrisch polymorfisme bepaalt dat binnen een klassendeclaratie een veldnaam kan associëren met verschillende typen en een methodenaam kan associëren met verschillende parameter- en retourtypen. Het veld en de methode kunnen dan verschillende typen aannemen in elke klasse-instantie (object). Een veld kan bijvoorbeeld van het type zijn Double(een lid van Java's standaard klassenbibliotheek dat een doublewaarde omhult ) en een methode kan een Doublein het ene object retourneren , en hetzelfde veld kan van het type zijn Stringen dezelfde methode kan een Stringin een ander object retourneren . Java ondersteunt parametrisch polymorfisme via generieke geneesmiddelen, die ik in een toekomstig artikel zal bespreken.
  4. Subtype betekent dat een type kan dienen als het subtype van een ander type. Wanneer een subtype-instantie in een supertype-context verschijnt, resulteert het uitvoeren van een supertype-bewerking op de subtype-instantie in de uitvoering van de subtype-versie van die bewerking. Beschouw bijvoorbeeld een codefragment dat willekeurige vormen tekent. U kunt deze tekencode beknopter uitdrukken door een Shapeklasse met een draw()methode te introduceren ; door de introductie van Circle, Rectangleen andere subklassen die voorrang hebben draw(); door een array van typen te introduceren Shapewaarvan de elementen verwijzingen naar Shapesubklasse-instanties opslaan ; en door Shapede draw()methode van elk exemplaar aan te roepen . Wanneer u belt draw(), is het de Circle's, Rectangle' s of een andere Shapeinstantiedraw()methode die wordt aangeroepen. We zeggen dat er vele vormen van Shape's draw()-methode.

Deze tutorial introduceert subtype polymorfisme. Je leert over upcasting en late binding, abstracte klassen (die niet kunnen worden geïnstantieerd) en abstracte methoden (die niet kunnen worden aangeroepen). Je leert ook over downcasting en identificatie van het runtime-type, en je krijgt een eerste blik op covariante retourtypes. Ik zal parametrisch polymorfisme bewaren voor een toekomstige tutorial.

Ad-hoc versus universeel polymorfisme

Zoals veel ontwikkelaars classificeer ik dwang en overbelasting als ad-hoc polymorfisme, en parametrisch en subtype als universeel polymorfisme. Hoewel het waardevolle technieken zijn, geloof ik niet dat dwang en overbelasting echt polymorfisme zijn; ze lijken meer op typeconversies en syntactische suiker.

Subtype polymorfisme: upcasting en late binding

Subtype polymorfisme is afhankelijk van upcasting en late binding. Upcasting is een vorm van casten waarbij je de overervingshiërarchie van een subtype naar een supertype ophaalt. Er is geen cast-operator bij betrokken omdat het subtype een specialisatie is van het supertype. Bijvoorbeeld Shape s = new Circle();upcasts van Circlenaar Shape. Dit is logisch omdat een cirkel een soort vorm is.

Na upcasting Circlenaar Shape, kun je Circle-specifieke methoden niet aanroepen , zoals een getRadius()methode die de straal van de cirkel retourneert, omdat Circle-specifieke methoden geen deel uitmaken van Shapede interface van. De toegang tot subtype-eigenschappen verliezen na het versmallen van een subklasse tot zijn superklasse lijkt zinloos, maar is noodzakelijk om subtype polymorfisme te bereiken.

Stel dat Shapeeen draw()methode wordt Circlegedeclareerd , de subklasse ervan overschrijft deze methode, Shape s = new Circle();is net uitgevoerd en de volgende regel specificeert s.draw();. Welke draw()methode heet: Shape's draw()methode of Circle' s draw()methode? De compiler weet niet welke draw()methode hij moet aanroepen. Het enige wat het kan doen is controleren of een methode bestaat in de superklasse, en controleren of de argumentenlijst en het retourneringstype van de methode-aanroep overeenkomen met de methode-declaratie van de superklasse. De compiler voegt echter ook een instructie in de gecompileerde code in die tijdens runtime elke referentie ophaalt en gebruikt som de juiste draw()methode aan te roepen . Deze taak staat bekend als late binding .

Late binding versus vroege binding

Late binding wordt gebruikt voor aanroepen naar niet- finalinstantiemethoden. Voor alle andere methodeaanroepen weet de compiler welke methode moet worden aangeroepen. Het voegt een instructie in de gecompileerde code in die de methode aanroept die is gekoppeld aan het type van de variabele en niet de waarde ervan. Deze techniek staat bekend als vroege binding .

Ik heb een applicatie gemaakt die polymorfisme van het subtype demonstreert in termen van upcasting en late binding. Deze applicatie bestaat uit Shape, Circle, Rectangle, en Shapesklassen, waarbij elke klasse wordt opgeslagen in een eigen bronbestand. Lijst 1 presenteert de eerste drie klassen.

Listing 1. Een hiërarchie van vormen declareren

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 + ")"); } }

Listing 2 geeft de Shapesapplicatieklasse weer waarvan de main()methode de applicatie aanstuurt.

Lijst 2. Upcasting en late binding in subtype polymorfisme

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(); } }

De declaratie van de shapesarray toont upcasting. De Circleen Rectangleverwijzingen worden in shapes[0]en shapes[1]en zijn upcast te typen Shape. Elk van shapes[0]en shapes[1]wordt beschouwd als een Shapeinstantie: shapes[0]wordt niet beschouwd als een Circle; shapes[1]wordt niet beschouwd als een Rectangle.

Late binding wordt aangetoond door de shapes[i].draw();uitdrukking. Indien igelijk aan 0, zorgt de door de compiler gegenereerde instructie ervoor dat Circlede draw()methode wordt aangeroepen. Wanneer echter igelijk is 1, zorgt deze instructie ervoor dat Rectanglede draw()methode wordt aangeroepen. Dit is de essentie van subtype polymorfisme.

Ervan uitgaande dat alle vier de bronbestanden ( Shapes.java, Shape.java, Rectangle.javaen Circle.java) bevinden zich in de huidige directory, compileren via een van de volgende opdracht regels:

javac *.java javac Shapes.java

Voer de resulterende applicatie uit:

java Shapes

Let op de volgende output:

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

Abstracte klassen en methoden

Wanneer u klassehiërarchieën ontwerpt, zult u merken dat klassen die zich boven aan deze hiërarchieën bevinden, generieker zijn dan klassen die lager staan. Een Vehiclesuperklasse is bijvoorbeeld algemener dan een Trucksubklasse. Evenzo is een Shapesuperklasse algemener dan een Circleof een Rectanglesubklasse.

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.

In dit geval zal de compiler niet klagen omdat downcasting van een superklasse naar een subklasse in dezelfde hiërarchie legaal is. Dat gezegd hebbende, als de toewijzing was toegestaan, zou de applicatie crashen wanneer deze probeerde uit te voeren subclass.method();. In dit geval zou de JVM proberen een niet-bestaande methode aan te roepen, want Superclassdeclareert niet method(). Gelukkig controleert de JVM of een cast legaal is voordat een cast-operatie wordt uitgevoerd. Als je dat Superclassniet detecteert method(), zou het een ClassCastExceptionobject werpen . (Ik zal uitzonderingen bespreken in een toekomstig artikel.)

Stel Listing 5 als volgt samen:

javac BadDowncast.java

Voer de resulterende applicatie uit:

java BadDowncast