
I 4 Principi OOP in Pratica: Esempi Java su Android
Conoscere i 4 principi della programmazione ad oggetti (OOP) a livello teorico è una cosa. Saperli applicare nel codice reale di un progetto Android è un’altra. In questa guida esploreremo i principi OOP Java Android, vedendo come incapsulamento, ereditarietà, polimorfismo e astrazione entrano davvero nel tuo codice Java su Android, con esempi concreti e progressivi.
Se ti servono prima le basi teoriche, puoi leggere questo articolo introduttivo: OOP in Android — teoria. Qui invece ci concentriamo sull’uso pratico nel contesto di un’app Android.
1. Incapsulamento nei principi OOP Java Android: proteggi i tuoi dati
L’incapsulamento consiste nel tenere nascosti i dettagli interni di una classe e nel permettere l’accesso ai dati solo tramite metodi controllati. In questo modo eviti che altre parti dell’app modifichino lo stato degli oggetti in modo scorretto. Comprendere i principi OOP Java Android, e in particolare l’incapsulamento, è fondamentale per garantire che i dati rimangano sempre in uno stato coerente e valido. Puoi trovare un ottimo esempio pratico di classe ben incapsulata nella guida su RecyclerView Android: Guida Completa con Adapter, ViewHolder e LayoutManager, dove gli adapter gestiscono dati privati in modo controllato.
Esempio: classe Utente con validazione dell’email
Immagina di gestire il profilo di un utente in un’app. Invece di avere campi pubblici modificabili ovunque, definisci una classe Utente con campi privati e metodi di accesso (getter/setter) che applicano la logica di validazione.
// Modello di dominio per un utente dell'app
public class Utente {
// Campi INCAPSULATI
private String id;
private String nome;
private String email;
public Utente(String id, String nome, String email) {
this.id = id;
this.nome = nome;
setEmail(email); // uso il setter per applicare la validazione
}
public String getId() {
return id;
}
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;
}
public String getEmail() {
return email;
}
// Setter con logica di validazione incapsulata
public void setEmail(String email) {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Email non valida");
}
this.email = email;
}
}
Uso in Android: passare un Utente tra Activity
Nell’app, questa classe diventa il tuo modello di dominio per l’utente loggato. Un modo comune per usarla è passarla tra Activity tramite Intent. Per farlo in modo semplice, puoi far implementare a Utente l’interfaccia Serializable (per casi semplici) o Parcelable (più efficiente).
// Activity di login: crea un Utente incapsulato e lo passa alla HomeActivity
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
Button buttonLogin = findViewById(R.id.buttonLogin);
buttonLogin.setOnClickListener(view -> {
// Dati di esempio, in un caso reale arrivano da un form o da un'API
Utente utente = new Utente("1", "Mario Rossi", "mario.rossi@example.com");
Intent intent = new Intent(LoginActivity.this, HomeActivity.class);
intent.putExtra("utente_loggato", utente); // richiede che Utente implementi Serializable
startActivity(intent);
});
}
}
// Activity di destinazione: recupera l'Utente incapsulato
public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
Utente utente = (Utente) getIntent().getSerializableExtra("utente_loggato");
TextView textBenvenuto = findViewById(R.id.textBenvenuto);
if (utente != null) {
textBenvenuto.setText("Benvenuto, " + utente.getNome());
}
}
}
Grazie all’incapsulamento, sai che ovunque nell’app l’email dell’utente sarà sempre valida, perché ogni modifica deve passare dal setter con la validazione.
Cosa succede senza incapsulamento (errore comune)
Un errore tipico è rendere i campi pubblici — o peggio, usare direttamente una Map per rappresentare l’utente.
// ESEMPIO SBAGLIATO: niente incapsulamento
public class UtenteSbagliato {
public String id;
public String nome;
public String email;
}
// In qualunque punto dell'app qualcuno può fare questo:
UtenteSbagliato u = new UtenteSbagliato();
u.email = "non_è_un_email"; // Nessuna validazione, stato incoerente
Il problema è che non hai nessun punto centralizzato dove garantire che i dati siano sempre coerenti. Debuggare bug legati a dati corrotti diventa molto più difficile.
2. Ereditarietà nell’OOP Java Android: evita la duplicazione del codice
L’ereditarietà ti permette di definire un comportamento comune in una classe base e di riutilizzarlo in più sottoclassi. Le sottoclassi estendono la classe padre e aggiungono (o modificano) solo ciò che serve. Per un esempio pratico di come l’ereditarietà viene usata in un progetto Android completo, puoi consultare la guida Come Creare un Gioco Android in Java, dove le classi di gioco ereditano da classi base comuni.
Esempio: Veicolo, Auto e Moto
Supponiamo di avere diversi tipi di veicoli in un’app di noleggio. Tutti i veicoli hanno alcune proprietà comuni (targa, modello, velocità massima), ma hanno anche comportamenti specifici.
public class Veicolo {
protected String targa;
protected String modello;
public Veicolo(String targa, String modello) {
this.targa = targa;
this.modello = modello;
}
public void avvia() {
Log.d("Veicolo", "Veicolo avviato: " + modello);
}
public void arresta() {
Log.d("Veicolo", "Veicolo arrestato: " + modello);
}
}
public class Auto extends Veicolo {
public Auto(String targa, String modello) {
super(targa, modello); // chiama il costruttore della classe base
}
public void apriBagagliaio() {
Log.d("Auto", "Bagagliaio aperto per: " + modello);
}
}
public class Moto extends Veicolo {
public Moto(String targa, String modello) {
super(targa, modello);
}
public void alzaCavalletto() {
Log.d("Moto", "Cavalletto alzato per: " + modello);
}
}
La logica comune per avviare e arrestare un veicolo vive nella classe Veicolo. Le sottoclassi aggiungono solo ciò che le rende diverse.
Uso in Android: BaseActivity con metodi comuni
In Android, un pattern molto comune è creare una BaseActivity da cui ereditano tutte le tue Activity. Lì metti il codice condiviso (gestione log, progress bar, toast, tracking, ecc.).
// Classe base per tutte le Activity dell'app
public abstract class BaseActivity extends AppCompatActivity {
protected void mostraMessaggio(String messaggio) {
Toast.makeText(this, messaggio, Toast.LENGTH_SHORT).show();
}
protected void logEvento(String tag, String messaggio) {
Log.d(tag, messaggio);
}
}
// Activity concreta che eredita il comportamento comune
public class DettaglioProdottoActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dettaglio_prodotto);
logEvento("DettaglioProdotto", "Activity creata");
Button buttonAcquista = findViewById(R.id.buttonAcquista);
buttonAcquista.setOnClickListener(view ->
mostraMessaggio("Prodotto aggiunto al carrello"));
}
}
Così eviti di copiare Toast.makeText(...) e Log.d(...) in ogni Activity. Se un domani vuoi cambiare il comportamento (es. sostituire Toast con uno Snackbar), lo fai una sola volta in BaseActivity.
Quando NON usare l’ereditarietà
L’ereditarietà è potente ma può creare gerarchie rigide e difficili da estendere. Se una classe non è davvero una specializzazione di un’altra (relazione “è un”), valuta la composizione.
Esempio: invece di far ereditare una classe AnalyticsActivity da BaseActivity solo per avere il tracking, puoi creare una classe separata AnalyticsTracker e farla usare (comporre) da più Activity.
// Composizione: l'Activity usa un oggetto helper invece di ereditare
public class AnalyticsTracker {
public void tracciaEvento(String nome) {
Log.d("Analytics", "Evento: " + nome);
}
}
public class CheckoutActivity extends AppCompatActivity {
private AnalyticsTracker tracker = new AnalyticsTracker();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_checkout);
tracker.tracciaEvento("checkout_aperto");
}
}
3. Polimorfismo nell’OOP Java Android: un’interfaccia, molti comportamenti
Il polimorfismo ti permette di trattare oggetti diversi come se fossero dello stesso tipo, purché implementino una stessa interfaccia o ereditino da una stessa classe base. Chiamerai gli stessi metodi, ma otterrai comportamenti diversi in base al tipo concreto.
Esempio: interfaccia Notificabile
Immagina un sistema di notifiche che può inviare push, email o SMS. Vuoi un punto unico che “chieda” l’invio, senza preoccuparsi del canale specifico.
// Interfaccia comune per tutti i tipi di notifica
public interface Notificabile {
void invia();
}
public class NotificaPush implements Notificabile {
private String titolo;
private String messaggio;
public NotificaPush(String titolo, String messaggio) {
this.titolo = titolo;
this.messaggio = messaggio;
}
@Override
public void invia() {
Log.d("NotificaPush", "Invio push: " + titolo + " - " + messaggio);
// Qui useresti la Firebase Messaging API, ecc.
}
}
public class NotificaEmail implements Notificabile {
private String indirizzo;
private String oggetto;
public NotificaEmail(String indirizzo, String oggetto) {
this.indirizzo = indirizzo;
this.oggetto = oggetto;
}
@Override
public void invia() {
Log.d("NotificaEmail", "Invio email a " + indirizzo + " - Oggetto: " + oggetto);
// Qui useresti un client HTTP verso il tuo backend
}
}
public class NotificaSMS implements Notificabile {
private String numero;
private String testo;
public NotificaSMS(String numero, String testo) {
this.numero = numero;
this.testo = testo;
}
@Override
public void invia() {
Log.d("NotificaSMS", "Invio SMS a " + numero + " - Testo: " + testo);
// Qui useresti l'API di un fornitore SMS
}
}
Uso in Android: chiamare invia() senza conoscere il tipo
Nel codice Android puoi lavorare solo con il tipo Notificabile e decidere il canale in base alle preferenze dell’utente o alla configurazione.
public class NotificheManager {
// Metodo che riceve un qualsiasi Notificabile e lo invia
public void inviaNotifica(Notificabile notificabile) {
notificabile.invia(); // polimorfismo: chiamata uguale, comportamento diverso
}
}
// Esempio di utilizzo in un ViewModel o in un Presenter
public class ProfiloViewModel {
private NotificheManager notificheManager = new NotificheManager();
public void inviaAvvisoPreferito(String tipoPreferito) {
Notificabile notifica;
switch (tipoPreferito) {
case "PUSH":
notifica = new NotificaPush("Titolo", "Messaggio push");
break;
case "EMAIL":
notifica = new NotificaEmail("utente@example.com", "Oggetto email");
break;
case "SMS":
default:
notifica = new NotificaSMS("+39123456789", "Testo SMS");
break;
}
notificheManager.inviaNotifica(notifica);
}
}
Override vs Overload: differenza rapida
Override: una sottoclasse ridefinisce un metodo della classe padre mantenendo la stessa firma (stessi parametri). Serve per fornire un comportamento specifico. Ogni Activity Android fa override di onCreate().
Overload: nella stessa classe definisci più metodi con lo stesso nome ma parametri diversi. Serve per offrire più varianti di utilizzo.
public class Calcolatrice {
// Overload: stesso nome, parametri diversi
public int somma(int a, int b) {
return a + b;
}
public int somma(int a, int b, int c) {
return a + b + c;
}
}
public class MiaActivity extends AppCompatActivity {
// Override: stessa firma di onCreate della classe base
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mia);
}
}
4. Astrazione nell’OOP Java Android: nascondi la complessità
L’astrazione ti permette di concentrarti su cosa fa una classe, non su come lo fa. Definisci un’interfaccia o classe astratta con i metodi che ti servono e rimandi alle implementazioni concrete la gestione dei dettagli. Per approfondire i concetti teorici di astrazione e i principi OOP in generale, puoi consultare la documentazione ufficiale Oracle Java OOP.
Esempio: classe astratta Repository
In molte app Android devi caricare e salvare dati da fonti diverse: database locale (Room), API REST, file, ecc. Puoi definire un Repository astratto che espone le operazioni fondamentali, e creare implementazioni diverse per ogni fonte dati.
// Astrazione: definisce cosa si può fare, non come
public abstract class Repository<T> {
public abstract List<T> carica();
public abstract void salva(List<T> elementi);
}
// Implementazione che usa un database locale (es. Room)
public class DatabaseRepository extends Repository<String> {
private final AppDatabase database;
public DatabaseRepository(AppDatabase database) {
this.database = database;
}
@Override
public List<String> carica() {
// Qui chiameresti i DAO di Room
return database.prodottoDao().caricaNomiProdotti();
}
@Override
public void salva(List<String> elementi) {
// Salva i dati nel DB
database.prodottoDao().salvaNomiProdotti(elementi);
}
}
// Implementazione che usa una API remota
public class ApiRepository extends Repository<String> {
private final ApiClient apiClient;
public ApiRepository(ApiClient apiClient) {
this.apiClient = apiClient;
}
@Override
public List<String> carica() {
// Chiamata HTTP sincrona/asincrona semplificata
return apiClient.recuperaNomiProdotti();
}
@Override
public void salva(List<String> elementi) {
apiClient.inviaNomiProdotti(elementi);
}
}
Uso in Android: ViewModel che non conosce la fonte dati
Nel ViewModel puoi dipendere solo dalla classe astratta Repository. Sarà il livello di configurazione (es. un ViewModelProvider.Factory o un container di dependency injection) a decidere se usare il database o le API.
public class ProdottiViewModel extends ViewModel {
private final Repository<String> repository;
private MutableLiveData<List<String>> prodottiLiveData = new MutableLiveData<>();
public ProdottiViewModel(Repository<String> repository) {
this.repository = repository;
}
public LiveData<List<String>> getProdotti() {
return prodottiLiveData;
}
public void caricaProdotti() {
// In un caso reale dovresti usare un thread secondario / coroutine
List<String> prodotti = repository.carica();
prodottiLiveData.postValue(prodotti);
}
}
La parte UI (Activity/Fragment) parla solo con il ViewModel, il ViewModel parla solo con il Repository astratto. Il resto è nascosto dietro l’astrazione, più facile da testare e sostituire.
Come i 4 principi lavorano insieme (esempio con RecyclerView)
Vediamo ora uno scenario tipico: una schermata che mostra una lista di prodotti tramite RecyclerView. Qui entrano in gioco tutti e quattro i principi:
- Incapsulamento: modello
Prodottocon campi privati e logica di validazione. - Ereditarietà: eventualmente una
BaseActivityper la lista. - Polimorfismo: uso di interfacce per i listener o per differenti sorgenti dati.
- Astrazione:
Repositoryper caricare i prodotti da DB o API senza cambiare il resto del codice.
Modello incapsulato Prodotto
public class Prodotto {
private String id;
private String nome;
private double prezzo;
public Prodotto(String id, String nome, double prezzo) {
this.id = id;
setNome(nome);
setPrezzo(prezzo);
}
public String getId() {
return id;
}
public String getNome() {
return nome;
}
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;
}
public double getPrezzo() {
return prezzo;
}
public void setPrezzo(double prezzo) {
if (prezzo < 0) {
throw new IllegalArgumentException("Il prezzo non può essere negativo");
}
this.prezzo = prezzo;
}
}
Astrazione e polimorfismo nel ProdottoRepository
// Astrazione specifica per Prodotto
public interface ProdottoRepository {
List<Prodotto> caricaProdotti();
}
// Implementazione che carica i dati da un'API remota
public class ProdottoApiRepository implements ProdottoRepository {
private final ApiClient apiClient;
public ProdottoApiRepository(ApiClient apiClient) {
this.apiClient = apiClient;
}
@Override
public List<Prodotto> caricaProdotti() {
// Chiamata HTTP fittizia
return apiClient.caricaProdotti();
}
}
// Implementazione che carica i dati da un database locale
public class ProdottoDatabaseRepository implements ProdottoRepository {
private final AppDatabase database;
public ProdottoDatabaseRepository(AppDatabase database) {
this.database = database;
}
@Override
public List<Prodotto> caricaProdotti() {
return database.prodottoDao().caricaProdotti();
}
}
Activity/ViewModel useranno solo ProdottoRepository. Puoi cambiare l’implementazione (API vs DB) senza toccare la UI.
RecyclerView e polimorfismo nel listener
Per gestire il click su un prodotto, usi un’interfaccia listener. L’Adapter lavorerà solo con il tipo astratto, mentre l’Activity deciderà cosa fare al click.
// Listener astratto per i click sui prodotti
public interface OnProdottoClickListener {
void onProdottoClick(Prodotto prodotto);
}
public class ProdottiAdapter extends RecyclerView.Adapter<ProdottiAdapter.ProdottoViewHolder> {
private List<Prodotto> prodotti;
private OnProdottoClickListener listener;
public ProdottiAdapter(List<Prodotto> prodotti, OnProdottoClickListener listener) {
this.prodotti = prodotti;
this.listener = listener;
}
@NonNull
@Override
public ProdottoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_prodotto, parent, false);
return new ProdottoViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ProdottoViewHolder holder, int position) {
Prodotto prodotto = prodotti.get(position);
holder.bind(prodotto);
}
@Override
public int getItemCount() {
return prodotti.size();
}
class ProdottoViewHolder extends RecyclerView.ViewHolder {
TextView textNome;
TextView textPrezzo;
ProdottoViewHolder(@NonNull View itemView) {
super(itemView);
textNome = itemView.findViewById(R.id.textNomeProdotto);
textPrezzo = itemView.findViewById(R.id.textPrezzoProdotto);
}
void bind(Prodotto prodotto) {
textNome.setText(prodotto.getNome());
textPrezzo.setText(String.valueOf(prodotto.getPrezzo()));
itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onProdottoClick(prodotto);
}
});
}
}
}
Activity che combina tutto
Nell’Activity metti insieme incapsulamento (modello Prodotto), astrazione (repository), polimorfismo (listener) ed eventualmente ereditarietà (se usi BaseActivity).
public class ListaProdottiActivity extends BaseActivity implements OnProdottoClickListener {
private RecyclerView recyclerView;
private ProdottiAdapter adapter;
private ProdottoRepository repository;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_lista_prodotti);
recyclerView = findViewById(R.id.recyclerViewProdotti);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// Qui decidi l'implementazione concreta del repository
repository = new ProdottoApiRepository(new ApiClient());
// repository = new ProdottoDatabaseRepository(AppDatabase.getInstance(this));
List<Prodotto> prodotti = repository.caricaProdotti();
adapter = new ProdottiAdapter(prodotti, this);
recyclerView.setAdapter(adapter);
}
@Override
public void onProdottoClick(Prodotto prodotto) {
// Esempio: apri il dettaglio usando un Intent
Intent intent = new Intent(this, DettaglioProdottoActivity.class);
intent.putExtra("id_prodotto", prodotto.getId());
startActivity(intent);
}
}
Grazie a questi principi OOP, la schermata è più pulita: il modello garantisce dati coerenti, il repository è intercambiabile, la RecyclerView è riutilizzabile, il listener è flessibile.
Se vuoi vedere una mini app completa che applica questo tipo di struttura, dai un’occhiata alla Mini App Gestione Prodotti Android.
Conclusione
Incapsulamento, ereditarietà, polimorfismo e astrazione non sono concetti teorici astratti: sono strumenti concreti per scrivere codice Android Java più pulito, manutenibile e testabile. Applicare i principi OOP Java Android nel codice quotidiano—nel modello Utente, nelle Activity base, nei listener di RecyclerView, nei repository—rappresenta il fondamento di un’architettura robusta e scalabile.
Il passo successivo è metterli davvero in pratica. Puoi farlo costruendo (o studiando) una mini app che li usa insieme in un flusso completo: lista, dettaglio, persistenza, navigazione. Qui trovi un esempio guidato: Mini App Gestione Prodotti Android.
Per approfondire o ripassare alcuni argomenti collegati:
Più scrivi codice con questi principi in mente, più diventeranno naturali. Il tuo obiettivo non è “usare OOP” in astratto, ma costruire schermate Android che siano facili da estendere e da modificare anche tra sei mesi.
Rispondi