Java polymorfism och dess typer

Polymorfism hänvisar till förmågan hos vissa enheter att förekomma i olika former. Det representeras populärt av fjärilen, som förvandlas från larv till puppa till imago. Polymorfism finns också i programmeringsspråk, som en modelleringsteknik som låter dig skapa ett enda gränssnitt till olika operander, argument och objekt. Java polymorfism resulterar i kod som är mer kortfattad och lättare att underhålla.

Medan denna handledning fokuserar på subtyp polymorfism, finns det flera andra typer du borde veta om. Vi börjar med en översikt över alla fyra typer av polymorfism.

ladda ner Skaffa koden Ladda ner källkoden till exempel applikationer i denna handledning. Skapad av Jeff Friesen för JavaWorld.

Typer av polymorfism i Java

Det finns fyra typer av polymorfism i Java:

  1. Tvång är en operation som serverar flera typer genom implicit typkonvertering. Till exempel delar du ett heltal med ett annat heltal eller ett flytpunktsvärde med ett annat flytpunktsvärde. Om en operand är ett heltal och den andra operand är ett flytpunktsvärde, tvingar kompilatorn (implicit konverterar) heltalet till ett flytpunktsvärde för att förhindra ett typfel. (Det finns ingen delningsoperation som stöder ett heltal operand och en floand-point operand.) Ett annat exempel är att skicka en underklassobjektreferens till metodens superklassparameter. Kompilatorn tvingar underklass-typen till superklass-typen för att begränsa operationerna till superklassen.
  2. Överbelastning avser att använda samma operatörssymbol eller metodnamn i olika sammanhang. Du kan till exempel använda för +att utföra heltalstillägg, flytpunktsaddition eller strängkompatering, beroende på vilka typer av dess operander. Flera metoder med samma namn kan också visas i en klass (genom deklaration och / eller arv).
  3. Parametrisk polymorfism föreskriver att inom en klassdeklaration kan ett fältnamn associeras med olika typer och ett metodnamn kan associeras med olika parameter- och returtyper. Fältet och metoden kan sedan ta på sig olika typer i varje klassinstans (objekt). Till exempel kan ett fält vara av typ Double(en medlem av Java: s standardklassbibliotek som slår in ett doublevärde) och en metod kan returnera a Doublei ett objekt, och samma fält kan vara av typ Stringoch samma metod kan returnera a Stringi ett annat objekt . Java stöder parametrisk polymorfism via generika, som jag kommer att diskutera i en framtida artikel.
  4. Undertyp betyder att en typ kan fungera som en annan typs undertyp. När en undertypsinstans visas i en supertypkontext leder exekveringen av en supertypoperation på subtypsinstansen till att subtypens version av den operationen körs. Tänk till exempel på ett fragment av kod som ritar godtyckliga former. Du kan uttrycka denna ritningskod mer koncist genom att introducera en Shapeklass med en draw()metod; genom att införa Circle, Rectangleoch andra underklasser som åsidosätter draw(); genom att införa en typ av array Shapevars element lagrar referenser till Shapesubklassinstanser; och genom att ringa Shape's draw()metod på varje instans. När du ringer draw()är det Circles, Rectangles eller andra Shapeinstanserdraw()metod som blir kallad. Vi säger att det finns många former av Shape's draw()metod.

Denna handledning introducerar subtyp polymorfism. Du lär dig om uppkastning och senbindning, abstrakta klasser (som inte kan instansieras) och abstrakta metoder (som inte kan kallas). Du lär dig också om downcasting och runtime-typidentifiering, och du får en första titt på kovarianta returtyper. Jag sparar parametrisk polymorfism för en framtida handledning.

Ad-hoc vs universell polymorfism

Liksom många utvecklare klassificerar jag tvång och överbelastning som ad hoc-polymorfism och parametrisk och subtyp som universell polymorfism. Medan värdefulla tekniker tror jag inte att tvång och överbelastning är sant polymorfism; de är mer som typomvandlingar och syntaktiskt socker.

Undertyp polymorfism: Upcasting och sen bindning

Undertypspolymorfism är beroende av uppkastning och sen bindning. Upcasting är en form av casting där du kastar upp arvshierarkin från en subtyp till en supertyp. Ingen rolloperatör är inblandad eftersom subtypen är en specialisering av supertypen. Till exempel Shape s = new Circle();upcasts från Circletill Shape. Detta är vettigt eftersom en cirkel är en slags form.

Efter uppkastning Circletill Shapekan du inte ringa Circle-specifika metoder, till exempel en getRadius()metod som returnerar cirkelns radie, eftersom Circle-specifika metoder inte ingår i Shapegränssnittet. Att förlora åtkomst till undertypsfunktioner efter att ha minskat en underklass till dess superklass verkar meningslöst, men är nödvändigt för att uppnå undertypspolymorfism.

Anta att Shapedeklarerar en draw()metod, dess Circleunderklass åsidosätter den här metoden, Shape s = new Circle();har exekverats, och nästa rad specificerar s.draw();. Vilken draw()metod kallas: Shapes draw()metod eller Circleär draw()metoden? Kompilatorn vet inte vilken draw()metod som ska ringas. Allt det kan göra är att verifiera att det finns en metod i superklassen och verifiera att metodanropets argumentlista och returtyp matchar superklassens metoddeklaration. Emellertid infogar kompilatorn också en instruktion i den kompilerade koden som vid körning hämtar och använder vilken referens som helst för satt kalla rätt draw()metod. Denna uppgift kallas sen bindning .

Senbindning vs tidig bindning

Sen bindning används för samtal till icke- finalinstansmetoder. För alla andra metodsamtal vet kompilatorn vilken metod som ska ringas. Den infogar en instruktion i den kompilerade koden som anropar metoden som är associerad med variabelns typ och inte dess värde. Denna teknik är känd som tidig bindning .

Jag har skapat en applikation som visar subtyp polymorfism när det gäller uppkastning och sen bindning. Denna ansökan består av Shape, Circle, Rectangleoch Shapesklasser, där varje klass lagras i sitt eget källfilen. Listning 1 presenterar de tre första klasserna.

Listning 1. Deklarera en hierarki av former

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

Listning 2 visar Shapesapplikationsklassen vars main()metod driver applikationen.

Listning 2. Upcasting och sen bindning i subtyp polymorfism

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

Deklarationen av shapesmatrisen visar uppkastning. Den Circleoch Rectanglereferenser lagras i shapes[0]och shapes[1]och är upcast att skriva Shape. Var shapes[0]och en och shapes[1]betraktas som en Shapeinstans: shapes[0]betraktas inte som en Circle; shapes[1]betraktas inte som en Rectangle.

Sen bindning demonstreras av shapes[i].draw();uttrycket. När iär lika 0, kompilatorn genererade instruktions orsaker Circle's draw()metod anropas. När iär lika 1, dock denna instruktions orsaker Rectangle's draw()metod att kallas. Detta är kärnan i subtyp polymorfism.

Om man antar att alla fyra källfiler ( Shapes.java, Shape.java, Rectangle.javaoch Circle.java) finns i den aktuella katalogen, sammanställa dem via någon av kommando följande rader:

javac *.java javac Shapes.java

Kör den resulterande applikationen:

java Shapes

Följ följande utdata:

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

Abstrakta klasser och metoder

När du utformar klasshierarkier kommer du att upptäcka att klasser närmare toppen av dessa hierarkier är mer generiska än klasser som är lägre. Till exempel är en Vehiclesuperklass mer generisk än en Truckunderklass. På samma sätt är en Shapesuperklass mer generisk än en Circleeller en Rectangleunderklass.

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.

I det här fallet kommer inte kompilatorn att klaga eftersom nedkastning från en superklass till en underklass i samma typhierarki är laglig. Som sagt, om uppdraget var tillåtet skulle applikationen krascha när den försökte utföra subclass.method();. I det här fallet skulle JVM försöka anropa en icke-existerande metod, för Superclassdeklarerar inte method(). Lyckligtvis verifierar JVM att en roll är laglig innan han utför en rolloperation. Om det upptäcks som Superclassinte deklarerar method()skulle det kasta ett ClassCastExceptionföremål. (Jag kommer att diskutera undantag i en framtida artikel.)

Sammanställa listan 5 enligt följande:

javac BadDowncast.java

Kör den resulterande applikationen:

java BadDowncast