Clase interne și anonime în Java

O clasă poate fi declarată în altă clasă (statică sau interioară), într-o metodă (locală) sau direct la folosire, fără nume (anonimă).

Sintaxă
class Exterior {
    static class Imbricata { }   // nu are nevoie de un obiect Exterior
    class Interioara { }         // legată de un obiect Exterior

    void metoda() {
        class Locala { }         // vizibilă doar în metodă
        Runnable r = new Runnable() {   // clasă anonimă
            public void run() { }
        };
    }
}

Ce face Clase interne și anonime

O clasă imbricată statică (static class) este o clasă obișnuită pusă în alta ca să le grupeze; se creează cu new Exterior.Imbricata(). O clasă interioară (fără static) este legată de un obiect al clasei exterioare și îi poate citi direct câmpurile, chiar și pe cele private.

O clasă locală este declarată într-o metodă și se folosește doar acolo. O clasă anonimă se declară și se instanțiază în aceeași expresie: new Interfata() { ... }. Clasele locale și anonime pot folosi variabilele locale ale metodei, dacă acestea sunt efectiv finale.

Pentru interfețele cu o singură metodă, o lambda înlocuiește aproape mereu clasa anonimă. Clasa anonimă rămâne utilă când ai nevoie de stare proprie sau de mai multe metode. Regulă practică: preferă static class dacă nu ai nevoie de obiectul exterior.

Exemple

Exemplul 1: Clasă imbricată statică și clasă interioară

Biblioteca.Carte este statică: se creează fără bibliotecă. Imprumut este interioară: citește câmpul privat nume al bibliotecii care a creat-o.

import java.util.ArrayList;
import java.util.List;

class Biblioteca {
    private final String nume;
    private final List<Imprumut> imprumuturi = new ArrayList<>();

    Biblioteca(String nume) {
        this.nume = nume;
    }

    static class Carte {
        final String titlu;
        Carte(String titlu) { this.titlu = titlu; }
    }

    class Imprumut {
        final Carte carte;
        final String cititor;

        Imprumut(Carte carte, String cititor) {
            this.carte = carte;
            this.cititor = cititor;
        }

        String fisa() {
            return nume + ": „" + carte.titlu + "” împrumutată de " + cititor;
        }
    }

    void imprumuta(Carte c, String cititor) {
        imprumuturi.add(new Imprumut(c, cititor));
    }

    void raport() {
        imprumuturi.forEach(i -> System.out.println(i.fisa()));
    }
}

public class Main {
    public static void main(String[] args) {
        Biblioteca.Carte ion = new Biblioteca.Carte("Ion");
        Biblioteca b = new Biblioteca("Biblioteca Județeană Iași");
        b.imprumuta(ion, "Ana");
        b.imprumuta(new Biblioteca.Carte("Baltagul"), "Radu");
        b.raport();

        Biblioteca.Imprumut extra = b.new Imprumut(ion, "Elena");  // sintaxa rară: obiect.new
        System.out.println(extra.fisa());
    }
}

Rezultat

Biblioteca Județeană Iași: „Ion” împrumutată de Ana
Biblioteca Județeană Iași: „Baltagul” împrumutată de Radu
Biblioteca Județeană Iași: „Ion” împrumutată de Elena

Exemplul 2: Clasă anonimă și lambda

Ambele comparatoare sortează după lungimea numelui. Clasa anonimă are și o stare proprie (un contor de comparații), lucru pe care o lambda nu îl poate avea.

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> orase = new ArrayList<>(List.of("Iași", "Constanța", "Arad", "Sibiu", "Bistrița"));

        Comparator<String> anonim = new Comparator<>() {
            int comparatii = 0;

            @Override
            public int compare(String a, String b) {
                comparatii++;
                return Integer.compare(a.length(), b.length());
            }

            @Override
            public String toString() {
                return "comparator cu " + comparatii + " comparații";
            }
        };
        orase.sort(anonim);
        System.out.println(orase + " | " + anonim);

        orase.sort((a, b) -> Integer.compare(b.length(), a.length()));
        System.out.println(orase);
    }
}

Rezultat

[Iași, Arad, Sibiu, Bistrița, Constanța] | comparator cu 8 comparații
[Constanța, Bistrița, Sibiu, Iași, Arad]

Exemplul 3: Clasă locală într-o metodă

Record-ul local Rand există doar în metoda raport. Clasa locală Formatare folosește parametrul moneda al metodei, care este efectiv final.

import java.util.List;

public class Main {
    static void raport(List<String> produse, List<Double> preturi, String moneda) {
        record Rand(String produs, double pret) { }

        class Formatare {
            String linie(Rand r) {
                return String.format("%-10s %8.2f %s", r.produs(), r.pret(), moneda);
            }
        }

        Formatare f = new Formatare();
        for (int i = 0; i < produse.size(); i++) {
            System.out.println(f.linie(new Rand(produse.get(i), preturi.get(i))));
        }
    }

    public static void main(String[] args) {
        raport(List.of("pâine", "lapte", "cafea"), List.of(5.5, 8.9, 42.0), "lei");
    }
}

Rezultat

pâine          5.50 lei
lapte          8.90 lei
cafea         42.00 lei

Greșeli frecvente

  • Creezi o clasă interioară dintr-un context static fără obiect exterior: non-static variable this cannot be referenced from a static context. Fă clasa static sau folosește exterior.new Interioara().
  • Modifici o variabilă locală folosită în clasa anonimă: local variables referenced from an inner class must be final or effectively final.
  • Lași o clasă interioară nestatică acolo unde nu e nevoie de obiectul exterior: fiecare instanță ține o referință ascunsă spre el și îl poate ține în memorie inutil.

Comenzi înrudite

  • Expresii lambda: O lambda este o funcție scurtă, fără nume, scrisă ca parametri -> corp, folosită oriunde se așteaptă o interfață funcțională.
  • static: static leagă un câmp sau o metodă de clasă, nu de un obiect: există o singură copie, accesată prin numele clasei.
  • Clase și obiecte (class): O clasă (cuvântul cheie class) descrie un tip nou: câmpuri pentru date și metode pentru comportament; obiectele sunt instanțele ei.
  • Interfețe (interface, implements): O interfață (interface) descrie ce metode trebuie să aibă o clasă; clasa o promite cu implements și poate implementa mai multe interfețe.
  • record: record declară o clasă imutabilă pentru date: constructorul, accesorii, equals, hashCode și toString sunt generați automat.