La scorsa settimana abbiamo parlato di ereditarietà in Java e abbiamo visto come non sia un concetto astratto, gioia di qualche modellatore esteta: se usata in modo appropriato apporta vantaggi tangibili nel codice, come centralizzazione, riusabilità e flessibilità. Oggi, in questa nuova lezione del nostro corso completo di programmazione in Java, vedremo un concetto strettamente connesso all’ereditarietà: il polimorfismo.
Overloading e Overriding
Prima di parlare di polimorfismo abbiamo però bisogno di introdurre due concetti propedeutici: l’Overloading e l’Overriding dei metodi.
Overloading
Parliamo di Overloading quando due metodi hanno lo stesso nome, ma diverso numero e/o tipo dei parametri in ingresso. Il valore di ritorno, invece, può essere differente in ognuno dei metodi, ma non necessariamente. Due metodi in Java non possono comunque differire soltanto per il tipo di ritorno.
Vediamo subito un esempio:
public int somma(int a, int b) { return a+b; } public int somma(int a, int b, int c) { return a+b+c; } |
Poiché Java è un linguaggio fortemente tipato è sempre chiaro anche a livello di compilazione capire quale sarà il metodo invocato tra gli n definiti.
Per chi di voi è familiare con C++ questo concetto è ben noto: tra l’altro in C++ è possibile effettuare anche l’overloading degli operatori, feature che in Java si è deciso di non introdurre.
Noi abbiamo già usato l’overloading in modo silente, quando abbiamo definito più costruttori nella classe Impiegato; ebbene sì: il costruttore è un metodo proprio come tutti gli altri, quindi può essere sottoposto ad overloading.
Overriding
Supponiamo di avere una classe A e una classe B che estende A; siamo in una situazione di overriding quando esiste un metodo di B che ha lo stesso nome e identico numero e tipo di parametri in ingresso (identica signature) di un metodo di A e che ne sovrascrive il comportamento. L’overriding entra quindi in gioco soltanto in congiunzione con una situazione di ereditarietà.
Torniamo al nostro esempio sull’Impiegato e il Manager. Aggiungiamo ad Impiegato un metodo che calcola il reddito netto annuo, ipotizzando 13 mensilità:
public class Impiegato { protected double stipendio; ... ... public double getRedditoNettoAnnuo() { return stipendio * 13; } } |
In Manager tale metodo viene ereditato, ma sarebbe non corretto: dobbiamo infatti conteggiare anche il premio produzione. Possiamo risolvere proprio grazie all’overriding:
public class Manager extends Impiegato { private double premioProduzione; ... ... public double getRedditoNettoAnnuo() { return stipendio * 13 + premioProduzione; } } |
Meglio ancora sarebbe invocare il metodo della classe padre, promuovendo il riutilizzo del codice:
public class Manager extends Impiegato { private double premioProduzione; ... ... public double getRedditoNettoAnnuo() { return super.getRedditoNettoAnnuo() + premioProduzione; } } |
Esempio di Polimorfismo
Ipotizziamo di lavorare nel dominio seguente: applicativo CAD di disegno di forme grafiche. Tutte le forme hanno un colore, un colore del bordo e uno spessore del bordo e possono essere disegnate:
import java.awt.Color; public class GraphicObject { protected Color color; protected Color borderColor; protected int border; public Color getColor() { return color; } public void setColor(Color color) { this.color = color; } public Color getBorderColor() { return borderColor; } public void setBorderColor(Color borderColor) { this.borderColor = borderColor; } public int getBorder() { return border; } public void setBorder(int border) { this.border = border; } public void draw() { System.out.println("Non so disegnare una forma"); } } |
Nello specifico avremo diversi tipi di oggetti grafici: per semplicità ipotizziamo che siano soltanto due: il cerchio e il rettangolo. Il primo so disegnarlo quando conosco il suo centro e il suo raggio, il secondo quando conosco le posizioni dei due vertici in alto a sinistra e in basso a destra:
public class Circle extends GraphicObject { private Point center; private double radius; public Point getCenter() { return center; } public void setCenter(Point center) { this.center = center; } public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } @Override public void draw() { System.out.println("Disegno un Cerchio"); } } |
public class Rectangle extends GraphicObject { private Point topLeft; private Point bottomRight; public Point getTopLeft() { return topLeft; } public void setTopLeft(Point topLeft) { this.topLeft = topLeft; } public Point getBottomRight() { return bottomRight; } public void setBottomRight(Point bottomRight) { this.bottomRight = bottomRight; } @Override public void draw() { System.out.println("Disegno un Rettangolo"); } } |
Il comportamento polimorfico si applica per il fatto che esiste un metodo draw che fa parte della classe padre, ma che viene ridefinito nei sottotipi specifici: questo significa che non dovrò avere diversi metodi, drawRectangtle, drawCircle, uno per ogni tipo diverso di oggetto grafico, ma avrò invece un solo metodo draw il cui comportamento cambia in base al tipo a runtime su cui viene invocato; si dice che il comportamento del metodo draw è polimorfico: cambia in base al tipo su cui è invocato.
Un esempio
Vediamo una dimostrazione di quanto detto:
public class GraphicObjectTest { public static void main(String[] args) { GraphicObject cerchio = new Circle(); GraphicObject rettangolo = new Rectangle(); // invoco sempre draw, ma il comportamento cambia // non è necessario dire if cerchio => fai questo // if rettangolo => fai questo cerchio.draw(); rettangolo.draw(); } } |
Se lanciamo il main precedente cosa viene scritto in console?
Otteniamo:
Disegno un Cerchio Disegno un Rettangolo
ovvero un comportamento polimorfico di draw: fa la cosa giusta in base al tipo. Se a qualcuno sembra strano che si possa scrivere:
GraphicObject cerchio = new Circle(); |
ricordiamo che Circle è un (estende) GraphicObject, quindi l’assegnazione di cui sopra ha perfettamente senso, anzi è spesso desiderabile: a tempo di compilazione la variabile cerchio è un GraphicObject, ma a runtime punta a una istanza di tipo Circle.
Conclusioni
Ci fermiamo qua per ora: abbiamo visto come l’ereditarietà, in congiunzione con l’overriding, offre la possibilità di realizzare comportamenti polimorfici, che modellano casi d’uso differenti senza riempire il codice di if (che come voi sapete alzano il colesterolo). Nel prossimo post faremo un ulteriore passo avanti, introducendo le classi astratte e le interfacce.
Alla prossima!