Classi Astratte vs Interfacce in Android Java: guida pratica
Capire bene la differenza tra classi astratte e interfacce è fondamentale per scrivere codice Android pulito, riutilizzabile e facile da testare. Classi astratte vs interfacce in Android Java rappresenta una scelta architetturale cruciale: entrambe servono per definire contratti e strutture comuni, ma vengono usate in situazioni diverse e hanno regole precise. In questo articolo vediamo cosa sono, quando usarle e costruiamo una mini app di esempio completa in Java.

In poche parole: una classe astratta è una base comune con stato e logica parziale, mentre un’interfaccia definisce solo cosa una classe deve saper fare, senza specificare come farlo. Nel mondo Android, inoltre, troverai entrambe continuamente: da Activity e ViewModel (classi) fino a OnClickListener e vari Callback (interfacce). In altri termini, la scelta tra le due dipende dal tipo di astrazione che vuoi modellare.
1. Cosa sono le Classi Astratte in Android Java?
Una classe astratta è una classe che non può essere istanziata direttamente. In particolare, può contenere:
- Metodi astratti (senza implementazione)
- Metodi concreti (con implementazione)
- Variabili di istanza (stato)
- Costruttore (usato dalle sottoclassi)
In Android, quindi, le usi quando vuoi definire una base comune per un insieme di schermate, componenti o logica, lasciando però alcuni dettagli alle sottoclassi.
Esempio: BaseActivity astratta
Ad esempio, immagina di avere più Activity che condividono la stessa logica di setup (toolbar, log, tracking, ecc.). In questo caso, puoi creare una classe astratta BaseActivity da estendere in tutte le schermate, evitando così duplicazioni di codice.
public abstract class BaseActivity extends AppCompatActivity {
// Stato condiviso tra tutte le Activity che estendono questa base
protected String screenName;
public BaseActivity(String screenName) {
this.screenName = screenName;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupToolbar();
logScreenView();
}
private void setupToolbar() {
// Logica comune per la toolbar
}
private void logScreenView() {
// Tracking comune per tutte le schermate
}
// Metodo astratto: ogni Activity figlia deve implementarlo
protected abstract void setupUi();
}
public class MainActivity extends BaseActivity {
public MainActivity() {
super("MainActivity");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setupUi();
}
@Override
protected void setupUi() {
// Inizializza le view specifiche di MainActivity
}
}
Principio OOP dimostrato: Template Method e riuso tramite ereditarietà. In sintesi, la logica comune sta nella classe astratta, mentre i dettagli sono delegati alle sottoclassi.
2. Cosa sono le Interfacce in Android Java?
Una interfaccia definisce un insieme di metodi (e, nelle versioni moderne di Java, anche metodi di default e statici), ma non contiene stato di istanza. Inoltre, una classe può implementare più interfacce, permettendo così una forma di “ereditarietà multipla” dei comportamenti, cosa che invece non è possibile con le classi astratte.
In Android, in particolare, le interfacce sono usatissime per:
- Gestire callback (es. risposta di una richiesta di rete)
- Definire listener (es. click su un item di lista)
- Separare contratto e implementazione (es. MVP/MVVM)
Esempio: OnItemClickListener per una RecyclerView
Un caso classico è definire una interfaccia per gestire il click sugli elementi di una lista.
public interface OnItemClickListener {
// Contratto: chi implementa questa interfaccia deve gestire il click
void onItemClick(int position);
}
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
private List<String> items;
private OnItemClickListener listener;
public MyAdapter(List<String> items, OnItemClickListener listener) {
this.items = items;
this.listener = listener;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_simple, parent, false);
return new MyViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
String item = items.get(position);
holder.textView.setText(item);
holder.itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onItemClick(position);
}
});
}
@Override
public int getItemCount() {
return items.size();
}
static class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.textView);
}
}
}
Principio OOP dimostrato: Disaccoppiamento tramite interfacce. In altri termini, l’adapter non conosce i dettagli di cosa succede al click; sa solo che chiamerà un metodo definito in un contratto. Di conseguenza, il codice risulta più flessibile e facilmente testabile.
3. Confronto diretto: Classi Astratte vs Interfacce
Vediamo ora un confronto diretto tra classi astratte e interfacce, con un occhio agli scenari Android. Comprendere bene le differenze tra classi astratte vs interfacce in Android Java è essenziale per fare scelte progettuali consapevoli e scrivere codice più manutenibile.
| Caratteristica | Classe Astratta | Interfaccia |
|---|---|---|
| Costruttore | Sì, può avere costruttori (chiamati dalle sottoclassi). | No, niente costruttori. |
| Stato (variabili di istanza) | Sì, può avere campi con stato condiviso. | No (solo costanti public static final). |
| Metodi con implementazione | Sì (metodi concreti) + metodi astratti. | Sì, da Java 8 tramite default, ma usati più raramente in Android Java. |
| Ereditarietà multipla | No: una classe può estendere solo una classe (astratta o concreta). | Sì: una classe può implementare più interfacce. |
| Uso tipico in Android | Basi comuni per Activity, ViewModel, repository, componenti con stato. | Listener, callback, contratti di funzionalità (es. Parcelable). |
| Quando preferirla | Quando hai logica e stato comune da condividere. | Quando vuoi solo definire un contratto, senza imporre implementazione o stato. |
4. Quando usare una Classe Astratta in Android Java (con esempi)
Usa una classe astratta quando:
- Hai stato condiviso tra più implementazioni (es. un saldo, un token, una configurazione).
- Hai parte della logica uguale per tutte le sottoclassi, ma alcuni passi devono essere personalizzati.
- Vuoi fornire metodi helper comuni alle sottoclassi.
Esempio: sistema di pagamento con classe astratta PaymentMethod
Supponiamo di avere diversi tipi di pagamento (carta, PayPal, contanti) che condividono alcune proprietà (nome del metodo, saldo disponibile) e alcune regole comuni (controllo del saldo, logging). Tuttavia, differiscono nel modo in cui il pagamento viene effettivamente elaborato. Per questo motivo, una classe astratta è la scelta ideale.
public abstract class PaymentMethod {
protected String nomeMetodo;
protected double saldoDisponibile;
public PaymentMethod(String nomeMetodo, double saldoDisponibile) {
this.nomeMetodo = nomeMetodo;
this.saldoDisponibile = saldoDisponibile;
}
public String getNomeMetodo() {
return nomeMetodo;
}
public double getSaldoDisponibile() {
return saldoDisponibile;
}
// Template method: passo comune + passo specifico
public boolean paga(double importo) {
if (!haSaldoSufficiente(importo)) {
return false;
}
boolean successo = elaboraPagamento(importo);
if (successo) {
saldoDisponibile -= importo;
}
return successo;
}
private boolean haSaldoSufficiente(double importo) {
return saldoDisponibile >= importo;
}
// Ogni metodo di pagamento deve implementare questo metodo
protected abstract boolean elaboraPagamento(double importo);
}
Principio OOP dimostrato: Template Method e riuso di stato e logica comune nelle sottoclassi. In aggiunta, questo approccio garantisce che la logica di validazione del saldo sia centralizzata e non duplicata in ogni implementazione.
5. Quando usare un’Interfaccia in Android Java (con esempi)
Usa una interfaccia quando:
- Vuoi definire cosa una classe sa fare, non come lo fa.
- Hai bisogno di più contratti su una stessa classe (es.
Pagabile,Serializable,Parcelable). - Vuoi disaccoppiare componenti che si parlano tramite callback.
Esempio: interfaccia Pagabile
Definiamo quindi una interfaccia generica per qualsiasi cosa che possa effettuare un pagamento:
public interface Pagabile {
boolean paga(double importo);
}
Qualsiasi classe (servizio, metodo di pagamento, simulatore, mock per i test) potrà implementare Pagabile e sarà dunque sostituibile nei punti in cui il codice richiede un oggetto pagabile. Di conseguenza, il codice diventa più modulare e facile da testare.
Esempio: OnClickListener personalizzato
Le interfacce sono inoltre la base di tutti i listener Android, come View.OnClickListener. Pertanto, definire interfacce simili nel tuo codice aiuta a mantenere le responsabilità ben separate.
6. Mini App Android Java: Sistema di Pagamento con Classi Astratte e Interfacce
Costruiamo ora una mini app che mostra come usare insieme una interfaccia e una classe astratta in un contesto reale Android. In particolare, vedremo come combinarle per modellare un sistema di pagamento completo e ben strutturato.
- Interfaccia
Pagabilecon metodopaga(double importo) - Classe astratta
MetodoPagamentocon stato condiviso (saldo,nome) e metodo astrattoelaboraPagamento() - Classi concrete:
PagamentoCarta,PagamentoPayPal,PagamentoContanti RecyclerViewcon lista metodi di pagamentoAlertDialogper conferma pagamentoMainActivityche orchestra tutto- Layout XML:
activity_main.xml,item_pagamento.xml,dialog_pagamento.xml
1. Interfaccia Pagabile in Android Java
Questa interfaccia definisce il contratto per qualsiasi oggetto che può effettuare un pagamento. In altri termini, chiunque voglia essere “pagabile” dovrà implementare questo metodo.
// Principio OOP: astrazione del comportamento
public interface Pagabile {
/**
* Effettua un pagamento.
* @param importo l'importo da pagare
* @return true se il pagamento è andato a buon fine, false altrimenti
*/
boolean paga(double importo);
}
2. Classe Astratta MetodoPagamento in Android Java
Questa classe astratta implementa Pagabile e fornisce stato e logica comune a tutti i metodi di pagamento. Inoltre, definisce un metodo astratto che ogni sottoclasse dovrà implementare con la propria logica specifica.
// Principio OOP: ereditarietà + Template Method
public abstract class MetodoPagamento implements Pagabile {
protected String nome;
protected double saldo;
public MetodoPagamento(String nome, double saldo) {
this.nome = nome;
this.saldo = saldo;
}
public String getNome() {
return nome;
}
public double getSaldo() {
return saldo;
}
@Override
public boolean paga(double importo) {
// Logica comune a tutti i metodi di pagamento
if (importo <= 0) {
return false;
}
if (saldo < importo) {
return false;
}
boolean successo = elaboraPagamento(importo);
if (successo) {
saldo -= importo;
}
return successo;
}
/**
* Ogni metodo di pagamento deve definire come viene effettivamente elaborato
* il pagamento (chiamata API, stampa scontrino, ecc.).
*/
protected abstract boolean elaboraPagamento(double importo);
}
3. Classi Concrete che estendono la Classe Astratta in Android Java
Ogni classe concreta estende MetodoPagamento e fornisce la propria implementazione di elaboraPagamento(). Tuttavia, grazie alla classe astratta, la logica comune non viene duplicata: ogni classe si occupa dunque solo del proprio comportamento specifico.
// Principio OOP: polimorfismo - tutte sono "MetodoPagamento" ma si comportano in modo diverso
public class PagamentoCarta extends MetodoPagamento {
private String numeroCarta;
public PagamentoCarta(String nome, double saldo, String numeroCarta) {
super(nome, saldo);
this.numeroCarta = numeroCarta;
}
@Override
protected boolean elaboraPagamento(double importo) {
// Qui potresti chiamare un gateway di pagamento, ecc.
// Per semplicità simuliamo sempre successo
return true;
}
}
public class PagamentoPayPal extends MetodoPagamento {
private String emailAccount;
public PagamentoPayPal(String nome, double saldo, String emailAccount) {
super(nome, saldo);
this.emailAccount = emailAccount;
}
@Override
protected boolean elaboraPagamento(double importo) {
// Simulazione chiamata a PayPal
return true;
}
}
public class PagamentoContanti extends MetodoPagamento {
public PagamentoContanti(String nome, double saldo) {
super(nome, saldo);
}
@Override
protected boolean elaboraPagamento(double importo) {
// Contanti: in un caso reale potresti solo registrare l'operazione
return true;
}
}
4. Layout XML: activity_main.xml
Layout principale con una RecyclerView e un campo per inserire l’importo da pagare. In aggiunta, questo layout funge da punto di ingresso principale dell’applicazione.
<?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="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/etImporto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Importo da pagare"
android:inputType="numberDecimal" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvMetodiPagamento"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
5. Layout XML: item_pagamento.xml
Elemento della lista che mostra il nome del metodo di pagamento e il saldo residuo. In particolare, questo layout viene inflato dall’adapter per ogni elemento della RecyclerView.
<?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/tvNomeMetodo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16sp" />
<TextView
android:id="@+id/tvSaldo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp" />
</LinearLayout>
6. Layout XML: dialog_pagamento.xml
Layout semplice per il contenuto del Dialog di conferma. Pertanto, deve essere chiaro e immediato per l’utente.
<?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="16dp">
<TextView
android:id="@+id/tvMessaggio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Confermi il pagamento?"
android:textSize="16sp" />
</LinearLayout>
7. Adapter RecyclerView con Interfaccia Listener in Android Java
L’adapter mostra la lista dei metodi di pagamento e, di conseguenza, notifica un listener quando l’utente seleziona un metodo. In questo modo, la logica di gestione del click rimane disaccoppiata dalla logica di visualizzazione.
// Principio OOP: uso di interfaccia listener per disaccoppiare View e logica
public class MetodiPagamentoAdapter extends RecyclerView.Adapter<MetodiPagamentoAdapter.MetodoViewHolder> {
public interface OnMetodoClickListener {
void onMetodoClick(MetodoPagamento metodo);
}
private List<MetodoPagamento> metodi;
private OnMetodoClickListener listener;
public MetodiPagamentoAdapter(List<MetodoPagamento> metodi, OnMetodoClickListener listener) {
this.metodi = metodi;
this.listener = listener;
}
@NonNull
@Override
public MetodoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_pagamento, parent, false);
return new MetodoViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MetodoViewHolder holder, int position) {
MetodoPagamento metodo = metodi.get(position);
holder.tvNomeMetodo.setText(metodo.getNome());
holder.tvSaldo.setText("Saldo: " + metodo.getSaldo());
holder.itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onMetodoClick(metodo);
}
});
}
@Override
public int getItemCount() {
return metodi.size();
}
static class MetodoViewHolder extends RecyclerView.ViewHolder {
TextView tvNomeMetodo;
TextView tvSaldo;
public MetodoViewHolder(@NonNull View itemView) {
super(itemView);
tvNomeMetodo = itemView.findViewById(R.id.tvNomeMetodo);
tvSaldo = itemView.findViewById(R.id.tvSaldo);
}
}
}
8. MainActivity: orchestrare Classi Astratte e Interfacce in Android Java
La MainActivity inizializza la lista dei metodi di pagamento, gestisce il click sugli elementi e, inoltre, mostra un AlertDialog per confermare il pagamento. Pertanto, rappresenta il punto di coordinamento tra tutte le componenti dell’app.
public class MainActivity extends AppCompatActivity implements MetodiPagamentoAdapter.OnMetodoClickListener {
private EditText etImporto;
private RecyclerView rvMetodiPagamento;
private MetodiPagamentoAdapter adapter;
private List<MetodoPagamento> metodiPagamento;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etImporto = findViewById(R.id.etImporto);
rvMetodiPagamento = findViewById(R.id.rvMetodiPagamento);
// Inizializza la lista dei metodi di pagamento
metodiPagamento = new ArrayList<>();
metodiPagamento.add(new PagamentoCarta("Carta Visa", 100.0, "1111-2222-3333-4444"));
metodiPagamento.add(new PagamentoPayPal("PayPal Personale", 50.0, "user@example.com"));
metodiPagamento.add(new PagamentoContanti("Contanti", 200.0));
adapter = new MetodiPagamentoAdapter(metodiPagamento, this);
rvMetodiPagamento.setLayoutManager(new LinearLayoutManager(this));
rvMetodiPagamento.setAdapter(adapter);
}
@Override
public void onMetodoClick(MetodoPagamento metodo) {
String importoStr = etImporto.getText().toString();
if (importoStr.isEmpty()) {
Toast.makeText(this, "Inserisci un importo", Toast.LENGTH_SHORT).show();
return;
}
double importo = Double.parseDouble(importoStr);
mostraDialogConferma(metodo, importo);
}
private void mostraDialogConferma(MetodoPagamento metodo, double importo) {
// Usa un AlertDialog semplice con layout personalizzato
View dialogView = getLayoutInflater().inflate(R.layout.dialog_pagamento, null);
TextView tvMessaggio = dialogView.findViewById(R.id.tvMessaggio);
tvMessaggio.setText("Confermi il pagamento di " + importo +
" con " + metodo.getNome() + "?");
new AlertDialog.Builder(this)
.setView(dialogView)
.setPositiveButton("Conferma", (dialog, which) -> {
boolean successo = metodo.paga(importo);
if (successo) {
Toast.makeText(this, "Pagamento effettuato", Toast.LENGTH_SHORT).show();
adapter.notifyDataSetChanged();
} else {
Toast.makeText(this, "Pagamento non riuscito (saldo insufficiente)", Toast.LENGTH_SHORT).show();
}
})
.setNegativeButton("Annulla", null)
.show();
}
}
In sintesi, i principi OOP riepilogati in questa mini app sono:
- Astrazione:
Pagabiledefinisce cosa è pagabile. - Ereditarietà:
MetodoPagamentofornisce stato e logica comune. - Polimorfismo:
PagamentoCarta,PagamentoPayPalePagamentoContantisono usati in modo uniforme tramite il tipoMetodoPagamento/Pagabile. - Disaccoppiamento: l’adapter usa un’interfaccia
OnMetodoClickListenerper comunicare con laMainActivity.
7. Errori comuni nella scelta tra Classi Astratte e Interfacce in Android Java
Ecco i 3 errori più frequenti che vedo nei progetti Android di chi inizia. In particolare, questi errori portano a codice difficile da mantenere e da estendere:
- 1. Usare una classe astratta quando servirebbe una interfaccia
Ad esempio creare unaBaseOnClickListenerastratta invece di una semplice interfaccia. Se non hai bisogno di stato o logica comune, una interfaccia è più flessibile. - 2. Mettere troppo codice nella classe base astratta
Classi comeBaseActivityoBaseFragmentche diventano “dio” e fanno di tutto. Risultato: sottoclassi rigide, difficile riusare il codice in contesti diversi. - 3. Duplicare logica tra interfacce e classi astratte
Definire metodi sia nella interfaccia che nella classe astratta quando basterebbe una sola astrazione. Regola pratica: la interfaccia definisce il contratto pubblico, la classe astratta fornisce l’implementazione parziale.
8. Conclusione: Classi Astratte vs Interfacce in Android Java
Capire quando usare una classe astratta e quando invece una interfaccia ti aiuta a progettare architetture Android più pulite e scalabili. La distinzione tra classi astratte vs interfacce in Android Java è fondamentale per modellare sistemi reali come quello di pagamento che hai visto, separando bene il cosa (interfaccia) dal come (classe astratta + classi concrete).
Regola mnemonica:
INTERFACCIA = COSA l’oggetto sa fare. CLASSE ASTRATTA = BASE comune con stato e logica.
Quando sei indeciso, chiediti: “Ho bisogno di stato condiviso e logica comune?” Se sì, dunque, opta per la classe astratta. Se invece ti serve solo un contratto che tante classi diverse possono implementare, punta su una interfaccia. In entrambi i casi, la chiarezza dell’intenzione progettuale è fondamentale.
Per approfondire ulteriormente i concetti OOP applicati ad Android in Java, puoi leggere anche:
Rispondi