
Ereditarietà avanzata Android Java per sviluppatori Java
L’ereditarietà avanzata Android Java è uno dei pilastri della programmazione orientata agli oggetti (Programmazione ad Oggetti in Android) e, in Android, assume un ruolo ancora più centrale. Quasi tutto ciò che tocchi nel framework Android è costruito su gerarchie di classi: Activity estende ContextThemeWrapper, Fragment Android estende Object ma implementa interfacce chiave, le View personalizzate estendono View o sottoclassi come TextView, Button o LinearLayout. Capire come usare ereditarietà, super, override, classi interne e classi anonime in modo avanzato è fondamentale per scrivere codice Android pulito, performante e manutenibile.
In questo articolo approfondiremo questi concetti con un taglio pratico, orientato a chi sviluppa Android in Java, mostrando esempi reali presi dalla vita quotidiana di uno sviluppatore: override dei metodi del ciclo di vita, personalizzazione delle View, adapter con ViewHolder, listener tramite classi anonime e una mini app di esempio che mette tutto insieme.
Perché l’ereditarietà avanzata Android Java è fondamentale nello sviluppo di app
Android è progettato attorno a classi base che forniscono gran parte del comportamento di default. Tu, come sviluppatore, raramente parti da zero: estendi classi esistenti e ne personalizzi solo una parte. Alcuni esempi tipici:
- Activity: estendi
AppCompatActivityper creare schermate con supporto alle action bar, temi, compatibilità con versioni precedenti. - Fragment: estendi
FragmentoDialogFragmentper costruire componenti UI riutilizzabili. - View: estendi
View,TextView,Buttono layout comeLinearLayoutper creare componenti grafici personalizzati.
L’ereditarietà avanzata in Android significa saper:
- chiamare correttamente i metodi della classe padre con
superquando necessario; - sovrascrivere i metodi (override) rispettando le regole del linguaggio e del framework;
- organizzare il codice con classi interne, statiche e non;
- usare classi anonime e lambda in modo consapevole, soprattutto per i listener.
1. super — richiamare il padre nell’ereditarietà avanzata Android Java
La keyword super ti permette di riferirti esplicitamente alla classe padre. In Android la userai continuamente, ad esempio in onCreate(), onResume(), Canvas Android (onDraw()) e in molti altri metodi del ciclo di vita.
super() nel costruttore nelle classi Android
Ogni volta che estendi una classe, il costruttore della sottoclasse deve chiamare (esplicitamente o implicitamente) un costruttore della superclasse. In Android questo è particolarmente evidente quando crei una custom View:
public class MyCustomView extends View {
public MyCustomView(Context context) {
super(context); // richiama il costruttore View(Context)
init();
}
public MyCustomView(Context context, AttributeSet attrs) {
super(context, attrs); // importante per l'uso da XML
init();
}
public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
// inizializzazione comune
}
}
Qui super(context, attrs) passa alla classe base (View) le informazioni necessarie per leggere gli attributi da XML, applicare gli stili, ecc. Dimenticare questa chiamata porta spesso a comportamenti inattesi o crash.
super.metodo() per estendere il comportamento di classi e View Android
Un pattern tipico in Android è:
- la superclasse implementa un comportamento di default corretto;
- tu vuoi aggiungere qualcosa prima o dopo quel comportamento, senza riscriverlo da zero.
In questi casi usi super.metodo() all’inizio o alla fine del tuo override. Esempi classici:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // fondamentale
setContentView(R.layout.activity_main);
// tua logica di inizializzazione
}
@Override
protected void onResume() {
super.onResume(); // mantiene il comportamento standard
// eventuale logica aggiuntiva (es. analytics, refresh dati)
}
}
Per le View grafiche, è comune richiamare super.onDraw() solo in alcuni casi (spesso lo si omette per disegnare completamente da zero):
public class CustomTextView extends AppCompatTextView {
public CustomTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
// Disegna uno sfondo personalizzato prima del testo
canvas.drawColor(Color.YELLOW);
// Lascia che la superclasse disegni il testo
super.onDraw(canvas);
// Poi disegna ad esempio una linea sotto il testo
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(4);
canvas.drawLine(0, getHeight() - 4, getWidth(), getHeight() - 4, paint);
}
}
Quando usare super e quando NON usarlo nell’ereditarietà avanzata Android Java
In generale:
- Devi quasi sempre chiamare
supernei metodi del ciclo di vita diActivityeFragment(onCreate,onStart,onResume,onPause,onStop,onDestroy, ecc.) perché la classe base gestisce componenti interni fondamentali (gestione del FragmentManager, ViewModel, sistema di salvataggio stato, ecc.). - Per i metodi grafici delle View (come
onDraw(),onMeasure()) la chiamata asuperdipende dal tuo obiettivo:- se vuoi integrare il comportamento di default (es. aggiungere un bordo ad una
TextView) chiamisuper; - se vuoi rimpiazzare completamente il disegno o la misura, potresti evitare di chiamarlo, gestendo tutto tu.
- se vuoi integrare il comportamento di default (es. aggiungere un bordo ad una
- Non chiamare mai
superalla fine per metodi che si aspettano di essere chiamati per primi (es.onCreate()in Activity): il framework si aspetta che alcune operazioni avvengano prima del tuo codice.
2. override — sovrascrivere correttamente nell’ereditarietà avanzata Android Java
L’override è il meccanismo che ti permette di fornire una nuova implementazione di un metodo ereditato dalla superclasse. In Android praticamente ogni classe personalizzata ne fa uso: override di metodi di callback, di ciclo di vita, di disegno, di gestione eventi.
@Override: perché è (di fatto) obbligatoria nel codice Android Java
L’annotazione @Override dice al compilatore: “questo metodo deve necessariamente esistere in una superclasse o interfaccia”. Se sbagli firma, nome o tipo di ritorno, il compilatore ti segnalerà un errore.
public class MyActivity extends AppCompatActivity {
// Corretto
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
// Senza @Override potresti accidentalmente creare un nuovo metodo
// invece di sovrascrivere quello esistente.
}
Nello sviluppo Android moderno, usa sempre @Override quando intendi sovrascrivere un metodo. È uno strumento di sicurezza, non solo una formalità.
Regole di override: visibilità e tipo di ritorno nelle gerarchie di classi Android
Quando sovrascrivi un metodo in Java, devi rispettare alcune regole:
- Firma compatibile: stesso nome, stessi parametri (ordine e tipi).
- Visibilità uguale o più ampia: non puoi rendere il metodo più restrittivo.
- Se la superclasse dichiara
public void foo(), non puoi usareprotectedoprivatenel metodo override.
- Se la superclasse dichiara
- Tipo di ritorno compatibile (covariant return type): puoi restituire lo stesso tipo o un sottotipo.
- Se la superclasse ha
public View getView(...), puoi restituireTextViewo una tua sottoclasse diView.
- Se la superclasse ha
- Eccezioni: non puoi dichiarare nuove checked exception più “ampie” di quelle dichiarate dal metodo base (in Android spesso non è un problema perché molti metodi non dichiarano checked exception).
Override vs overloading: differenza pratica nello sviluppo Android Java
In Android è facile confondere override e overloading:
- Override: stessa firma del metodo nella superclasse. Cambi il comportamento ereditato.
- Overloading: stesso nome ma parametri diversi. Crei un nuovo metodo, non tocchi quello della superclasse.
public class MyButton extends AppCompatButton {
// Override: stesso metodo di Button
@Override
public boolean performClick() {
// tua logica
boolean handled = super.performClick();
// altro codice
return handled;
}
// Overloading: metodo diverso, non chiamato dal framework
public boolean performClick(int extraFlag) {
// NON verrà mai chiamato automaticamente dal sistema
return performClick();
}
}
Quando implementi callback del framework (come onCreate, onDraw, onTouchEvent), è quasi sempre un override. Un overloading potrebbe non essere mai chiamato automaticamente dal framework e restare inutilizzato.
Esempi Android di override: onMeasure(), onDraw(), onTouchEvent()
Nelle custom View, tre metodi chiave che spesso si sovrascrivono sono:
onMeasure(int widthMeasureSpec, int heightMeasureSpec): calcola la dimensione finale della View.onDraw(Canvas canvas): disegna il contenuto grafico.onTouchEvent(MotionEvent event): gestisce gli eventi touch.
public class CircleView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
paint.setColor(Color.BLUE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredSize = 200; // px, solo per esempio
int width = resolveSize(desiredSize, widthMeasureSpec);
int height = resolveSize(desiredSize, heightMeasureSpec);
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); // opzionale, qui non disegna nulla
float radius = getWidth() / 2f;
canvas.drawCircle(radius, radius, radius, paint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// ad esempio cambiamo colore
paint.setColor(Color.RED);
invalidate();
return true; // evento gestito
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
paint.setColor(Color.BLUE);
invalidate();
return true;
}
return super.onTouchEvent(event);
}
}
3. Classi interne (Inner Classes) in Android Java
Le classi interne sono classi dichiarate all’interno di un’altra classe. In Java (e quindi in Android) ne esistono due principali tipi:
- Static nested class: dichiarata con
static. Non ha un riferimento implicito alla classe esterna. - Inner class (non-static): dichiarata senza
static. Mantiene un riferimento implicito all’istanza della classe esterna.
Static nested class nelle architetture Android
Una static nested class è come una classe normale, ma “raggruppata” logicamente dentro un’altra. In Android è usata spesso per evitare memory leak, perché non mantiene un riferimento implicito all’istanza esterna.
public class MyActivity extends AppCompatActivity {
private static class MyHandler extends Handler {
private final WeakReference<MyActivity> activityRef;
MyHandler(MyActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MyActivity activity = activityRef.get();
if (activity != null && !activity.isFinishing()) {
// usa activity in sicurezza
}
}
}
}
Questo pattern è preferibile a una inner class non statica, perché evita che il Handler trattenga la Activity in memoria anche dopo la sua distruzione.
Inner class (non-static) e relazione con la classe esterna
Una inner class non statica mantiene un riferimento implicito alla sua classe esterna. È utile quando la classe interna deve accedere frequentemente a campi e metodi dell’istanza esterna.
public class CustomViewGroup extends ViewGroup {
private final LayoutHelper layoutHelper = new LayoutHelper();
public CustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
// Inner class non statica: può accedere direttamente a getChildCount(), getChildAt(), ecc.
class LayoutHelper {
void layoutChildren() {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// logica di layout personalizzata
}
}
}
}
Quando usare le inner class in Android (ViewHolder, Builder, Handler)
Alcuni casi d’uso comuni delle classi interne in Android:
- ViewHolder nei RecyclerView Android Adapter: quasi sempre una static nested class per evitare leak.
- Builder per oggetti complessi: spesso una static nested class (es.
AlertDialog.Builder). - Handler: spesso static nested class con
WeakReferencealla Activity o Fragment, per gestione sicura dei messaggi.
Esempio pratico: ViewHolder come static nested class in Android
Nel pattern RecyclerView, il ViewHolder è il luogo ideale per una static nested class:
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
private final List<Product> products;
public ProductAdapter(List<Product> products) {
this.products = products;
}
@NonNull
@Override
public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_product, parent, false);
return new ProductViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) {
Product product = products.get(position);
holder.bind(product);
}
@Override
public int getItemCount() {
return products.size();
}
// Static nested class: non tiene un riferimento implicito a ProductAdapter
static class ProductViewHolder extends RecyclerView.ViewHolder {
TextView nameTextView;
TextView priceTextView;
ProductViewHolder(@NonNull View itemView) {
super(itemView);
nameTextView = itemView.findViewById(R.id.text_name);
priceTextView = itemView.findViewById(R.id.text_price);
}
void bind(Product product) {
nameTextView.setText(product.getName());
priceTextView.setText(String.valueOf(product.getPrice()));
}
}
}
Usando una static nested class per il ViewHolder riduci il rischio di memory leak e mantieni il codice più ordinato.
4. Anonymous class in Android Java
Una anonymous class è una classe senza nome, creata “al volo” per implementare un’interfaccia o estendere una classe astratta. In Android è diffusissima per gestire listener ed eseguire codice in background.
Anonymous class: cosa sono e quando usarle nelle app Android
Le classi anonime sono utili quando:
- devi creare una singola implementazione di un’interfaccia;
- il comportamento è relativamente semplice e locale;
- non vuoi aggiungere una classe pubblica o interna solo per quel caso d’uso.
Esempio classico: gestire il click di un bottone:
Button button = findViewById(R.id.my_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Azione al click
}
}); // Intent Android: https://apptutorial.link/richiamare-activity-android-studio/
Esempi pratici Android di anonymous class: OnClickListener, Runnable, Comparator
Alcuni esempi tipici di classi anonime in Android con Java:
// 1. OnClickListener (Intent Android: https://apptutorial.link/richiamare-activity-android-studio/)
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(), "Clicked!", Toast.LENGTH_SHORT).show();
}
});
// 2. Runnable con Handler
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
// codice eseguito dopo un delay
}
}, 1000);
// 3. Comparator per ordinare una lista
Collections.sort(products, new Comparator<Product>() {
@Override
public int compare(Product p1, Product p2) {
return p1.getName().compareTo(p2.getName());
}
});
Anonymous class vs Lambda — quando preferire l’una o l’altra in Android Java
Se usi Java 8+ con supporto alle lambda su Android, spesso puoi sostituire le classi anonime con lambda quando implementi interfacce funzionali (interfacce con un solo metodo astratto), come View.OnClickListener o Runnable.
// Classe anonima
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doSomething();
}
});
// Lambda equivalente (più concisa)
view.setOnClickListener(v -> doSomething());
Quando preferire l’una o l’altra?
- Lambda: quando il codice è breve, leggibile e l’interfaccia è funzionale. Migliora molto la chiarezza.
- Classe anonima: quando devi accedere a
thisdell’interfaccia (e non della classe esterna), o quando l’interfaccia non è funzionale, o quando devi overrideare più metodi (es. alcuni listener complessi).
Limiti delle anonymous class nello sviluppo Android
Le classi anonime hanno alcuni svantaggi:
- Verbose in Java rispetto alle lambda.
- Difficili da riusare: essendo senza nome, non puoi istanziarle altrove.
- Rischio di memory leak: come le inner class non statiche, mantengono un riferimento implicito all’istanza esterna (es. Activity), quindi vanno usate con cautela per operazioni di lunga durata.
- Testabilità ridotta: il codice inline è più difficile da testare isolatamente rispetto a una classe separata.
5. Esempio pratico completo: mini app con custom View e ereditarietà avanzata Android Java
Mettiamo insieme i concetti visti finora con un esempio concreto: una mini app che mostra un semplice “indicatore di progresso” personalizzato. Useremo:
- una custom View che estende
View; - override di
onMeasure()eonDraw(); - una static nested class per raggruppare i parametri di configurazione;
- una anonymous class per il listener del bottone che aggiorna il progresso.
Step 1: la custom View che estende View con ereditarietà avanzata Android Java
public class ProgressCircleView extends View {
private Paint backgroundPaint;
private Paint progressPaint;
private int progress = 0; // 0 - 100
private ProgressConfig config;
public ProgressCircleView(Context context) {
this(context, null);
}
public ProgressCircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ProgressCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); // super nel costruttore
init(null);
}
private void init(ProgressConfig config) {
if (config == null) {
// se non specificato, usiamo una config di default
this.config = new ProgressConfig.Builder().build();
} else {
this.config = config;
}
backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
backgroundPaint.setColor(this.config.backgroundColor);
backgroundPaint.setStyle(Paint.Style.STROKE);
backgroundPaint.setStrokeWidth(this.config.strokeWidth);
progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
progressPaint.setColor(this.config.progressColor);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeWidth(this.config.strokeWidth);
progressPaint.setStrokeCap(Paint.Cap.ROUND);
}
public void setProgress(int progress) {
this.progress = Math.max(0, Math.min(100, progress));
invalidate(); // richiede un redraw
}
public void applyConfig(ProgressConfig config) {
init(config);
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Override di onMeasure per creare una vista quadrata
int desiredSize = config.desiredSizePx;
int width = resolveSize(desiredSize, widthMeasureSpec);
int height = resolveSize(desiredSize, heightMeasureSpec);
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); // qui non disegna nulla, ma è safe
float padding = config.strokeWidth / 2f;
float radius = (getWidth() - 2 * padding) / 2f;
float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;
// cerchio di background (360°)
canvas.drawCircle(centerX, centerY, radius, backgroundPaint);
// arco del progresso (da -90° per partire dall'alto)
RectF oval = new RectF(
padding,
padding,
getWidth() - padding,
getHeight() - padding
);
float sweepAngle = 360 * (progress / 100f);
canvas.drawArc(oval, -90, sweepAngle, false, progressPaint);
}
// Static nested class per la configurazione
public static class ProgressConfig {
final int backgroundColor;
final int progressColor;
final float strokeWidth;
final int desiredSizePx;
private ProgressConfig(Builder builder) {
this.backgroundColor = builder.backgroundColor;
this.progressColor = builder.progressColor;
this.strokeWidth = builder.strokeWidth;
this.desiredSizePx = builder.desiredSizePx;
}
// Builder come static nested class
public static class Builder {
private int backgroundColor = Color.LTGRAY;
private int progressColor = Color.GREEN;
private float strokeWidth = 20f;
private int desiredSizePx = 300;
public Builder setBackgroundColor(int color) {
this.backgroundColor = color;
return this;
}
public Builder setProgressColor(int color) {
this.progressColor = color;
return this;
}
public Builder setStrokeWidth(float width) {
this.strokeWidth = width;
return this;
}
public Builder setDesiredSizePx(int sizePx) {
this.desiredSizePx = sizePx;
return this;
}
public ProgressConfig build() {
return new ProgressConfig(this);
}
}
}
}
Qui abbiamo:
- supercostruttori che chiamano il
super(...)appropriato; - override di
onMeasure()eonDraw()per controllare dimensioni e disegno; - una
static nested classProgressConfige il suoBuilderper incapsulare la configurazione.
Step 2: Activity che usa la custom View e una anonymous class
public class ProgressActivity extends AppCompatActivity {
private ProgressCircleView progressCircleView;
private Button increaseButton;
private int currentProgress = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_progress);
progressCircleView = findViewById(R.id.progress_view);
increaseButton = findViewById(R.id.button_increase);
// Applichiamo una configurazione personalizzata
ProgressCircleView.ProgressConfig config =
new ProgressCircleView.ProgressConfig.Builder()
.setBackgroundColor(Color.LTGRAY)
.setProgressColor(Color.MAGENTA)
.setStrokeWidth(30f)
.setDesiredSizePx(400)
.build();
progressCircleView.applyConfig(config);
// Anonymous class per il listener del bottone
increaseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
currentProgress += 10;
if (currentProgress > 100) {
currentProgress = 0;
}
progressCircleView.setProgress(currentProgress);
}
});
}
}
Abbiamo così una mini app che mette in pratica:
- ereditarietà avanzata (custom View che estende
View); - uso di
supernel costruttore e nei metodi overridden; - override dei metodi chiave del disegno e del layout;
- classi interne statiche (
ProgressConfige ilBuilder); - classi anonime per il listener del bottone.
6. Conclusione e risorse correlate sull’ereditarietà avanzata Android Java
L’ereditarietà avanzata Android Java non è solo teoria OOP: è il modo concreto in cui interagisci col framework ogni giorno. Capire quando e come usare super, sovrascrivere i metodi corretti rispettando le regole del linguaggio, organizzare il codice con classi interne (statiche e non) e usare classi anonime e lambda in modo consapevole ti permette di scrivere app più robuste, modulari e facili da mantenere. Per approfondire il funzionamento delle Activity e del loro ciclo di vita, consulta la documentazione ufficiale Android.
Se senti il bisogno di ripassare le basi della programmazione a oggetti in Java (classi, ereditarietà semplice, interfacce, polimorfismo), puoi leggere l’articolo introduttivo: OOP in Android. Per vedere un esempio più completo che combina ereditarietà, pattern architetturali e gestione di dati reali, ti consiglio anche di dare un’occhiata alla mini app di esempio Mini App Gestione Prodotti, dove troverai un uso esteso di adapter, ViewHolder, override di metodi Android e listener organizzati con classi interne e anonime.
Prenditi il tempo di sperimentare: crea una tua custom View, gioca con i lifecycle method di Activity e Fragment, prova diverse soluzioni con classi interne, anonime e lambda. L’esperienza pratica è il modo migliore per padroneggiare davvero l’ereditarietà avanzata in Android.
Lascia un commento