Classi Interne e Anonymous Class in Android Java: Inner Class, Static Nested, Local e Anonymous con Esempi

Classi interne e anonymous class in Android Java: guida completa con esempi pratici

In Android Java le classi interne (inner class, static nested class, local class e anonymous class) sono uno strumento fondamentale per organizzare meglio il codice, incapsulare logica e lavorare con callback ed eventi dell'interfaccia grafica. In questa guida tecnica ma accessibile vediamo come usarle in modo efficace nello sviluppo di app Android moderne.

1. Introduzione ai tipi di classi interne in Java

Prima di entrare negli esempi Android, è utile ricordare come sono organizzate le classi interne in Java. Se hai bisogno di un ripasso dei concetti di base della programmazione ad oggetti (OOP) in Android, ti consiglio di leggere l'articolo dedicato.

In Java esistono quattro principali categorie di classi interne:

  • Inner Class non statica: è legata a un'istanza della classe esterna. Può accedere direttamente a metodi e campi dell'istanza esterna.
  • Static Nested Class: è una classe interna dichiarata con static. Non è legata a un'istanza della classe esterna e si comporta in modo simile a una classe di primo livello, ma con visibilità limitata.
  • Anonymous Class: è una classe senza nome, creata “al volo” per implementare un'interfaccia o estendere una classe. È molto usata come callback (ad esempio per OnClickListener).
  • Local Class: è una classe dichiarata all'interno di un metodo, di un costruttore o di un blocco. Ha visibilità limitata a quel contesto.

Nel contesto Android queste classi interne vengono utilizzate quotidianamente per:

  • Gestire eventi dell'interfaccia (click, long click, swipe, ecc.).
  • Incapsulare la logica di un ViewHolder in un RecyclerView.Adapter.
  • Creare callback per thread secondari (Runnable), timer, DialogFragment e molto altro.

2. Inner Class non statica con esempio in un'Activity Android

Una inner class non statica mantiene un riferimento implicito all'istanza esterna. In un'Activity Android questo significa che la inner class può accedere direttamente ai widget (come TextView, Button) e ai metodi dell'Activity.

Esempio: inner class che implementa View.OnClickListener dentro una MainActivity.

Java – Inner class non statica in Activity

public class MainActivity extends AppCompatActivity {

    private TextView textResult;
    private Button buttonHello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textResult = findViewById(R.id.textResult);
        buttonHello = findViewById(R.id.buttonHello);

        // Uso della inner class non statica come listener
        buttonHello.setOnClickListener(new HelloClickListener());
    }

    /**
     * Inner class non statica.
     * Può accedere direttamente ai campi e ai metodi di MainActivity.
     */
    private class HelloClickListener implements View.OnClickListener {

        @Override
        public void onClick(View v) {
            // Accedo direttamente a textResult perché la inner class
            // mantiene un riferimento implicito all'istanza di MainActivity
            textResult.setText("Ciao da Inner Class!");
            mostraToast();
        }
    }

    private void mostraToast() {
        Toast.makeText(this, "Bottone premuto", Toast.LENGTH_SHORT).show();
    }
}

Quando usare una inner class non statica:

  • Quando la classe interna ha bisogno di accedere spesso allo stato dell'istanza esterna (widget, variabili, metodi).
  • Quando la logica della inner class è fortemente legata all'istanza (ad esempio listener specifici di un'Activity).

3. Static Nested Class con esempio ViewHolder in RecyclerView

Una static nested class è una classe interna dichiarata con static. Non ha un riferimento implicito all'istanza esterna e in genere accede solo a membri static della classe che la contiene.

Nel mondo Android, l'esempio più comune è il ViewHolder all'interno di un RecyclerView.Adapter. Se non lo hai ancora fatto, ti consiglio di leggere l'articolo su RecyclerView in Android per avere il quadro completo.

Java – Adapter con static nested ViewHolder

public class ContattoAdapter extends RecyclerView.Adapter<ContattoAdapter.ContattoViewHolder> {

    private final List<Contatto> contatti;

    public ContattoAdapter(List<Contatto> contatti) {
        this.contatti = contatti;
    }

    @NonNull
    @Override
    public ContattoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .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.textNome.setText(contatto.getNome());
        holder.textTelefono.setText(contatto.getTelefono());
    }

    @Override
    public int getItemCount() {
        return contatti.size();
    }

    /**
     * Static nested class: non ha riferimento implicito a ContattoAdapter.
     * Questo aiuta a evitare memory leak e rende il codice più chiaro.
     */
    public static class ContattoViewHolder extends RecyclerView.ViewHolder {

        TextView textNome;
        TextView textTelefono;

        public ContattoViewHolder(@NonNull View itemView) {
            super(itemView);
            textNome = itemView.findViewById(R.id.textNome);
            textTelefono = itemView.findViewById(R.id.textTelefono);
        }
    }
}

Perché usare una static nested class per il ViewHolder?

  • Performance e memoria: il ViewHolder non ha bisogno di accedere all'istanza dell'adapter, quindi evitare il riferimento implicito riduce il rischio di memory leak.
  • Chiarezza: rende evidente che la classe non dipende dallo stato dell'adapter.
  • Struttura: la classe rimane logicamente collegata all'adapter ma è isolata come implementazione.

4. Anonymous Class: OnClickListener, Runnable, Comparator con esempi Android

Una anonymous class è una classe senza nome definita e istanziata in un unico punto. In Java tradizionale, prima delle lambda, era il modo principale per creare callback veloci. In Android la troverai spesso con listener, runnable e comparator.

4.1 Anonymous class per View.OnClickListener

buttonHello.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this,
                "Click gestito da anonymous class",
                Toast.LENGTH_SHORT).show();
    }
});

Qui non creiamo una classe separata per il listener: implementiamo OnClickListener direttamente nel punto in cui ne abbiamo bisogno.

4.2 Anonymous class per Runnable (thread secondario)

new Thread(new Runnable() {
    @Override
    public void run() {
        // Esegui operazioni in background
        // ...

        // Torna sul thread principale (UI thread)
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MainActivity.this,
                        "Operazione completata",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }
}).start();

In questo esempio abbiamo due anonymous class: una per il Runnable del thread secondario e una per il Runnable passato a runOnUiThread().

4.3 Anonymous class per Comparator (ordinamento lista)

Collections.sort(contatti, new Comparator<Contatto>() {
    @Override
    public int compare(Contatto c1, Contatto c2) {
        // Ordina per nome in modo crescente
        return c1.getNome().compareToIgnoreCase(c2.getNome());
    }
});

In assenza delle lambda, questo è il modo più comune per definire velocemente un criterio di ordinamento personalizzato.

5. Local Class con esempio pratico

Una local class è una classe dichiarata all'interno di un metodo. È utile quando hai bisogno di una classe di supporto che abbia senso solo in un determinato metodo, e non vuoi “inquinare” il livello superiore della classe.

Esempio: una local class per validare un form di login all'interno di un metodo validaForm() in un'Activity.

private void validaForm() {
    final String email = editEmail.getText().toString();
    final String password = editPassword.getText().toString();

    // Local class: visibile solo dentro validaForm()
    class Validator {

        boolean isEmailValida() {
            return email.contains("@") && email.contains(".");
        }

        boolean isPasswordValida() {
            return password.length() >= 6;
        }
    }

    Validator validator = new Validator();

    if (!validator.isEmailValida()) {
        editEmail.setError("Email non valida");
        return;
    }

    if (!validator.isPasswordValida()) {
        editPassword.setError("Password troppo corta");
        return;
    }

    // Se arrivo qui i dati sono validi
    Toast.makeText(this, "Login ok", Toast.LENGTH_SHORT).show();
}

La local class Validator è visibile solo dentro validaForm() e può accedere alle variabili email e password dichiarate come final (o effettivamente final).

6. Mini App Lista Contatti: esempio completo

Mettiamo insieme quanto visto finora costruendo una piccola schermata che mostra una lista di contatti con RecyclerView. Utilizzeremo:

  • Static nested class per il ViewHolder.
  • Anonymous class per il OnClickListener sugli item.
  • Anonymous class per il Comparator di ordinamento.

6.1 Modello dati Contatto

public class Contatto {
    private final String nome;
    private final String telefono;

    public Contatto(String nome, String telefono) {
        this.nome = nome;
        this.telefono = telefono;
    }

    public String getNome() {
        return nome;
    }

    public String getTelefono() {
        return telefono;
    }
}

6.2 Layout XML Activity principale

res/layout/activity_lista_contatti.xml

<?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=".ListaContattiActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerContatti"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/
        >

</androidx.constraintlayout.widget.ConstraintLayout>

6.3 Layout XML per il singolo item

res/layout/item_contatto.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    card_view:cardElevation="2dp"
    card_view:cardUseCompatPadding="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="12dp">

        <TextView
            android:id="@+id/textNome"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textStyle="bold"
            android:text="Nome"/>

        <TextView
            android:id="@+id/textTelefono"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp"
            android:textColor="#666666"
            android:text="Telefono"/>

    </LinearLayout>

</androidx.cardview.widget.CardView>

6.4 Adapter con ViewHolder static nested e OnClickListener anonimo

public class ContattoAdapter extends RecyclerView.Adapter<ContattoAdapter.ContattoViewHolder> {

    public interface OnContattoClickListener {
        void onContattoClick(Contatto contatto);
    }

    private final List<Contatto> contatti;
    private final OnContattoClickListener listener;

    public ContattoAdapter(List<Contatto> contatti, OnContattoClickListener listener) {
        this.contatti = contatti;
        this.listener = listener;
    }

    @NonNull
    @Override
    public ContattoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_contatto, parent, false);
        return new ContattoViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ContattoViewHolder holder, int position) {
        final Contatto contatto = contatti.get(position);
        holder.textNome.setText(contatto.getNome());
        holder.textTelefono.setText(contatto.getTelefono());

        // Anonymous class per OnClickListener sul singolo item
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (listener != null) {
                    listener.onContattoClick(contatto);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return contatti.size();
    }

    // Static nested class per il ViewHolder
    public static class ContattoViewHolder extends RecyclerView.ViewHolder {

        TextView textNome;
        TextView textTelefono;

        public ContattoViewHolder(@NonNull View itemView) {
            super(itemView);
            textNome = itemView.findViewById(R.id.textNome);
            textTelefono = itemView.findViewById(R.id.textTelefono);
        }
    }
}

6.5 Activity che utilizza RecyclerView e Comparator anonimo

ListaContattiActivity.java

public class ListaContattiActivity extends AppCompatActivity {

    private RecyclerView recyclerContatti;
    private ContattoAdapter adapter;
    private List<Contatto> contatti;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_lista_contatti);

        recyclerContatti = findViewById(R.id.recyclerContatti);
        recyclerContatti.setLayoutManager(new LinearLayoutManager(this));

        contatti = creaContattiDiEsempio();

        // Comparator definito con anonymous class per ordinare i contatti per nome
        Collections.sort(contatti, new Comparator<Contatto>() {
            @Override
            public int compare(Contatto c1, Contatto c2) {
                return c1.getNome().compareToIgnoreCase(c2.getNome());
            }
        });

        adapter = new ContattoAdapter(contatti, new ContattoAdapter.OnContattoClickListener() {
            @Override
            public void onContattoClick(Contatto contatto) {
                // Gestisci il click sul contatto
                Toast.makeText(ListaContattiActivity.this,
                        "Hai selezionato: " + contatto.getNome(),
                        Toast.LENGTH_SHORT).show();

                // Esempio: potresti aprire un Fragment o un DialogFragment
                // vedi gli articoli dedicati a
                // https://apptutorial.link/cambiare-fragment-android/
                // https://apptutorial.link/dialogfragment-android/
            }
        });

        recyclerContatti.setAdapter(adapter);
    }

    private List<Contatto> creaContattiDiEsempio() {
        List<Contatto> lista = new ArrayList<>();
        lista.add(new Contatto("Andrea", "3331111111"));
        lista.add(new Contatto("Luca", "3332222222"));
        lista.add(new Contatto("Maria", "3333333333"));
        lista.add(new Contatto("Giulia", "3334444444"));
        return lista;
    }
}

Questa mini app dimostra in un contesto reale come combinare static nested class, anonymous class e comparator per costruire una schermata Android completa.

7. Tabella comparativa: Inner vs Static Nested vs Anonymous vs Lambda

Per riassumere, ecco una tabella che confronta le principali caratteristiche delle diverse tipologie di classi interne in Java (con riferimento all'uso in Android). Le lambda non sono una classe interna, ma offrono una sintassi alternativa alle anonymous class per interfacce funzionali.

TipoHa nome?Riferimento all'istanza esternaUso tipico in AndroidVantaggiSvantaggi
Inner Class (non statica)SìSìListener legati a un'Activity o Fragment, helper che usano spesso lo stato esternoAccesso diretto ai membri dell'istanza esterna, codice leggibileRischio di memory leak se usata in modo scorretto (thread lunghi, handler, ecc.)
Static Nested ClassSìNoViewHolder di RecyclerView, helper che non dipendono dall'istanzaPiù sicura lato memoria, dipendenze chiare, più facile da testareNon può usare direttamente membri non statici della classe esterna
Anonymous ClassNoDi solito sì (permette accesso a membri esterni e variabili effectively final)OnClickListener, Runnable, Comparator, callback varieDefinizione e utilizzo in un unico punto, ottima per callback sempliciCodice verboso se i metodi sono lunghi, difficile da riutilizzare
Local ClassSì, ma visibile solo nel metodoSì (può accedere a membri esterni e variabili final/effectively final)Logica di supporto confinata in un singolo metodo complessoIncapsula bene i dettagli implementativi, migliora la leggibilitàPuò complicare il metodo se troppo grande, difficile da testare separatamente
Lambda (Java 8+)No (è una espressione)Dipende dal tipo di cattura; usata solo con interfacce funzionaliAlternative compatte a anonymous class per listener e callback sempliciSintassi molto concisa, codice più pulito per callback breviMeno adatta quando servono molti metodi o stato interno complesso

8. Conclusione

Le classi interne in Java sono uno strumento potente e flessibile, particolarmente utile nello sviluppo Android dove la gestione di eventi, callback e componenti UI è all'ordine del giorno. Saper scegliere tra inner class, static nested class, anonymous class, local class e lambda ti permette di scrivere codice più pulito, sicuro e manutenibile.

Come linea guida pratica:

  • Usa static nested class quando non ti serve l'istanza esterna (es. ViewHolder).
  • Usa inner class non statica quando la classe interna deve accedere spesso a membri non statici dell'istanza esterna.
  • Usa anonymous class o lambda per callback brevi e locali (listener, runnable, comparator).
  • Usa local class per strutturare meglio metodi complessi senza esporre dettagli all'intera classe.

Per approfondire la progettazione delle tue Activity e Fragment in un'ottica orientata agli oggetti, puoi dare un'occhiata anche agli articoli su Activity, Fragment e DialogFragment. Combinando questi concetti con un buon uso delle classi interne potrai strutturare app Android robuste e facilmente estendibili.

Commenti

Lascia un commento