Lezione 4 · Canale 1 · giovedì 1 ottobre 2026

Ereditarietà e incapsulamento in Java

Progettazione del Software

Sottoclassi: la gerarchia delle figure

Una classe ne estende un'altra con extends: la classe base (superclasse) fornisce campi e metodi, la classe derivata (sottoclasse) li eredita e può aggiungerne. Per decidere se usare extends ci si chiede se gli oggetti della nuova classe sono un tipo particolare di quelli dell'altra. Se la risposta è sì, si estende.

Figura ha tre sottoclassi, Ellisse, Rettangolo e Triangolo. Un quadrato è un rettangolo con i lati uguali e un cerchio è un'ellisse particolare: Quadrato e Cerchio non hanno campi in più rispetto alla loro superclasse.

Figuradescrizione: stringaEllisseRettangolobase: interoaltezza: interoTriangololato1: interolato2: interolato3: interoCerchioQuadrato
Ogni freccia va dalla sottoclasse alla superclasse: un quadrato è un rettangolo, un cerchio un'ellisse, e tutte sono figure. Quadrato e Cerchio non aggiungono attributi.

Il verso dell'estensione conta. Se Rettangolo estendesse Quadrato, in una variabile di tipo Quadrato si potrebbe mettere un rettangolo qualsiasi.

public class Figura {
    String descrizione;

    public Figura(String descrizione) {
        this.descrizione = descrizione;
    }

    public String toString() {
        return "Figura [descrizione=" + descrizione + "]";
    }
}

Rettangolo aggiunge i campi interi base e altezza. Per l'area basta usare i dati già presenti: un rettangolo di base 2 e altezza 1 ha area 2.

public class Rettangolo extends Figura {
    int base, altezza;

    public Rettangolo(String descrizione, int base, int altezza) {
        super(descrizione);
        this.base = base;
        this.altezza = altezza;
    }

    public double area() {
        return base * altezza;
    }

    public double perimetro() {
        return 2 * (base + altezza);
    }
}

Con lati 4 e 5 il perimetro è 2 (4+5)=182\,(4+5)=18.

Costruttori delle sottoclassi

Un oggetto della sottoclasse contiene anche i campi ereditati, e alcuni possono essere inaccessibili alla sottoclasse. Perciò il suo costruttore chiama un costruttore della superclasse con super(...), che deve essere la prima istruzione.

  • Senza una chiamata esplicita, Java invoca super(), il costruttore senza parametri: se la superclasse non lo ha, c'è un errore di compilazione.
  • Una classe senza costruttori riceve un costruttore standard senza parametri, che chiama super() e lascia gli altri campi al valore di default.

In Triangolo il costruttore standard dà quindi errore, perché Figura ha solo Figura(String descrizione). Va scritto un costruttore con la descrizione e i tre lati.

L'ambiente di sviluppo (Eclipse) genera costruttori e toString a partire dai campi scelti. Per il toString di Rettangolo vanno selezionati anche i campi ereditati, altrimenti la descrizione non compare. Il codice generato risparmia scrittura ripetitiva, ma bisogna saperlo leggere: non sempre è quello voluto, come si vede in Quadrato.

Metodi stub

L'area del triangolo richiede la formula di Erone, che qui non si implementa. Un metodo che dichiara un valore di ritorno ma non restituisce nulla non compila, quindi si inserisce un return provvisorio.

public double area() {
    return 1000;   // stub: da completare
}

Un metodo con l'intestazione ma senza il contenuto vero è uno stub: si può già chiamare. Il valore 1000 è riconoscibile se compare per errore in un risultato. Esistono anche classi stub, con al più qualche metodo vuoto. Gli stub servono per scrivere intanto il codice che usa la classe, oppure per far lavorare in parallelo programmatori o team che dipendono l'uno dalle classi dell'altro.

Dati non validi: controllo a esecuzione

Il costruttore di Triangolo accetta lati 3, 2, 4, con perimetro 9, ma anche 1, 2, 4: compila, si esegue e dà perimetro 7, benché non esista un triangolo con quei lati. Anche i valori negativi sarebbero accettati.

Il compilatore non può controllarlo: i valori possono arrivare da un utente, da un file, da un database o dalla rete, e in generale si conoscono solo a esecuzione. Deve verificarli la classe. Tre lati formano un triangolo se ciascuno, al massimo, è uguale alla somma degli altri due:

l1≤l2+l3l2≤l1+l3l3≤l1+l2\begin{gathered} l_1\le l_2+l_3\\ l_2\le l_1+l_3\\ l_3\le l_1+l_2 \end{gathered}

Con l'uguaglianza il triangolo è schiacciato, di area zero; con un lato maggiore della somma degli altri due i lati non si congiungono. Il controllo, nel costruttore, segnala la violazione di almeno una delle tre condizioni:

public Triangolo(String descrizione, int lato1, int lato2, int lato3) {
    super(descrizione);
    if (lato1 > lato2 + lato3 || lato2 > lato1 + lato3
            || lato3 > lato1 + lato2)
        throw new RuntimeException("Violazione della proprietà triangolare");
    this.lato1 = lato1;
    this.lato2 = lato2;
    this.lato3 = lato3;
}

throw new RuntimeException(...) solleva un'eccezione: il programma si interrompe e la console mostra il messaggio, la classe e la riga che hanno prodotto l'errore. Un errore deve comparire il prima possibile ed essere evidente. In un programma per utenti, invece, si segnalerebbe il dato errato chiedendolo di nuovo. Le eccezioni si approfondiscono più avanti.

private e incapsulamento

Controllare solo il costruttore non basta. Con i campi accessibili, un triangolo valido diventa non valido con t.lato1 = 1, senza alcun controllo. La verifica può stare dove si modificano i dati o dove si usano, per esempio nel calcolo del perimetro. Per garantire che ogni modifica sia controllata, le modifiche devono passare per metodi della classe, e deve essere impossibile aggirarli. A questo serve private. Il controllo, che serve anche negli altri metodi di modifica, va in un metodo a parte, verifica.

public class Triangolo extends Figura {
    private int lato1, lato2, lato3;

    public void modificaLato1(int nuovo) {
        if (!verifica(nuovo, lato2, lato3))
            throw new RuntimeException("Violazione della proprietà triangolare");
        lato1 = nuovo;
    }

    public int getLato1() {
        return lato1;
    }

    private boolean verifica(int a, int b, int c) {
        return a <= b + c && b <= a + c && c <= a + b;
    }
    // costruttore e altri metodi come sopra
}

Un campo o un metodo private ha il nome utilizzabile solo fra la graffa di apertura e quella di chiusura della propria classe. In main, t.lato1 = 1 e anche solo t.lato1 sono errori di compilazione, mentre t.modificaLato1(1) è lecito. Un metodo public, invece, è visibile ai clienti della classe: l'insieme dei membri pubblici è l'interfaccia pubblica, e limitare la visibilità serve all'information hiding.

  • La regola riguarda dove si scrive il nome, non chi conosce il dato: il valore resta leggibile con getLato1(). private non significa riservato.
  • Gli errori di accesso sono rilevati solo in compilazione, perché dipendono da dove è scritto il codice.
  • Il parametro lato1 del costruttore è una variabile diversa dal campo this.lato1: conta il campo, che si può scrivere solo nella classe.
  • Anche una sottoclasse di Triangolo non potrebbe scrivere lato1.

Il problema si ripresenta in Quadrato. Il suo costruttore riceve un solo lato e lo passa due volte alla superclasse; il codice generato per costruttore e toString, con base e altezza, va riscritto.

public class Quadrato extends Rettangolo {
    public Quadrato(String descrizione, int lato) {
        super(descrizione, lato, lato);
    }
}

Con base e altezza modificabili dall'esterno, q.altezza = 12 su un quadrato di lato 2 dà una figura di base 2 e altezza 12, con perimetro 28: non è più un quadrato. La soluzione è l'incapsulamento: campi privati, accessibili solo tramite metodi get (lettura) e set (modifica), che possono aggiungere controlli. Dopo averli resi privati, Quadrato legge l'altezza con getAltezza(). Il controllo che mantiene base e altezza uguali richiede le eccezioni e si completa più avanti.

Oggetti non modificabili e creazione con new

Se i lati si assegnano solo nel costruttore e non esiste alcun metodo di modifica, un triangolo si crea con i lati controllati e poi non cambia più. Chi usa la classe può solo crearne un altro. Le stringhe Java funzionano così: non si cambia un carattere, se ne costruisce una nuova.

Ogni new crea un oggetto nuovo e invoca un costruttore. Assegnare un new a una variabile già usata cambia l'oggetto a cui la variabile si riferisce, non quello vecchio. L'assegnamento valuta prima il lato destro:

new Triangolo(...)Crea l'oggettocampi al valore di defaultEsegue il costruttoreoggetto inizializzatoAssegna a tt
L'assegnazione avviene per ultima. Se il costruttore solleva un'eccezione, il programma termina prima e la variabile t resta com'era.

Insiemi di figure

Tutte le sottoclassi sono figure, quindi in un HashSet di Figura si possono inserire quadrati, rettangoli e triangoli: un oggetto della sottoclasse è anche un oggetto della superclasse.

HashSet<Figura> figure = new HashSet<Figura>();
figure.add(new Quadrato("q", 2));
figure.add(new Rettangolo("r", 4, 5));
figure.add(new Triangolo("t", 3, 2, 4));

Esercizio: calcolare il perimetro totale delle figure dell'insieme. Con una variabile di tipo Figura si usano solo i membri di Figura, che ancora non ha perimetro: la classe va modificata.

Formulario

Gerarchia

  • extends: tipo particolare
  • super(...): prima istruzione
  • sottoclasse: anche superclasse

Rettangolo: base bb, altezza hh

P=2 (b+h)P=2\,(b+h)

Triangolo: lati l1,l2,l3l_1,l_2,l_3

P=l1+l2+l3P=l_1+l_2+l_3

Proprietà triangolare

l1≤l2+l3l2≤l1+l3l3≤l1+l2\begin{gathered} l_1\le l_2+l_3\\ l_2\le l_1+l_3\\ l_3\le l_1+l_2 \end{gathered}

Accesso

  • public: visibile ai clienti
  • private: scrivibile nella classe
  • errori: a compilazione

Incapsulamento

  • campi private
  • accesso con get e set
  • i controlli stanno nei metodi

Eccezioni

  • throw new RuntimeException(...)
  • mostra messaggio e riga
  • errore subito e ben visibile

Oggetti

  • ogni new crea un oggetto
  • prima si valuta il lato destro
  • senza metodi di modifica: fisso