Salta al contenuto principale

05 - Astrazione e Contratti: Interfacce e Classi Astratte

Ereditarieta, polimorfismo, classi astratte e interfacce in Java. Default methods, implementazione multipla e quando usare cosa.

Cosa ne pensi di questo articolo?

05 - Astrazione e Contratti: Interfacce e Classi Astratte
JavaFoto:Daniil KomovPexels

In Breve

Ereditarieta, polimorfismo, classi astratte e interfacce in Java. Default methods, implementazione multipla e quando usare cosa.

Astrazione e Contratti in Java

L'astrazione permette di definire "contratti" che le classi devono rispettare, separando il "cosa" dal "come". In Java questo si realizza con classi astratte e interfacce.

Cosa Imparerai

  • Ereditarieta e la parola chiave extends
  • Polimorfismo e override dei metodi
  • Classi astratte e metodi astratti
  • Interfacce e implementazione multipla
  • Default methods nelle interfacce
  • Quando usare classi astratte vs interfacce

Ereditarieta

L'ereditarieta permette a una classe di acquisire attributi e metodi da un'altra classe. La classe che eredita è detta sottoclasse, quella da cui eredita è la superclasse.

Ereditarieta Base
// Superclasse (classe padre)
public class Persona {
    protected String nome;
    protected String cognome;
    protected int eta;

    public Persona(String nome, String cognome, int eta) {
        this.nome = nome;
        this.cognome = cognome;
        this.eta = eta;
    }

    public String getNomeCompleto() {
        return nome + " " + cognome;
    }

    public void presentati() {
        System.out.println("Mi chiamo " + getNomeCompleto());
    }
}

// Sottoclasse (classe figlia) - eredita da Persona
public class Studente extends Persona {
    private int matricola;
    private double mediaVoti;

    public Studente(String nome, String cognome, int eta, int matricola) {
        super(nome, cognome, eta); // chiama costruttore della superclasse
        this.matricola = matricola;
        this.mediaVoti = 0;
    }

    // Metodo aggiuntivo specifico di Studente
    public void studia(String materia) {
        System.out.println(nome + " sta studiando " + materia);
    }

    // Getter
    public int getMatricola() {
        return matricola;
    }
}

Polimorfismo e Override

Il polimorfismo permette di trattare oggetti di classi diverse attraverso un tipo comune. L'override permette di ridefinire un metodo ereditato.

Override dei Metodi
public class Studente extends Persona {
    private int matricola;

    public Studente(String nome, String cognome, int eta, int matricola) {
        super(nome, cognome, eta);
        this.matricola = matricola;
    }

    // Override: ridefinisce il metodo della superclasse
    @Override
    public void presentati() {
        System.out.println("Ciao! Sono " + getNomeCompleto());
        System.out.println("Matricola: " + matricola);
    }
}

public class Docente extends Persona {
    private String materia;

    public Docente(String nome, String cognome, int eta, String materia) {
        super(nome, cognome, eta);
        this.materia = materia;
    }

    @Override
    public void presentati() {
        System.out.println("Buongiorno, sono il Prof. " + cognome);
        System.out.println("Insegno " + materia);
    }
}

// Polimorfismo in azione
Persona[] persone = new Persona[3];
persone[0] = new Studente("Mario", "Rossi", 20, 123456);
persone[1] = new Docente("Luigi", "Verdi", 45, "Programmazione");
persone[2] = new Persona("Anna", "Bianchi", 30);

for (Persona p : persone) {
    p.presentati(); // chiama il metodo corretto per ogni tipo
    System.out.println("---");
}

Regole dell'Override

  • L'annotazione @Override è opzionale ma raccomandata
  • Il metodo deve avere la stessa firma (nome e parametri)
  • Il tipo di ritorno deve essere lo stesso o un sottotipo
  • Non si può ridurre la visibilità (da public a private)
  • Non si possono override metodi final o static

Classi Astratte

Una classe astratta non può essere istanziata direttamente e può contenere metodi astratti (senza implementazione) che le sottoclassi devono implementare.

Classe Astratta
// Classe astratta: non può essere istanziata
public abstract class FormaGeometrica {
    protected String nome;
    protected String colore;

    public FormaGeometrica(String nome, String colore) {
        this.nome = nome;
        this.colore = colore;
    }

    // Metodo concreto: ha implementazione
    public void descrivi() {
        System.out.println("Sono un/una " + nome + " di colore " + colore);
    }

    // Metodi astratti: senza implementazione, le sottoclassi DEVONO implementarli
    public abstract double calcolaArea();
    public abstract double calcolaPerimetro();
}

// Sottoclasse concreta: DEVE implementare tutti i metodi astratti
public class Cerchio extends FormaGeometrica {
    private double raggio;

    public Cerchio(String colore, double raggio) {
        super("cerchio", colore);
        this.raggio = raggio;
    }

    @Override
    public double calcolaArea() {
        return Math.PI * raggio * raggio;
    }

    @Override
    public double calcolaPerimetro() {
        return 2 * Math.PI * raggio;
    }
}

public class Rettangolo extends FormaGeometrica {
    private double base;
    private double altezza;

    public Rettangolo(String colore, double base, double altezza) {
        super("rettangolo", colore);
        this.base = base;
        this.altezza = altezza;
    }

    @Override
    public double calcolaArea() {
        return base * altezza;
    }

    @Override
    public double calcolaPerimetro() {
        return 2 * (base + altezza);
    }
}

// Utilizzo
FormaGeometrica[] forme = {
    new Cerchio("rosso", 5),
    new Rettangolo("blu", 4, 3)
};

for (FormaGeometrica forma : forme) {
    forma.descrivi();
    System.out.printf("Area: %.2f, Perimetro: %.2f%n",
        forma.calcolaArea(), forma.calcolaPerimetro());
}

Interfacce

Un'interfaccia definisce un contratto: un insieme di metodi che le classi implementatrici devono fornire. A differenza delle classi astratte, una classe può implementare multiple interfacce.

Definire Interfacce
// Interfaccia: definisce un contratto
public interface Valutabile {
    // Tutti i metodi sono implicitamente public abstract
    double getVoto();
    String getGiudizio();
}

public interface Stampabile {
    void stampa();
}

public interface Esportabile {
    String esportaJSON();
    String esportaCSV();
}

// Una classe può implementare MULTIPLE interfacce
public class Esame implements Valutabile, Stampabile, Esportabile {
    private String nomeCorso;
    private int voto;
    private boolean lode;

    public Esame(String nomeCorso, int voto, boolean lode) {
        this.nomeCorso = nomeCorso;
        this.voto = voto;
        this.lode = lode && voto == 30;
    }

    // Implementazione Valutabile
    @Override
    public double getVoto() {
        return voto;
    }

    @Override
    public String getGiudizio() {
        if (voto >= 28) return "Ottimo";
        if (voto >= 24) return "Buono";
        if (voto >= 18) return "Sufficiente";
        return "Insufficiente";
    }

    // Implementazione Stampabile
    @Override
    public void stampa() {
        String votoStr = lode ? "30 e Lode" : String.valueOf(voto);
        System.out.println(nomeCorso + ": " + votoStr + " (" + getGiudizio() + ")");
    }

    // Implementazione Esportabile
    @Override
    public String esportaJSON() {
        return String.format("{\"corso\":\"%s\",\"voto\":%d,\"lode\":%b}",
            nomeCorso, voto, lode);
    }

    @Override
    public String esportaCSV() {
        return String.format("%s,%d,%b", nomeCorso, voto, lode);
    }
}

Default Methods (Java 8+)

Metodi Default
public interface Calcolabile {
    double calcola();

    // Default method: ha un'implementazione di default
    default String formattaRisultato() {
        return String.format("%.2f", calcola());
    }

    // Static method nell'interfaccia
    static double arrotonda(double valore, int decimali) {
        double fattore = Math.pow(10, decimali);
        return Math.round(valore * fattore) / fattore;
    }
}

public class MediaVoti implements Calcolabile {
    private int[] voti;

    public MediaVoti(int... voti) {
        this.voti = voti;
    }

    @Override
    public double calcola() {
        int somma = 0;
        for (int v : voti) somma += v;
        return (double) somma / voti.length;
    }

    // Può override il default method se vuole
    @Override
    public String formattaRisultato() {
        return "Media: " + String.format("%.2f", calcola()) + "/30";
    }
}

Classi Astratte vs Interfacce

Confronto

Aspetto Classe Astratta Interfaccia
Ereditarieta Singola (extends) Multipla (implements)
Costruttori Si No
Stato (attributi) Si (qualsiasi) Solo costanti (static final)
Metodi concreti Si Solo default/static
Uso tipico Base comune con stato Contratto di comportamento

Quando usare cosa

  • Classe astratta: Quando hai codice comune da condividere, stato, o una relazione "e un tipo di"
  • Interfaccia: Quando vuoi definire un comportamento che classi diverse possono implementare
  • Entrambi: Puoi usare una classe astratta che implementa interfacce

Esempio Completo: Sistema Universitario

Interfacce
public interface Valutabile {
    double getMedia();
    String getGiudizio();
}

public interface Identificabile {
    String getCodice();
    String getTipo();
}
Classe Astratta Base
public abstract class MembroUniversita implements Identificabile {
    protected String nome;
    protected String cognome;
    protected String codice;

    public MembroUniversita(String nome, String cognome, String codice) {
        this.nome = nome;
        this.cognome = cognome;
        this.codice = codice;
    }

    public String getNomeCompleto() {
        return nome + " " + cognome;
    }

    @Override
    public String getCodice() {
        return codice;
    }

    // Metodo astratto: ogni sottoclasse definisce il suo comportamento
    public abstract void svolgiFunzione();
}
Implementazioni
public class StudenteUniversitario extends MembroUniversita implements Valutabile {
    private int[] voti = new int[20];
    private int numEsami = 0;

    public StudenteUniversitario(String nome, String cognome, int matricola) {
        super(nome, cognome, "S" + matricola);
    }

    public void registraEsame(int voto) {
        if (numEsami < voti.length && voto >= 18 && voto <= 30) {
            voti[numEsami++] = voto;
        }
    }

    @Override
    public String getTipo() {
        return "Studente";
    }

    @Override
    public void svolgiFunzione() {
        System.out.println(getNomeCompleto() + " studia per gli esami");
    }

    @Override
    public double getMedia() {
        if (numEsami == 0) return 0;
        int somma = 0;
        for (int i = 0; i < numEsami; i++) somma += voti[i];
        return (double) somma / numEsami;
    }

    @Override
    public String getGiudizio() {
        double media = getMedia();
        if (media >= 28) return "Eccellente";
        if (media >= 24) return "Buono";
        if (media >= 18) return "Sufficiente";
        return "In corso";
    }
}

public class Professore extends MembroUniversita {
    private String materia;
    private int oreSettimanali;

    public Professore(String nome, String cognome, String codice, String materia) {
        super(nome, cognome, "P" + codice);
        this.materia = materia;
        this.oreSettimanali = 20;
    }

    @Override
    public String getTipo() {
        return "Professore";
    }

    @Override
    public void svolgiFunzione() {
        System.out.println("Prof. " + cognome + " insegna " + materia);
    }

    public void assegnaVoto(StudenteUniversitario studente, int voto) {
        studente.registraEsame(voto);
        System.out.printf("Il Prof. %s ha assegnato %d a %s%n",
            cognome, voto, studente.getNomeCompleto());
    }
}
Test
public class TestUniversita {
    public static void main(String[] args) {
        // Creazione membri
        StudenteUniversitario mario = new StudenteUniversitario("Mario", "Rossi", 123456);
        StudenteUniversitario anna = new StudenteUniversitario("Anna", "Bianchi", 789012);
        Professore profVerdi = new Professore("Luigi", "Verdi", "001", "Programmazione");

        // Assegnazione voti
        profVerdi.assegnaVoto(mario, 28);
        profVerdi.assegnaVoto(mario, 30);
        profVerdi.assegnaVoto(anna, 24);
        profVerdi.assegnaVoto(anna, 26);

        // Polimorfismo: array di tipo base
        MembroUniversita[] membri = {mario, anna, profVerdi};

        System.out.println("\n=== Membri Universita ===");
        for (MembroUniversita m : membri) {
            System.out.println(m.getTipo() + ": " + m.getNomeCompleto());
            System.out.println("Codice: " + m.getCodice());
            m.svolgiFunzione();

            // Verifica se implementa Valutabile
            if (m instanceof Valutabile) {
                Valutabile v = (Valutabile) m;
                System.out.printf("Media: %.2f - %s%n", v.getMedia(), v.getGiudizio());
            }
            System.out.println("---");
        }
    }
}

Conclusione

In questo articolo abbiamo esplorato ereditarieta, polimorfismo, classi astratte e interfacce - i pilastri avanzati della OOP che permettono di creare codice flessibile e riutilizzabile.

Nel prossimo articolo vedremo le API Core di Java: String, StringBuilder, Wrapper classes e la Date/Time API.

Punti Chiave da Ricordare

  • extends: Ereditarieta singola da una classe
  • implements: Implementazione di interfacce (multipla)
  • @Override: Ridefinisce un metodo della superclasse
  • abstract: Metodo senza implementazione, classe non istanziabile
  • interface: Contratto di comportamento senza stato
  • default: Metodo con implementazione nelle interfacce

Domande frequenti

Cos'è l'astrazione in Java?

L'astrazione in Java permette di definire "contratti" che le classi devono rispettare, separando il "cosa" dal "come".

Come si realizza l'astrazione in Java?

L'astrazione in Java si realizza con classi astratte e interfacce.

Cos'è l'ereditarietà in Java?

L'ereditarietà in Java permette a una classe di acquisire attributi e metodi da un'altra classe.

Che differenza c'è tra classi astratte e interfacce in Java?

Una classe astratta non può essere istanziata direttamente e può contenere metodi astratti, mentre un'interfaccia definisce un contratto che le classi implementatrici devono fornire.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti