Incapsulamento in Android con Java: guida completa con esempi pratici
L’incapsulamento è uno dei pilastri fondamentali della programmazione orientata agli oggetti (OOP) e viene usato in ogni singola app Android, spesso senza che ce ne accorgiamo. In questo articolo vediamo cos’è, perché è importante nello sviluppo Android con Java e come applicarlo con classi ben progettate, getter/setter, Builder pattern e una mini app pratica “Rubrica Contatti”.
1. Introduzione: cos’è l’incapsulamento
L’incapsulamento consiste nel nascondere i dettagli interni di una classe (dati e logica) e nell’esporre solo un’interfaccia pubblica sicura e controllata. In pratica, invece di accedere direttamente ai campi di un oggetto, interagiamo tramite metodi (getter, setter o metodi di business) che impongono regole e validazioni.
In Android questo è cruciale perché:
- Evita stati incoerenti negli oggetti usati da Activity, Fragment, ViewModel, Adapter, ecc.
- Rende il codice più facile da testare (le regole sono concentrate nelle classi Model).
- Riduce i bug causati da modifiche incontrollate alle variabili condivise fra più componenti.
Dati pubblici vs dati privati
Se un campo è public, può essere letto e modificato da qualsiasi altra classe. Questo rende impossibile controllare come e quando il valore cambia.
Se un campo è private, è accessibile solo all’interno della classe stessa. Le altre classi possono interagire con quel dato solo attraverso metodi pubblici controllati (getter, setter o altri metodi). Questo è il cuore dell’incapsulamento.
2. Modificatori di accesso in Java
Java offre quattro livelli di accesso principali:
| Modificatore | Visibile in | Descrizione |
|---|---|---|
public | Ovunque (stesso package e altri package) | Accessibile da qualsiasi classe. |
protected | Stesso package + sottoclassi (anche in altri package) | Usato spesso per permettere l'override nelle sottoclassi. |
private | Solo all'interno della classe | Massimo livello di protezione. |
| (nessun modificatore) | Solo nello stesso package | Detto anche package-private. |
Esempio di una classe con campi con livelli diversi:
public class EsempioAccesso {
public String nomePubblico; // Accessibile ovunque
protected int valoreProtetto; // Stesso package + sottoclassi
String descrizionePackage; // Package-private (nessun modificatore)
private boolean attivo; // Solo dentro questa classe
public void esempio() {
// Qui posso accedere a tutti i campi
attivo = true;
}
}
Nel contesto Android, useremo soprattutto:
privateper i campi delle classi Model, Adapter, helper, ecc.publicper metodi che rappresentano l'interfaccia dell'oggetto (es.getNome(),aggiungiContatto()).protectedper metodi da sovrascrivere nelle sottoclassi (es. custom View, Activity base, ecc.).
3. Getter e Setter: come e quando usarli
I getter e setter sono metodi pubblici che permettono di leggere e modificare campi privati in modo controllato.
Esempio semplice:
public class Persona {
private String nome;
// Getter
public String getNome() {
return nome;
}
// Setter
public void setNome(String nome) {
this.nome = nome;
}
}
Il vantaggio rispetto ai campi pubblici è che dentro ai setter possiamo aggiungere validazione e logica aggiuntiva.
Esempio con classe Utente con validazione
public class Utente {
// Campi privati: non accessibili dall'esterno direttamente
private String nome;
private String email;
private int eta;
// Costruttore
public Utente(String nome, String email, int eta) {
setNome(nome); // Uso i setter per riutilizzare la validazione
setEmail(email);
setEta(eta);
}
// Getter
public String getNome() {
return nome;
}
public String getEmail() {
return email;
}
public int getEta() {
return eta;
}
// Setter con validazione
public void setNome(String nome) {
if (nome == null || nome.trim().isEmpty()) {
throw new IllegalArgumentException("Il nome non può essere vuoto");
}
this.nome = nome.trim();
}
public void setEmail(String email) {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Email non valida");
}
this.email = email.trim();
}
public void setEta(int eta) {
if (eta < 0 || eta > 120) {
throw new IllegalArgumentException("Età non valida");
}
this.eta = eta;
}
}
Best practice per getter e setter
- Non esporre getter/setter “tanto per”. Esporre solo ciò che serve davvero.
- Mettere logica di validazione nei setter (e/o nel costruttore), non sparsa nelle Activity.
- Usare nomi chiari:
getNome(),setNome(),isAttivo()per boolean. - Evitare di permettere modifiche non necessarie: se un campo non deve cambiare dopo la creazione, non avere il setter (o rendere il campo
final).
4. Classi Model ben progettate: esempio con Prodotto
Le classi Model rappresentano i dati della nostra app (utente, prodotto, contatto, ordine, ecc.). Devono essere semplici, coerenti e ben incapsulate.
Requisiti per la classe Prodotto:
id(String) – identificativo univoco, non vuoto.nome(String) – non vuoto.prezzo(double) – non negativo.quantita(int) – >= 0.
public class Prodotto {
private String id;
private String nome;
private double prezzo;
private int quantita;
public Prodotto(String id, String nome, double prezzo, int quantita) {
setId(id);
setNome(nome);
setPrezzo(prezzo);
setQuantita(quantita);
}
// Getter
public String getId() {
return id;
}
public String getNome() {
return nome;
}
public double getPrezzo() {
return prezzo;
}
public int getQuantita() {
return quantita;
}
// Setter con validazione
public void setId(String id) {
if (id == null || id.trim().isEmpty()) {
throw new IllegalArgumentException("ID prodotto non valido");
}
this.id = id.trim();
}
public void setNome(String nome) {
if (nome == null || nome.trim().isEmpty()) {
throw new IllegalArgumentException("Il nome del prodotto non può essere vuoto");
}
this.nome = nome.trim();
}
public void setPrezzo(double prezzo) {
if (prezzo < 0) {
throw new IllegalArgumentException("Il prezzo non può essere negativo");
}
this.prezzo = prezzo;
}
public void setQuantita(int quantita) {
if (quantita < 0) {
throw new IllegalArgumentException("La quantità non può essere negativa");
}
this.quantita = quantita;
}
// Metodo di utilità
public double calcolaTotale() {
return prezzo * quantita;
}
}
In questo modo qualsiasi Activity, Fragment o Repository che usi Prodotto è sicura che l'oggetto sia in uno stato consistente.
5. Incapsulamento con Builder Pattern: esempio con Notifica
Quando una classe ha molti parametri, usare un costruttore classico può diventare ingestibile:
// Esempio da evitare
Notifica notifica = new Notifica(
"Titolo",
"Messaggio",
true,
false,
"HIGH",
System.currentTimeMillis()
);
È difficile ricordare l'ordine dei parametri e il codice diventa poco leggibile. Il Builder pattern risolve questo problema permettendo di costruire l'oggetto passo per passo, con metodi espliciti (ad esempio per costruire una notifica da mostrare tramite DialogFragment).
Esempio di classe Notifica con Builder
public class Notifica {
// Campi immutabili (solo getter, nessun setter pubblico)
private final String titolo;
private final String messaggio;
private final boolean mostraIcona;
private final boolean vibrazione;
private final String priorita; // es: "LOW", "DEFAULT", "HIGH"
private final long timestamp;
// Costruttore privato: può essere chiamato solo dal Builder
private Notifica(Builder builder) {
this.titolo = builder.titolo;
this.messaggio = builder.messaggio;
this.mostraIcona = builder.mostraIcona;
this.vibrazione = builder.vibrazione;
this.priorita = builder.priorita;
this.timestamp = builder.timestamp;
}
// Getter pubblici (niente setter: oggetto immutabile)
public String getTitolo() {
return titolo;
}
public String getMessaggio() {
return messaggio;
}
public boolean isMostraIcona() {
return mostraIcona;
}
public boolean isVibrazione() {
return vibrazione;
}
public String getPriorita() {
return priorita;
}
public long getTimestamp() {
return timestamp;
}
// Builder statico interno alla classe
public static class Builder {
private String titolo;
private String messaggio;
private boolean mostraIcona = true; // valore di default
private boolean vibrazione = false; // valore di default
private String priorita = "DEFAULT";
private long timestamp = System.currentTimeMillis();
public Builder setTitolo(String titolo) {
if (titolo == null || titolo.trim().isEmpty()) {
throw new IllegalArgumentException("Il titolo non può essere vuoto");
}
this.titolo = titolo.trim();
return this; // permette il chaining
}
public Builder setMessaggio(String messaggio) {
if (messaggio == null || messaggio.trim().isEmpty()) {
throw new IllegalArgumentException("Il messaggio non può essere vuoto");
}
this.messaggio = messaggio.trim();
return this;
}
public Builder setMostraIcona(boolean mostraIcona) {
this.mostraIcona = mostraIcona;
return this;
}
public Builder setVibrazione(boolean vibrazione) {
this.vibrazione = vibrazione;
return this;
}
public Builder setPriorita(String priorita) {
if (priorita == null) {
throw new IllegalArgumentException("La priorità non può essere null");
}
this.priorita = priorita;
return this;
}
public Builder setTimestamp(long timestamp) {
this.timestamp = timestamp;
return this;
}
public Notifica build() {
if (titolo == null) {
throw new IllegalStateException("Titolo obbligatorio");
}
if (messaggio == null) {
throw new IllegalStateException("Messaggio obbligatorio");
}
return new Notifica(this);
}
}
}
Uso della Notifica con il Builder:
Notifica notifica = new Notifica.Builder()
.setTitolo("Nuovo messaggio")
.setMessaggio("Hai ricevuto un nuovo messaggio dalla chat")
.setPriorita("HIGH")
.setVibrazione(true)
.build();
Questo approccio migliora l'incapsulamento perché:
- I campi della classe sono immutabili (solo getter).
- La validazione è centralizzata nel Builder.
- È chiaro quali parametri stiamo impostando.
6. Mini App pratica: Rubrica Contatti
Passiamo ora a un esempio completo di mini app Android: una semplice rubrica che mostra una lista di contatti in una RecyclerView.
6.1 Classe Contatto completamente incapsulata
Requisiti:
nome,cognome– non vuoti.telefono– non vuoto (per semplicità controlliamo solo che non sia vuoto).email– opzionale, ma se presente deve contenere@.
public class Contatto {
private String nome;
private String cognome;
private String telefono;
private String email; // può essere null o vuota
public Contatto(String nome, String cognome, String telefono, String email) {
setNome(nome);
setCognome(cognome);
setTelefono(telefono);
setEmail(email);
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
if (nome == null || nome.trim().isEmpty()) {
throw new IllegalArgumentException("Il nome non può essere vuoto");
}
this.nome = nome.trim();
}
public String getCognome() {
return cognome;
}
public void setCognome(String cognome) {
if (cognome == null || cognome.trim().isEmpty()) {
throw new IllegalArgumentException("Il cognome non può essere vuoto");
}
this.cognome = cognome.trim();
}
public String getTelefono() {
return telefono;
}
public void setTelefono(String telefono) {
if (telefono == null || telefono.trim().isEmpty()) {
throw new IllegalArgumentException("Il telefono non può essere vuoto");
}
this.telefono = telefono.trim();
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
if (email == null || email.trim().isEmpty()) {
// email opzionale: permettiamo null o stringa vuota
this.email = null;
return;
}
if (!email.contains("@")) {
throw new IllegalArgumentException("Email non valida");
}
this.email = email.trim();
}
// Metodo di utilità: restituisce il nome completo
public String getNomeCompleto() {
return nome + " " + cognome;
}
}
6.2 Layout XML con RecyclerView
Layout principale activity_main.xml con una RecyclerView per visualizzare i contatti.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewContatti"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Layout per il singolo elemento della lista item_contatto.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/textNomeCompleto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16sp"
android:text="Nome Cognome" />
<TextView
android:id="@+id/textTelefono"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:text="Telefono" />
<TextView
android:id="@+id/textEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:text="Email"
android:visibility="gone" />
</LinearLayout>
6.3 Adapter: ContattoAdapter con ViewHolder
L'adapter connette la lista di Contatto alla RecyclerView. Ogni ViewHolder visualizza un singolo contatto.
public class ContattoAdapter extends RecyclerView.Adapter<ContattoAdapter.ContattoViewHolder> {
private List<Contatto> contatti;
public ContattoAdapter(List<Contatto> contatti) {
this.contatti = contatti;
}
@NonNull
@Override
public ContattoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.item_contatto, parent, false);
return new ContattoViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ContattoViewHolder holder, int position) {
Contatto contatto = contatti.get(position);
holder.bind(contatto);
}
@Override
public int getItemCount() {
return contatti.size();
}
// ViewHolder incapsula le view del singolo elemento
static class ContattoViewHolder extends RecyclerView.ViewHolder {
private final TextView textNomeCompleto;
private final TextView textTelefono;
private final TextView textEmail;
public ContattoViewHolder(@NonNull View itemView) {
super(itemView);
textNomeCompleto = itemView.findViewById(R.id.textNomeCompleto);
textTelefono = itemView.findViewById(R.id.textTelefono);
textEmail = itemView.findViewById(R.id.textEmail);
}
public void bind(Contatto contatto) {
textNomeCompleto.setText(contatto.getNomeCompleto());
textTelefono.setText(contatto.getTelefono());
// Mostra l'email solo se presente
if (contatto.getEmail() != null) {
textEmail.setVisibility(View.VISIBLE);
textEmail.setText(contatto.getEmail());
} else {
textEmail.setVisibility(View.GONE);
}
}
}
}
6.4 MainActivity: creazione e visualizzazione dei contatti
Esempio di MainActivity che crea qualche contatto (rispettando le regole di incapsulamento) e li mostra in lista.
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerViewContatti;
private ContattoAdapter contattoAdapter;
private List<Contatto> listaContatti;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerViewContatti = findViewById(R.id.recyclerViewContatti);
// Inizializzo la lista di contatti
listaContatti = new ArrayList<>();
// Aggiungo contatti usando il costruttore (che applica la validazione)
listaContatti.add(new Contatto("Mario", "Rossi", "3331234567", "mario.rossi@example.com"));
listaContatti.add(new Contatto("Luisa", "Bianchi", "3339876543", null));
listaContatti.add(new Contatto("Carlo", "Verdi", "3205554433", "carlo.verdi@example.com"));
// Imposto l'adapter
contattoAdapter = new ContattoAdapter(listaContatti);
recyclerViewContatti.setAdapter(contattoAdapter);
// Il layout manager è impostato anche via XML, ma possiamo impostarlo via codice se preferiamo
// recyclerViewContatti.setLayoutManager(new LinearLayoutManager(this));
}
}
In questa mini app:
- La classe
Contattoè completamente incapsulata e valida i dati in ingresso. - L'adapter non si preoccupa di validare: assume che i dati siano già corretti.
- L'Activity crea e gestisce solo oggetti validi.
7. Errori comuni con l'incapsulamento
Nello sviluppo Android è facile cadere in alcuni errori tipici legati all'incapsulamento.
1. Campi pubblici accessibili ovunque
// Esempio da evitare
public class Utente {
public String nome;
public String email;
}
Qualsiasi parte dell'app può modificare nome e email senza alcuna regola, rendendo difficile tracciare bug e garantire la consistenza dei dati.
Soluzione: rendere i campi private e fornire metodi di accesso controllati (getter, setter con validazione o Builder).
2. Getter e setter inutili ovunque
Generare automaticamente getter e setter per tutti i campi, anche quando non servono, è un anti-pattern. Esporre tutto equivale spesso ad avere campi pubblici.
Soluzione: esporre solo ciò che serve davvero al resto del codice. Se un campo è interno all'implementazione, non serve un getter pubblico.
3. Over-engineering
A volte, per applicare l'incapsulamento, si finisce con gerarchie di classi troppo complesse, decine di pattern non necessari e codice difficile da mantenere.
Soluzione: partire da classi semplici con campi privati, costruttori chiari, qualche metodo di utilità e solo il livello di astrazione realmente necessario. Aggiungere complessità solo quando serve (YAGNI).
4. Logica di validazione sparsa nelle Activity
Un altro errore comune è validare i dati solo nelle Activity/Fragment prima di creare gli oggetti, lasciando i Model “stupidi”.
Soluzione: spostare la logica di validazione il più possibile dentro le classi Model (costruttori, setter, Builder). L'UI si occupa solo di raccogliere l'input e mostrare errori all'utente.
8. Conclusione e approfondimenti
L'incapsulamento è alla base di un codice Android robusto, testabile e manutenibile. Usare campi privati, getter/setter con validazione, classi Model ben progettate e, quando serve, pattern come il Builder ti permette di:
- Evitare stati incoerenti negli oggetti usati in tutta l'app.
- Centralizzare la logica di validazione e di business.
- Ridurre i bug e rendere il codice più leggibile.
Nel contesto Android, queste tecniche si applicano quotidianamente a classi come Model per la persistenza (Room, Retrofit DTO), ViewModel, Adapter, helper, ecc. Se vuoi continuare ad approfondire l'OOP in Android, puoi leggere anche gli altri articoli della serie:
- Programmazione ad Oggetti (OOP) in Android
- I 4 Principi OOP in pratica
- Classi astratte vs Interfacce
- Ereditarietà Avanzata in Android
- Polimorfismo in pratica in Android
Applicando in modo coerente l'incapsulamento nei tuoi progetti, vedrai che sarà più semplice evolvere l'app, aggiungere funzionalità e mantenere il codice pulito nel tempo.
Lascia un commento