Polimorfismo di Java e suoi tipi
Il polimorfismo si riferisce alla capacità di alcune entità di manifestarsi in forme diverse. È popolarmente rappresentato dalla farfalla, che si trasforma da larva a pupa a imago. Il polimorfismo esiste anche nei linguaggi di programmazione, come tecnica di modellazione che consente di creare un'unica interfaccia per vari operandi, argomenti e oggetti. Il polimorfismo Java produce un codice più conciso e più facile da mantenere.
Sebbene questo tutorial si concentri sul polimorfismo del sottotipo, ci sono molti altri tipi che dovresti conoscere. Inizieremo con una panoramica di tutti e quattro i tipi di polimorfismo.
download Ottieni il codice Scarica il codice sorgente per applicazioni di esempio in questo tutorial. Creato da Jeff Friesen per JavaWorld.Tipi di polimorfismo in Java
Esistono quattro tipi di polimorfismo in Java:
- La coercizione è un'operazione che serve più tipi tramite la conversione di tipo implicito. Ad esempio, dividi un numero intero per un altro numero intero o un valore a virgola mobile per un altro valore a virgola mobile. Se un operando è un numero intero e l'altro operando è un valore in virgola mobile, il compilatore esercita una coercizione (implicitamente convertiti) il numero intero per un valore a virgola mobile per impedire un errore di tipo. (Non esiste alcuna operazione di divisione che supporti un operando intero e un operando a virgola mobile.) Un altro esempio è il passaggio di un riferimento a un oggetto di sottoclasse al parametro della superclasse di un metodo. Il compilatore obbliga il tipo di sottoclasse al tipo di superclasse per limitare le operazioni a quelle della superclasse.
- Il sovraccarico si riferisce all'uso dello stesso simbolo di operatore o nome di metodo in contesti diversi. Ad esempio, è possibile utilizzarlo
+
per eseguire addizioni di interi, addizioni in virgola mobile o concatenazioni di stringhe, a seconda dei tipi di operandi. Inoltre, più metodi con lo stesso nome possono apparire in una classe (tramite dichiarazione e / o ereditarietà). - Il polimorfismo parametrico stabilisce che all'interno di una dichiarazione di classe, un nome di campo può essere associato a diversi tipi e un nome di metodo può essere associato a diversi parametri e tipi restituiti. Il campo e il metodo possono quindi assumere diversi tipi in ciascuna istanza di classe (oggetto). Ad esempio, un campo potrebbe essere di tipo
Double
(un membro della libreria di classi standard di Java che racchiude undouble
valore) e un metodo potrebbe restituire aDouble
in un oggetto e lo stesso campo potrebbe essere di tipoString
e lo stesso metodo potrebbe restituire aString
in un altro oggetto . Java supporta il polimorfismo parametrico tramite generici, di cui parlerò in un prossimo articolo. - Sottotipo significa che un tipo può fungere da sottotipo di un altro tipo. Quando un'istanza di sottotipo appare in un contesto di supertipo, l'esecuzione di un'operazione di supertipo sull'istanza di sottotipo determina l'esecuzione della versione del sottotipo di quell'operazione. Si consideri ad esempio un frammento di codice che disegna forme arbitrarie. Puoi esprimere questo codice di disegno in modo più conciso introducendo una
Shape
classe con undraw()
metodo; introducendoCircle
,Rectangle
e altre sottoclassi che sovrascrivonodraw()
; introducendo un array di tipo iShape
cui elementi memorizzano i riferimenti alleShape
istanze di sottoclassi; e chiamandoShape
ildraw()
metodo di su ogni istanza. Quando chiamidraw()
, è laCircle
s,Rectangle
o un'altraShape
istanzadraw()
metodo che viene chiamato. Diciamo che ci sono molte forme diShape
'sdraw()
metodo.
Questo tutorial introduce il polimorfismo del sottotipo. Imparerai l'upcasting e l'associazione tardiva, le classi astratte (che non possono essere istanziate) e i metodi astratti (che non possono essere chiamati). Imparerai anche il downcasting e l'identificazione del tipo di runtime e darai una prima occhiata ai tipi restituiti covarianti. Salverò il polimorfismo parametrico per un futuro tutorial.
Polimorfismo ad-hoc vs universale
Come molti sviluppatori, classifico la coercizione e il sovraccarico come polimorfismo ad-hoc e parametrico e sottotipo come polimorfismo universale. Sebbene siano tecniche preziose, non credo che la coercizione e il sovraccarico siano un vero polimorfismo; sono più simili a conversioni di tipo e zucchero sintattico.
Sottotipo polimorfismo: Upcasting e late binding
Il polimorfismo del sottotipo si basa sull'upcasting e sul binding tardivo. L'upcasting è una forma di casting in cui esegui il cast della gerarchia di ereditarietà da un sottotipo a un supertipo. Nessun operatore di cast è coinvolto perché il sottotipo è una specializzazione del supertipo. Ad esempio, Shape s = new Circle();
upcast da Circle
a Shape
. Questo ha senso perché un cerchio è una sorta di forma.
Dopo l'upcast Circle
a Shape
, non è possibile chiamare Circle
metodi -specific, come un getRadius()
metodo che restituisce il raggio del cerchio, perché i Circle
metodi -specific non fanno parte Shape
dell'interfaccia di. Perdere l'accesso alle caratteristiche del sottotipo dopo aver ristretto una sottoclasse alla sua superclasse sembra inutile, ma è necessario per ottenere il polimorfismo del sottotipo.
Supponiamo che Shape
dichiari un draw()
metodo, che la sua Circle
sottoclasse sovrascriva questo metodo, che Shape s = new Circle();
sia appena stato eseguito e che la riga successiva specifichi s.draw();
. Quale draw()
metodo si chiama: Shape
's draw()
metodo o Circle
' s draw()
metodo? Il compilatore non sa quale draw()
metodo chiamare. Tutto ciò che può fare è verificare che un metodo esista nella superclasse e verificare che l'elenco degli argomenti della chiamata al metodo e il tipo restituito corrispondano alla dichiarazione del metodo della superclasse. Tuttavia, il compilatore inserisce anche un'istruzione nel codice compilato che, in fase di esecuzione, recupera e utilizza qualsiasi riferimento sia s
per chiamare il draw()
metodo corretto . Questa attività è nota come associazione tardiva .
Legame tardivo vs legame precoce
L'associazione tardiva viene utilizzata per le chiamate a final
metodi non di istanza. Per tutte le altre chiamate al metodo, il compilatore sa quale metodo chiamare. Inserisce un'istruzione nel codice compilato che chiama il metodo associato al tipo di variabile e non il suo valore. Questa tecnica è nota come associazione anticipata .
Ho creato un'applicazione che dimostra il polimorfismo del sottotipo in termini di upcasting e late binding. Questa applicazione è costituita da Shape
, Circle
, Rectangle
e Shapes
le classi, in cui ogni classe è memorizzato in un file di origine. Il listato 1 presenta le prime tre classi.
Listato 1. Dichiarazione di una gerarchia di forme
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 + ")"); } }
Il Listato 2 presenta la Shapes
classe dell'applicazione il cui main()
metodo guida l'applicazione.
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
.
In questo caso il compilatore non si lamenterà perché il downcasting da una superclasse a una sottoclasse nella stessa gerarchia di tipo è legale. Detto questo, se l'assegnazione fosse consentita, l'applicazione andrebbe in crash durante il tentativo di esecuzione subclass.method();
. In questo caso la JVM tenterebbe di chiamare un metodo inesistente, perché Superclass
non dichiara method()
. Fortunatamente, la JVM verifica che un cast sia legale prima di eseguire un'operazione di cast. Rilevando che Superclass
non dichiara method()
, verrebbe lanciato un ClassCastException
oggetto. (Discuterò delle eccezioni in un prossimo articolo.)
Compilare il listato 5 come segue:
javac BadDowncast.java
Esegui l'applicazione risultante:
java BadDowncast