☕ Lezione 8

Polimorfismo - Molte Forme, Un'Interfaccia

Polimorfismo: Il Potere della Flessibilità

🎯 Obiettivi della lezione

💡 Cos'è il Polimorfismo?

Il polimorfismo (dal greco "molte forme") permette di trattare oggetti di classi diverse attraverso un'interfaccia comune.

In pratica: un riferimento a una superclasse può puntare a oggetti di qualsiasi sottoclasse.

Esempio del mondo reale: Tutti i telecomandi hanno pulsanti play/stop, ma ogni telecomando controlla dispositivi diversi (TV, stereo, DVD). L'interfaccia è la stessa, ma il comportamento cambia!

📝 Esempio Base

// Superclasse
public class Animale {
    protected String nome;
    
    public Animale(String nome) {
        this.nome = nome;
    }
    
    public void emettiVerso() {
        System.out.println("L'animale emette un verso");
    }
    
    public void mangia() {
        System.out.println(nome + " sta mangiando");
    }
}

// Sottoclasse 1
public class Cane extends Animale {
    public Cane(String nome) {
        super(nome);
    }
    
    @Override
    public void emettiVerso() {
        System.out.println(nome + " dice: Bau bau! 🐕");
    }
    
    public void scodinzola() {
        System.out.println(nome + " scodinzola felice!");
    }
}

// Sottoclasse 2
public class Gatto extends Animale {
    public Gatto(String nome) {
        super(nome);
    }
    
    @Override
    public void emettiVerso() {
        System.out.println(nome + " dice: Miao! 🐱");
    }
    
    public void faiLeFusa() {
        System.out.println(nome + " fa le fusa... purr purr");
    }
}

🎨 Polimorfismo in Azione!

public class TestPolimorfismo {
    public static void main(String[] args) {
        // POLIMORFISMO: riferimenti di tipo Animale che puntano a sottoclassi
        Animale animale1 = new Cane("Fido");
        Animale animale2 = new Gatto("Micio");
        Animale animale3 = new Cane("Rex");
        
        // Posso chiamare metodi della superclasse
        animale1.emettiVerso();  // Chiama la versione di Cane!
        animale2.emettiVerso();  // Chiama la versione di Gatto!
        animale3.emettiVerso();  // Chiama la versione di Cane!
        
        System.out.println();
        
        animale1.mangia();
        animale2.mangia();
        
        // animale1.scodinzola();  // ERRORE! Il riferimento è di tipo Animale
        // animale2.faiLeFusa();   // ERRORE! Il riferimento è di tipo Animale
    }
}
Fido dice: Bau bau! 🐕 Micio dice: Miao! 🐱 Rex dice: Bau bau! 🐕 Fido sta mangiando Micio sta mangiando

✨ Magia del Polimorfismo:

Anche se i riferimenti sono di tipo Animale, quando chiamiamo emettiVerso(), Java chiama la versione corretta in base all'oggetto REALE (Cane o Gatto)!

Questo si chiama binding dinamico o late binding.

📦 Array Polimorfici

Il polimorfismo diventa potentissimo con array e collezioni:

public class TestArrayPolimorfici {
    public static void main(String[] args) {
        // Array di Animale che contiene oggetti diversi!
        Animale[] zoo = new Animale[5];
        
        zoo[0] = new Cane("Fido");
        zoo[1] = new Gatto("Micio");
        zoo[2] = new Cane("Rex");
        zoo[3] = new Gatto("Felix");
        zoo[4] = new Cane("Pluto");
        
        System.out.println("=== Tutti gli animali emettono il loro verso ===");
        for (Animale animale : zoo) {
            animale.emettiVerso();  // Chiama il metodo corretto per ogni tipo!
        }
        
        System.out.println("\n=== Tutti gli animali mangiano ===");
        for (Animale animale : zoo) {
            animale.mangia();
        }
    }
}
=== Tutti gli animali emettono il loro verso === Fido dice: Bau bau! 🐕 Micio dice: Miao! 🐱 Rex dice: Bau bau! 🐕 Felix dice: Miao! 🐱 Pluto dice: Bau bau! 🐕 === Tutti gli animali mangiano === Fido sta mangiando Micio sta mangiando Rex sta mangiando Felix sta mangiando Pluto sta mangiando

🔍 instanceof - Controllare il Tipo

L'operatore instanceof verifica se un oggetto è un'istanza di una certa classe.

Sintassi: oggetto instanceof NomeClasse

Restituisce true o false

public class TestInstanceOf {
    public static void main(String[] args) {
        Animale[] animali = {
            new Cane("Fido"),
            new Gatto("Micio"),
            new Cane("Rex")
        };
        
        for (Animale animale : animali) {
            animale.emettiVerso();
            
            // Controllo il tipo e chiamo metodi specifici
            if (animale instanceof Cane) {
                Cane cane = (Cane) animale;  // Cast
                cane.scodinzola();
            } else if (animale instanceof Gatto) {
                Gatto gatto = (Gatto) animale;  // Cast
                gatto.faiLeFusa();
            }
            
            System.out.println();
        }
    }
}
Fido dice: Bau bau! 🐕 Fido scodinzola felice! Micio dice: Miao! 🐱 Micio fa le fusa... purr purr Rex dice: Bau bau! 🐕 Rex scodinzola felice!

🔄 Casting di Oggetti

Due tipi di casting:

Upcasting (automatico): da sottoclasse a superclasse

Cane cane = new Cane("Fido");
Animale animale = cane;  // Automatico!

Downcasting (esplicito): da superclasse a sottoclasse

Animale animale = new Cane("Fido");
Cane cane = (Cane) animale;  // Serve il cast esplicito!

⚠️ Attenzione al downcasting!

Il downcasting può causare un ClassCastException se sbagliamo tipo:

Animale animale = new Gatto("Micio");
Cane cane = (Cane) animale;  // ERRORE A RUNTIME!

Usa sempre instanceof prima del cast!

🎮 Esempio Pratico: Sistema di Pagamento

// Superclasse astratta
public abstract class MetodoPagamento {
    protected double importo;
    
    public MetodoPagamento(double importo) {
        this.importo = importo;
    }
    
    // Metodo astratto - ogni sottoclasse lo implementa
    public abstract void effettuaPagamento();
    
    public void mostraRiepilogo() {
        System.out.println("Importo da pagare: €" + importo);
    }
}

// Sottoclasse 1
public class CartaCredito extends MetodoPagamento {
    private String numeroCartanumero;
    private String intestatario;
    
    public CartaCredito(double importo, String numeroCarta, String intestatario) {
        super(importo);
        this.numeroCarta = numeroCarta;
        this.intestatario = intestatario;
    }
    
    @Override
    public void effettuaPagamento() {
        System.out.println("Pagamento con Carta di Credito");
        System.out.println("Intestatario: " + intestatario);
        System.out.println("Carta: **** **** **** " + numeroCarta.substring(12));
        System.out.println("Importo: €" + importo);
        System.out.println("✓ Pagamento autorizzato!");
    }
}

// Sottoclasse 2
public class PayPal extends MetodoPagamento {
    private String email;
    
    public PayPal(double importo, String email) {
        super(importo);
        this.email = email;
    }
    
    @Override
    public void effettuaPagamento() {
        System.out.println("Pagamento con PayPal");
        System.out.println("Account: " + email);
        System.out.println("Importo: €" + importo);
        System.out.println("✓ Pagamento completato!");
    }
}

// Sottoclasse 3
public class Contanti extends MetodoPagamento {
    private double importoRicevuto;
    
    public Contanti(double importo, double importoRicevuto) {
        super(importo);
        this.importoRicevuto = importoRicevuto;
    }
    
    @Override
    public void effettuaPagamento() {
        System.out.println("Pagamento in Contanti");
        System.out.println("Importo dovuto: €" + importo);
        System.out.println("Importo ricevuto: €" + importoRicevuto);
        double resto = importoRicevuto - importo;
        if (resto > 0) {
            System.out.println("Resto: €" + resto);
        }
        System.out.println("✓ Pagamento ricevuto!");
    }
}
public class TestPagamenti {
    public static void main(String[] args) {
        // Array polimorfico di metodi di pagamento
        MetodoPagamento[] pagamenti = new MetodoPagamento[3];
        
        pagamenti[0] = new CartaCredito(150.00, "1234567812345678", "Mario Rossi");
        pagamenti[1] = new PayPal(89.99, "mario.rossi@email.com");
        pagamenti[2] = new Contanti(25.50, 30.00);
        
        // Elaboro tutti i pagamenti allo stesso modo!
        for (int i = 0; i < pagamenti.length; i++) {
            System.out.println("=== Pagamento " + (i+1) + " ===");
            pagamenti[i].mostraRiepilogo();
            pagamenti[i].effettuaPagamento();
            System.out.println();
        }
        
        // Calcolo totale incassato
        double totale = 0;
        for (MetodoPagamento p : pagamenti) {
            totale += p.importo;
        }
        System.out.println("Totale incassato: €" + totale);
    }
}
=== Pagamento 1 === Importo da pagare: €150.0 Pagamento con Carta di Credito Intestatario: Mario Rossi Carta: **** **** **** 5678 Importo: €150.0 ✓ Pagamento autorizzato! === Pagamento 2 === Importo da pagare: €89.99 Pagamento con PayPal Account: mario.rossi@email.com Importo: €89.99 ✓ Pagamento completato! === Pagamento 3 === Importo da pagare: €25.5 Pagamento in Contanti Importo dovuto: €25.5 Importo ricevuto: €30.0 Resto: €4.5 ✓ Pagamento ricevuto! Totale incassato: €265.49

🎯 Vantaggi del Polimorfismo

✅ Perché usare il polimorfismo?

  • Codice più flessibile: posso aggiungere nuove sottoclassi senza modificare il codice esistente
  • Codice più pulito: evito lunghe catene di if-else
  • Interfaccia uniforme: tratto oggetti diversi allo stesso modo
  • Estensibilità: facile aggiungere nuove funzionalità
  • Manutenibilità: modifiche localizzate nelle singole classi

🔧 Metodi Polimorfici

public class GestioneAnimali {
    // Metodo polimorfico - accetta qualsiasi Animale
    public static void faiEmettereVerso(Animale animale) {
        System.out.println("Facciamo emettere il verso a " + animale.nome);
        animale.emettiVerso();
    }
    
    // Metodo che lavora con array di Animale
    public static void curaAnimali(Animale[] animali) {
        System.out.println("=== Cura degli animali ===");
        for (Animale animale : animali) {
            System.out.println("Curando " + animale.nome + "...");
            animale.mangia();
            animale.emettiVerso();
            System.out.println();
        }
    }
    
    public static void main(String[] args) {
        Cane cane = new Cane("Fido");
        Gatto gatto = new Gatto("Micio");
        
        // Posso passare sia Cane che Gatto!
        faiEmettereVerso(cane);
        faiEmettereVerso(gatto);
        
        System.out.println();
        
        Animale[] animali = {
            new Cane("Rex"),
            new Gatto("Felix"),
            new Cane("Pluto")
        };
        
        curaAnimali(animali);
    }
}

✏️ Esercizi Guidati

Esercizio 1: Forme Geometriche Polimorfiche

MEDIO

Obiettivo: Usare il polimorfismo con forme geometriche.

Requisiti:

  1. Crea classe astratta Forma con:
    • Metodo astratto: calcolaArea()
    • Metodo astratto: calcolaPerimetro()
  2. Crea sottoclassi: Cerchio, Rettangolo, Triangolo
  3. Nel main, crea array di Forma con oggetti diversi
  4. Calcola e stampa area totale di tutte le forme

Esercizio 2: Strumenti Musicali

MEDIO

Obiettivo: Simulare un'orchestra con polimorfismo.

Requisiti:

  • Classe astratta StrumentoMusicale con metodo suona()
  • Sottoclassi: Chitarra, Pianoforte, Batteria
  • Ogni strumento suona() in modo diverso
  • Crea metodo concerti(StrumentoMusicale[] orchestra)

💪 Esercizi da Svolgere

Esercizio 3: Veicoli in Movimento

FACILE

Crea Veicolo → Auto/Moto/Bici con:

  • Metodo astratto muovi()
  • Array di veicoli
  • Fai muovere tutti i veicoli

Esercizio 4: Sistema Notifiche

MEDIO

Crea Notifica → Email/SMS/Push con:

  • Metodo invia()
  • Usa polimorfismo per inviare diverse notifiche
  • Conta quante notifiche di ogni tipo

Esercizio 5: Dipendenti e Stipendi

DIFFICILE

Crea Dipendente → Manager/Impiegato/Stagista con:

  • Metodo calcolaStipendio() diverso per ciascuno
  • Array di dipendenti misti
  • Calcola stipendio totale aziendale
  • Trova dipendente con stipendio massimo

🎯 Concetti Chiave da Ricordare

  • Polimorfismo = molte forme, un'interfaccia
  • Riferimento superclasse → oggetto sottoclasse
  • Binding dinamico: metodo scelto a runtime
  • instanceof verifica il tipo
  • Upcasting automatico, downcasting esplicito
  • Array polimorfici contengono oggetti di tipi diversi
  • Rende il codice flessibile ed estensibile

📊 Quando Usare il Polimorfismo

✅ Situazioni ideali:

  • Collezioni di oggetti di tipi diversi ma correlati
  • Quando vuoi trattare oggetti diversi in modo uniforme
  • Per rendere il codice estensibile (nuove classi senza modificare esistente)
  • Nei pattern di design (Strategy, Factory, etc.)