1. Polimorfismo: che cos’è e a cosa serve
Se pensi che il polimorfismo sia qualcosa dal mondo dei mutanti Marvel, devo deluderti: in programmazione tutto è molto più tranquillo, ma non meno magico. Il polimorfismo è la capacità di oggetti con implementazioni diverse di reagire in modo diverso alle stesse chiamate di metodi.
Esempio reale:
Hai una classe Book e una classe Magazine, entrambe ereditano dalla classe astratta LibraryItem. Vuoi poter chiamare il metodo printInfo() per qualsiasi elemento della biblioteca, e che esso stampi le informazioni necessarie — per un libro autore e titolo, per una rivista numero di uscita e data.
Esempio di codice:
abstract class LibraryItem {
String title;
LibraryItem(String title) {
this.title = title;
}
abstract void printInfo();
}
class Book extends LibraryItem {
String author;
Book(String title, String author) {
super(title);
this.author = author;
}
@Override
void printInfo() {
System.out.println("Libro: " + title + ", autore: " + author);
}
}
class Magazine extends LibraryItem {
int issueNumber;
Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
void printInfo() {
System.out.println("Rivista: " + title + ", numero: " + issueNumber);
}
}
Ora puoi creare un array di elementi diversi e chiamare printInfo() per ciascuno:
LibraryItem[] items = {
new Book("Il signore delle mosche", "William Golding"),
new Magazine("Scienza e vita", 5)
};
for (LibraryItem item : items) {
item.printInfo();
}
// Stamperà:
// Libro: Il signore delle mosche, autore: William Golding
// Rivista: Scienza e vita, numero: 5
Ecco come funziona il polimorfismo!
2. Errori tipici con il polimorfismo
Tentativo di chiamare metodi che non esistono nel tipo base
Uno degli errori più frequenti è provare a invocare un metodo dichiarato solo nella classe figlia tramite un riferimento al tipo base.
LibraryItem item = new Book("Harry Potter", "J. Rowling");
// item.getAuthor(); // Errore di compilazione! In LibraryItem non esiste il metodo getAuthor()
Java compila il codice in base a ciò che vede nel tipo della variabile (LibraryItem), non nell’oggetto reale (Book). Quindi, se hai bisogno di chiamare un metodo specifico del libro, devi fare il cast:
if (item instanceof Book) {
Book book = (Book) item;
// Ora si può chiamare book.getAuthor()
}
Cast senza verifica
Se sei sicuro che l’oggetto sia un Book, ma in realtà non lo è, otterrai una ClassCastException a runtime. Per esempio:
LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // BOOM! ClassCastException
Modo corretto — controllare sempre il tipo:
if (item instanceof Book) {
Book book = (Book) item;
// OK
} else {
System.out.println("Non è un libro!");
}
Mancato utilizzo dei vantaggi del polimorfismo
A volte gli sviluppatori scrivono codice rigidamente legato a tipi concreti, sebbene si potrebbero usare astrazioni. Per esempio, se scrivi:
Book[] books = ...;
for (Book book : books) {
book.printInfo();
}
Questo funziona solo per i libri. E se domani arrivassero riviste, giornali, fumetti? Meglio usare un array di LibraryItem[] e lavorare con i metodi della classe base o dell’interfaccia.
3. Astrazioni: a cosa servono e come non rovinarle
Classi astratte e interfacce
L’astrazione è l’arte di evidenziare l’essenziale e nascondere i dettagli. In Java ci sono classi astratte e interfacce per questo scopo.
- Classe astratta — una classe che non può essere istanziata direttamente, ma solo ereditata.
- Interfaccia — un contratto: cosa deve saper fare una classe, non come lo fa.
Errore 1: Creare una classe astratta senza metodi astratti
Se nella tua classe astratta non c’è alcun metodo astratto, chiediti: deve davvero essere astratta? Forse è più semplice fare una classe normale?
abstract class UselessAbstract {
void sayHello() {
System.out.println("Hello!");
}
}
// Meglio fare una classe normale se non ci sono metodi astratti
Errore 2: Mancanza dell’implementazione dei metodi obbligatori nei discendenti
Se una classe estende una classe astratta o implementa un’interfaccia, deve implementare tutti i metodi astratti. Se te ne dimentichi, il compilatore te lo ricorderà; ma capita che i metodi vengano implementati “pro forma” e non facciano nulla. Questo è negativo per la manutenzione del codice.
class Magazine extends LibraryItem {
Magazine(String title, int issueNumber) {
super(title);
// ...
}
@Override
void printInfo() {
// Vuoto! Male!
}
}
Errore 3: Gerarchie di astrazioni troppo profonde o contorte
Quando le classi ereditano l’una dall’altra per cinque–dieci livelli, diventa molto difficile orientarsi. Meglio creare gerarchie “piatte”, dove tutto è chiaro.
Esempio negativo:
LibraryItem
|
BookItem
|
PrintedBook
|
IllustratedBook
|
ChildrenIllustratedBook
Complicato, vero? Meglio limitarsi a due o tre livelli.
4. Pratica: applicazione di polimorfismo e astrazioni in un’app didattica
Miglioriamo la tua app didattica per la biblioteca. Prima avevi solo i libri. Ora aggiungiamo le riviste e realizziamo un’interfaccia comune per le pubblicazioni a stampa.
Dichiariamo una classe astratta:
abstract class LibraryItem {
protected String title;
public LibraryItem(String title) {
this.title = title;
}
public abstract void printInfo();
}
Aggiungiamo le classi derivate:
class Book extends LibraryItem {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
@Override
public void printInfo() {
System.out.println("Libro: " + title + ", autore: " + author);
}
}
class Magazine extends LibraryItem {
private int issueNumber;
public Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
public void printInfo() {
System.out.println("Rivista: " + title + ", numero: " + issueNumber);
}
}
Usiamo il polimorfismo:
LibraryItem[] items = {
new Book("Clean Code", "Robert Martin"),
new Magazine("Java World", 3)
};
for (LibraryItem item : items) {
item.printInfo();
}
Aggiungiamo un’interfaccia per le pubblicazioni elettroniche
Supponiamo che alcune pubblicazioni possano essere lette online. Introduciamo un’interfaccia:
interface ReadableOnline {
void openOnline();
}
class EBook extends Book implements ReadableOnline {
private String url;
public EBook(String title, String author, String url) {
super(title, author);
this.url = url;
}
@Override
public void openOnline() {
System.out.println("Apriamo il libro elettronico all'indirizzo: " + url);
}
}
Ora è possibile lavorare con i libri elettronici tramite l’interfaccia:
ReadableOnline ebook = new EBook("Java For Dummies", "Barry Burd", "https://example.com/java");
ebook.openOnline();
5. Come evitare problemi con polimorfismo e astrazioni: best practice
- Usate interfacce e classi astratte per descrivere il comportamento, non lo stato.
Ad esempio, l’interfaccia Printable descrive bene la capacità di “stampa”, mentre mantenere nell’interfaccia un campo String title è già una cattiva idea. - Verificate il tipo dell’oggetto con instanceof prima del cast.
Soprattutto se l’oggetto può essere di tipi diversi. Questo ti proteggerà da ClassCastException. - Puntate a gerarchie “piatte” e comprensibili.
Quanto più semplice è l’albero di ereditarietà, tanto più facile è mantenere ed estendere il codice. - Evitate astrazioni “senza senso”.
Se la classe non contiene metodi astratti e non è destinata a essere ereditata, non renderla astratta. - Usate l’annotazione @Override sempre quando sovrascrivete un metodo.
Aiuta il compilatore a intercettare errori nella firma.
6. Errori tipici nel lavoro con polimorfismo e astrazioni
Errore n. 1: Cast senza verifica
A volte viene voglia di “tagliare la strada” e fare il cast senza verifica. Può funzionare, ma può anche portare a un crash improvviso del programma. Usate sempre instanceof:
if (item instanceof Book) {
Book book = (Book) item;
// ...
}
Errore n. 2: Tentare di chiamare un metodo della classe figlia tramite un riferimento al tipo base
LibraryItem item = new Book("Java", "Autore");
item.getAuthor(); // Errore di compilazione: in LibraryItem non c'è questo metodo!
Soluzione — o fare il cast, oppure aggiungere il metodo necessario alla classe base (se ha senso).
Errore n. 3: Implementazione incompleta di un’interfaccia o di una classe astratta
Se ti dimentichi di implementare tutti i metodi dell’interfaccia, il compilatore non ti permetterà di costruire il progetto. Ma se implementi “stub” che non fanno nulla, ciò porterà a comportamenti inattesi.
Errore n. 4: Gerarchia di ereditarietà troppo profonda
Se hai più di tre livelli di ereditarietà, chiediti se non sia possibile semplificare l’architettura.
Errore n. 5: Violazione del principio della singola responsabilità
Se un’astrazione descrive troppe responsabilità, diventa difficile da mantenere. È meglio suddividere in più interfacce o classi.
GO TO FULL VERSION