Incapsulamento in Java: private, getter, setter ed esempi

L’incapsulamento in Java protegge lo stato di un oggetto e mette a disposizione operazioni controllate. Non consiste soltanto nello scrivere private e generare automaticamente getter e setter: il punto è garantire che l’oggetto rimanga sempre in uno stato valido.

Il problema: campi pubblici e dati incoerenti

class Serbatoio {
    public int capacita;
    public int livello;
}

// Dal codice esterno:
Serbatoio s = new Serbatoio();
s.capacita = 50;
s.livello = -10; // Valore assurdo, ma il compilatore lo consente

L’esempio è volutamente sbagliato: i campi pubblici permettono a qualsiasi codice di assegnare un livello negativo o superiore alla capacità. Il compilatore Java non conosce automaticamente le regole fisiche del serbatoio.

Information hiding e invarianti di classe

L’information hiding consiste nel nascondere i dettagli della rappresentazione interna. Il codice esterno usa l’interfaccia pubblica senza dipendere da come sono memorizzati i dati. Un’invariante è una condizione che ogni stato valido dell’oggetto deve rispettare. Per il serbatoio scegliamo capacita > 0 e 0 <= livello <= capacita.

Esempio completo: Serbatoio incapsulato

class Serbatoio {
    private final int capacita;
    private int livello;

    public Serbatoio(int capacita, int livelloIniziale) {
        if (capacita <= 0) {
            throw new IllegalArgumentException("Capacita non valida");
        }
        if (livelloIniziale < 0 || livelloIniziale > capacita) {
            throw new IllegalArgumentException("Livello iniziale non valido");
        }
        this.capacita = capacita;
        this.livello = livelloIniziale;
    }

    public void rifornisci(int litri) {
        if (litri <= 0 || litri > capacita - livello) {
            throw new IllegalArgumentException("Rifornimento non valido");
        }
        livello += litri;
    }

    public void preleva(int litri) {
        if (litri <= 0 || litri > livello) {
            throw new IllegalArgumentException("Prelievo non valido");
        }
        livello -= litri;
    }

    public int getLivello() {
        return livello;
    }

    public int getCapacita() {
        return capacita;
    }
}

public class Main {
    public static void main(String[] args) {
        Serbatoio s = new Serbatoio(50, 20);
        s.rifornisci(10);                // Livello: 30
        s.preleva(8);                    // Livello: 22
        System.out.println(s.getLivello()); // 22
    }
}

La classe è responsabile di verificare le condizioni prima di cambiare il livello. Gli attributi non possono essere modificati direttamente da Main. Il campo capacita è anche final: dopo la costruzione non può essere riassegnato.

Tabella di traccia e stato valido

OperazioneLivello primaEsitoLivello dopo
new Serbatoio(50,20)–Costruzione valida20
rifornisci(10)20Accettata30
preleva(8)30Accettata22
preleva(30)22Rifiutata: litri insufficienti22
rifornisci(40)22Rifiutata: supera la capacità22
Le operazioni non valide non modificano lo stato. Le righe rifiutate indicano cosa accadrebbe se tentassimo quelle chiamate in una gestione che intercetta l’eccezione.

Nel programma Java riportato sopra le operazioni rifiutate lanciano IllegalArgumentException. Se l’eccezione non viene gestita, il flusso normale si interrompe. Se viene intercettata dal codice client, il serbatoio continua a conservare il valore precedente.

Getter e setter: quali servono davvero?

Un getter restituisce informazioni sull’oggetto, ad esempio getLivello(). Un setter chiede di modificare un campo, ma deve rispettare i vincoli dell’oggetto. In questa classe non abbiamo creato setLivello(): il livello cambia solo tramite rifornisci() e preleva(), che esprimono operazioni significative del problema.

Modificatori di accesso in Java

ModificatoreAccesso essenziale
publicDa tutto il codice che può accedere alla classe
privateNella classe che dichiara il membro (incluse le regole Java per classi annidate)
Nessun modificatore (package-private)Dalle classi dello stesso package
protectedDallo stesso package e dalle sottoclassi con le regole previste da Java
Di norma i campi di stato vengono dichiarati private.

Esercizi con soluzione

  • Controllo del costruttore: cosa succede con new Serbatoio(40, 60)?
  • Precondizione: un serbatoio con capacità 60 e livello 55 può ricevere 6 litri?
  • Progettazione: aggiungi un metodo isVuoto() che restituisca un boolean senza cambiare il livello.
  • Errore di progetto: spiega perché rendere pubblico livello rende inefficaci le verifiche nei metodi.
  • Estensione: definisci una classe Magazzino con quantita privata e metodi carica e scarica.

Soluzioni sintetiche

Nel primo esercizio il costruttore rifiuta lo stato perché 60 supera la capacità 40. Nel secondo, il rifornimento va rifiutato: 55 + 6 supera 60. Il metodo isVuoto() può restituire livello == 0. Il campo pubblico vanificherebbe i controlli perché il client potrebbe assegnare valori non validi senza chiamare i metodi.

Nel percorso OOP: Torna all’indice delle lezioni Java · Ripassa classi e oggetti.