1. Definizione di incapsulamento
Incapsulamento — uno dei principi fondamentali della programmazione orientata agli oggetti (OOP). In parole semplici, incapsulamento significa “nascondere” le parti interne di un oggetto (i suoi «meccanismi interni») e consentire l’accesso solo tramite «porte» appositamente previste — i metodi pubblici.
Immaginate una moderna macchina da caffè. L’utente vede solo i pulsanti e il display — non ha bisogno di sapere come sono fatti la caldaia, la pompa e i tubi all’interno. Preme «Cappuccino» — e ottiene il risultato. Tutto ciò che sta dentro è nascosto. Questo è l’incapsulamento!
In Java (e in altri linguaggi OOP) l’incapsulamento si ottiene grazie a:
- Nascondimento dei dati — i campi della classe sono dichiarati private (o almeno non public).
- Interfaccia pubblica — “verso l’esterno” si espongono solo i metodi realmente necessari all’utente dell’oggetto.
Schema: come appare l’incapsulamento
+-------------------------------+
| Klass Student |
|-------------------------------|
| - name: String | // campo private
| - age: int | // campo private
|-------------------------------|
| + getName(): String | // metodo public
| + setName(String): void | // metodo public
| + getAge(): int | // metodo public
| + setAge(int): void | // metodo public
+-------------------------------+
Qui il segno - indica private (nascosto), mentre + — public (accessibile dall’esterno).
Che cosa sono getter e setter?
Prima di capire perché serve l’incapsulamento, conosciamo rapidamente getter e setter — metodi speciali che ci aiutano a “dialogare” con i campi privati di una classe.
Getter — un metodo che legge il valore di un campo privato. Di solito si chiama getImyaPolya().
Setter — un metodo che imposta il valore di un campo privato. Di solito si chiama setImyaPolya(znachenie).
Esempio semplice:
public class Student {
private String name; // campo privato - non visibile dall'esterno
// Getter - "dammi il nome dello studente"
public String getName() {
return name;
}
// Setter - "imposta il nome dello studente"
public void setName(String name) {
this.name = name;
}
}
Come funziona:
Student student = new Student();
student.setName("Vasya"); // impostiamo il nome tramite il setter
String name = student.getName(); // otteniamo il nome tramite il getter
Pensate ai getter e ai setter come a «richieste educate» all’oggetto: invece di mettere le mani nelle sue tasche (student.name = "Vasya"), chiediamo gentilmente: «Per favore, imposta il nome» (student.setName("Vasya")).
Anticipando: tra un paio di lezioni studieremo in dettaglio getter e setter, ne scopriremo i segreti e impareremo a usarli al meglio. Per ora basta capirne l’idea di base!
2. Perché serve l’incapsulamento?
Protezione dei dati da usi non corretti
Se tutti i campi di una classe fossero pubblici (public), qualsiasi codice esterno potrebbe modificarne i valori liberamente:
Student s = new Student();
s.age = -1000; // Ops, uno studente vampiro!
Questo è pericoloso! Il vostro programma potrebbe comportarsi in modo imprevedibile, e i bug comparirebbero nei punti più inaspettati.
Possibilità di cambiare l’implementazione interna senza influire sul codice esterno
L’incapsulamento permette di modificare l’interno di una classe senza rompere il codice che la usa. Per esempio, potete cambiare il modo in cui i dati sono memorizzati o aggiungere validazioni nei metodi, e gli utenti della classe non se ne accorgeranno — chiameranno comunque gli stessi metodi.
Migliore leggibilità e manutenibilità del codice
Quando i dettagli interni sono nascosti, l’interfaccia esterna diventa più pulita e chiara. Al programmatore che usa la vostra classe non serve capire come funzioni dentro — basta conoscere quali metodi sono disponibili e cosa fanno.
Esempio dalla vita reale
Pensate a come usate lo smartphone. Non vi chiedete come vengano gestiti i tocchi sullo schermo, com’è fatta la batteria o come funzioni il modulo radio. Richiamate semplicemente le funzioni di cui avete bisogno tramite un’interfaccia comprensibile (icone, pulsanti). Se il produttore cambiasse l’implementazione interna, probabilmente non ve ne accorgereste nemmeno.
Esempio reale nel codice
Immaginiamo di avere una classe BankAccount. Nella versione vecchia del programma il saldo era memorizzato come stringa con punti come separatori, per esempio "1.000.50". Poi gli sviluppatori hanno deciso di memorizzare il saldo come numero double. Se il campo fosse stato pubblico, tutto il vecchio codice che accedeva direttamente a account.balance si sarebbe rotto.
Ma se usiamo l’incapsulamento e nascondiamo il campo esponendo solo i metodi deposit() e getBalance(), il codice esterno non noterà alcun cambiamento:
public class BankAccount {
private double balance; // campo nascosto
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
}
Ora, se domani volessimo memorizzare il saldo per esempio in centesimi (long), ci basterebbe cambiare l’implementazione interna della classe e tutto il resto del codice che chiama deposit() e getBalance() continuerebbe a funzionare come prima.
3. Esempi di cattivo e buon incapsulamento
Esempio negativo: campi pubblici
public class Student {
public String name;
public int age;
}
Problemi di questo approccio:
- Qualsiasi codice può assegnare ai campi valori arbitrari, anche non validi.
- Non c’è possibilità di aggiungere controlli sui dati.
- Se decidete di cambiare tipo o struttura del campo, dovrete modificare tutto il codice che lo usa.
Esempio positivo: campi privati e metodi pubblici
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
// Si può aggiungere un controllo!
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Il nome non può essere vuoto");
}
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
// Verifichiamo che l'età non sia negativa
if (age < 0) {
throw new IllegalArgumentException("L'età non può essere negativa");
}
this.age = age;
}
}
Vantaggi:
- Il codice esterno non può modificare i campi direttamente — solo tramite metodi.
- Si possono aggiungere controlli, log, azioni automatiche (per esempio aggiornare statistiche).
- Se la rappresentazione interna cambia (per esempio l’età viene memorizzata in un altro formato), l’interfaccia esterna rimane la stessa.
Come appare nell’uso
Student s = new Student();
s.setName("Alisa");
s.setAge(20);
System.out.println(s.getName() + ", età: " + s.getAge());
Provate ad assegnare un’età negativa — otterrete un errore già in fase di esecuzione! Il vostro programma è protetto dagli errori banali.
4. Relazione con gli altri principi OOP
L’incapsulamento è la «madre» di tutti gli altri principi dell’OOP. Senza di esso non ci sarebbero ereditarietà, polimorfismo né astrazione. Li studieremo più avanti, ma intanto accenniamoli brevemente:
- Ereditarietà (extends) permette di creare nuove classi basandosi su classi esistenti, estendendone o modificandone il comportamento. Se i dettagli interni della classe fossero esposti, una sottoclasse potrebbe rompere inavvertitamente qualcosa di importante.
- Polimorfismo (capacità di oggetti di classi diverse di reagire agli stessi messaggi in modi differenti) non è possibile senza una chiara separazione tra implementazione interna e interfaccia esterna.
- Astrazione — selezionare solo le caratteristiche essenziali dell’oggetto nascondendo i dettagli. L’incapsulamento aiuta a realizzare l’astrazione nella pratica.
Analogia
Immaginate un’automobile. Al conducente sono accessibili solo volante, pedali e leve — questa è l’interfaccia. Tutto il resto (motore, cambio, elettronica) è nascosto sotto il cofano. Se il conducente potesse controllare direttamente ogni vite del motore, gli incidenti sarebbero molto più frequenti!
5. Esempio pratico: incapsulamento nella nostra applicazione
Continuiamo a sviluppare la nostra applicazione didattica — per esempio, la «Rubrica». Supponiamo di avere una classe Contact che memorizza nome e telefono.
Senza incapsulamento (controesempio):
public class Contact {
public String name;
public String phone;
}
Utilizzo:
Contact contact = new Contact();
contact.name = ""; // Ops! Il nome è vuoto
contact.phone = null; // Il telefono non è impostato
Con incapsulamento (approccio corretto):
public class Contact {
private String name;
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Il nome del contatto non può essere vuoto");
}
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
if (phone == null || phone.isBlank()) {
throw new IllegalArgumentException("Il telefono non può essere vuoto");
}
this.phone = phone;
}
}
Ora il codice esterno non potrà lasciare nome o telefono vuoti:
Contact contact = new Contact();
contact.setName("Ivan");
contact.setPhone("+1-999-123-45-67");
Se si prova a impostare un nome vuoto — il programma genererà un errore.
6. Dettagli utili
Incapsulamento e manutenzione a lungo termine del codice
Quando lavorate a un piccolo progetto didattico, può sembrare di poter fare tutto “in buona fede”: chi mai assegnerà un’età negativa o un nome vuoto? Ma non appena il progetto cresce, arrivano altri sviluppatori e persino voi, dopo un paio di mesi, dimenticate i dettagli dell’implementazione — ed è qui che l’incapsulamento salva dal caos.
- È facile cambiare l’interno della classe — se sarà necessario memorizzare il telefono come oggetto di tipo PhoneNumber e non come stringa, vi basterà cambiare l’implementazione senza toccare il codice esterno.
- Più semplice da testare — se le modifiche passano solo attraverso metodi, è facile tracciare quali dati cambiano e quando.
- Meno bug — protezione da valori non validi e modifiche accidentali.
Domanda: servono sempre getter e setter?
Spesso i principianti pensano: «Se l’incapsulamento sono campi privati e getter/setter pubblici, allora bisogna fare getter e setter per ogni campo!». Non è proprio così.
- A volte un campo deve essere di sola lettura (per esempio, l’identificatore univoco dell’oggetto). Allora fornisci solo il getter.
- A volte un campo non deve essere esposto per nulla — allora non fare né getter né setter.
- Il setter può essere privato, se il campo si può modificare solo all’interno della classe.
Regola d’oro: esponi solo i dati e i metodi che sono realmente necessari al codice esterno.
Visualizzazione: confronto degli approcci
| Approccio | Esempio di accesso al campo | Possibilità di controllo | Sicurezza |
|---|---|---|---|
| campi public | |
No | Bassa |
| campi private + metodi | |
Sì | Alta |
7. Errori tipici nell’uso dell’incapsulamento
Errore n. 1: Tutti i campi della classe sono dichiarati public. Questo è l’errore più comune tra i principianti. Un codice del genere diventa rapidamente ingestibile: chiunque può modificare qualsiasi dato a vostra insaputa. Non fatelo — anche se la tentazione di risparmiare tempo è forte!
Errore n. 2: Getter e setter senza controlli e logica. Se create metodi di accesso, usateli per la validazione: non permettete l’assegnazione di valori non validi. Limitarsi a “copiare” il valore dal parametro al campo non è sempre la scelta migliore.
Errore n. 3: Divulgazione prematura della struttura interna. Se create in anticipo getter/setter per tutti i campi “per ogni evenienza”, rischiate di esporre troppi dettagli che poi sarà difficile modificare.
Errore n. 4: Restituzione diretta di oggetti mutabili. Se un campo è un oggetto mutabile (per esempio una lista), non restituirlo direttamente tramite il getter. Meglio restituire una copia o renderlo immutabile.
GO TO FULL VERSION