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:

  1. 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.
  2. 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à).
  3. 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 un doublevalore) e un metodo potrebbe restituire a Doublein un oggetto e lo stesso campo potrebbe essere di tipo Stringe lo stesso metodo potrebbe restituire a Stringin un altro oggetto . Java supporta il polimorfismo parametrico tramite generici, di cui parlerò in un prossimo articolo.
  4. 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 Shapeclasse con un draw()metodo; introducendo Circle, Rectanglee altre sottoclassi che sovrascrivono draw(); introducendo un array di tipo i Shapecui elementi memorizzano i riferimenti alle Shapeistanze di sottoclassi; e chiamando Shapeil draw()metodo di su ogni istanza. Quando chiami draw(), è la Circles, Rectangleo un'altra Shapeistanzadraw()metodo che viene chiamato. Diciamo che ci sono molte forme di Shape's draw()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 Circlea Shape. Questo ha senso perché un cerchio è una sorta di forma.

Dopo l'upcast Circlea Shape, non è possibile chiamare Circlemetodi -specific, come un getRadius()metodo che restituisce il raggio del cerchio, perché i Circlemetodi -specific non fanno parte Shapedell'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 Shapedichiari un draw()metodo, che la sua Circlesottoclasse 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 sper 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 finalmetodi 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, Rectanglee Shapesle 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 Shapesclasse 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é Superclassnon dichiara method(). Fortunatamente, la JVM verifica che un cast sia legale prima di eseguire un'operazione di cast. Rilevando che Superclassnon dichiara method(), verrebbe lanciato un ClassCastExceptionoggetto. (Discuterò delle eccezioni in un prossimo articolo.)

Compilare il listato 5 come segue:

javac BadDowncast.java

Esegui l'applicazione risultante:

java BadDowncast