09. Corso completo di Programmazione Java – Introduzione all’Object Oriented Programming – Parte IV

Introduzione all'Object Oriented Programming - Parte 4 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!

By Manuele Piastra

Sono un Project Manager i cui skill tecnici sono focalizzati al momento sullo sviluppo di applicazioni JavaEE (application server IBM Websphere 7.0 utilizzando JSF (RichFaces), JPA (EclipseLink) ed EJB3) ed Android. Presso OmniaGroup ricopro il ruolo di Training Manager: seleziono il personale tecnico, mi occupo della sua crescita formativa organizzando Corsi e Workshop sia interni che esterni, molti dei quali hanno visto me come docente. Ho tenuto sette sessioni di un corso di formazione di 50 ore sulle tecnologie JavaEE su Application Server WebSphere IBM presso il nostro cliente principale, Coop Italia, dei cui sistemi informativi sono consulente di riferimento. Sono blogger su CoseNonJaviste, blog tecnico che si occupa di Android e Java. Consulta il mio Curriculum online per maggiori informazioni.