CodeGym /Corsi /JAVA 25 SELF /Problemi del polimorfismo e delle astrazioni

Problemi del polimorfismo e delle astrazioni

JAVA 25 SELF
Livello 23 , Lezione 3
Disponibile

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 nel­l’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.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION