Classi innestate

Progettazione e Sviluppo del Software

C.D.L. Tecnologie dei Sistemi Informatici

Danilo Pianini — danilo.pianini@unibo.it

Gianluca Aguzzi — gianluca.aguzzi@unibo.it

Angelo Filaseta — angelo.filaseta@unibo.it

Compiled on: 2025-12-05 — versione stampabile

back

Classi innestate statiche

Classi innestate statiche – idea e terminologia

Principali elementi

  • Dentro una classe A, chiamata outer è possibile innestare la definizione di un’altra classe B, chiamata innestata (statica) – in inglese, static nested
  • B viene quindi vista come se fosse una proprietà statica di A
    • richiamabile via A, come le new e le chiamate statiche
// situazione di partenza
class A {...}
class B {...}
// modifica, usando le inner class
class A {
    ...
    static class B { .. }
}

Classi innestate statiche – casistica

Possibilità di innestamento

  • Anche una interfaccia può fungere da Outer
  • Si possono innestare anche interfacce
  • Il nesting può essere multiplo e/o multilivello
  • L’accesso alle classi/interfacce innestate statiche avviene con sintassi Outer.A, Outer.B, Outer.I, Outer.A.C
class Outer {
    ...
    static class A { ... static class C { ... } ... }
    static class B { ... }
    interface I { ... } // static è implicito nelle interfacce
}

Classi innestate statiche – accesso

Uso

  • L’accesso alle classi/interfacce innestate statiche avviene con sintassi Outer.StaticNested
    • ovvero, come se fosse un membro statico della classe
  • Da dentro Outer si può accedere anche direttamente con StaticNested
  • L’accesso da fuori Outer di StaticNested segue le regole del suo modificatore d’accesso
  • Esterna e interna si vedono a vicenda anche le proprietà private
class Outer {
    ...
    static class StaticNested { 
       ...
    }
}
..
Outer.StaticNested obj = new Outer.StaticNested(...);

Motivazioni

Una necessità generale

Vi sono situazioni in cui per risolvere un singolo problema è opportuno generare più classi, e non si vuole affiancarle solo come classi dello stesso package

Almeno tre motivazioni (non necessariamente contemporanee)

  1. Evitare il proliferare di classi in un package, specialmente quando solo una di queste debba essere pubblica
  2. Migliorare l’incapsulamento, con un meccanismo per consentire un accesso locale anche a proprietà private
  3. Migliorare la leggibilità, inserendo classi là dove serve (con nomi qualificati, quindi più espressivi)

Esempio: specializzazioni come classi innestate

  • La classe astratta, o comunque base, è la outer
  • Alcune specializzazioni ritenute frequenti e ovvie vengono innestate, ma comunque rese pubbliche
  • due implicazioni:
    • schema di nome delle inner class
    • possibilità di accedere alle proprietà statiche

Note

  • Un’indicazione che sia opportuno usare classi innestate è la presenza di nomi composti da una parte comune
  • Ad esempio:
    • Counter che modella un contatore
    • BidirectionalCounter, sempre un contatore, ma che può incrementare di più step alla volta
    • MultiCounter, sempre un contatore, ma che conta anche all’indietro

Classe Counter e specializzazioni innestate

package it.unibo.nested;

public class Counter {
    private int value;
    public Counter(int initialValue) { this.value = initialValue; }
    public void increment() { this.value++; }
    public int getValue() { return this.value; }

    public static class Multi extends Counter {
        public Multi(int initialValue) { super(initialValue); }
        public void multiIncrement(int n) { super.value += n; }
    }

    public static class Bidirectional extends Counter {
        public Bidirectional(int initialValue) { super(initialValue); }
        public void decrement() { super.value--; }
    }
}

Uso di Counter e specializzazioni innestate

package it.unibo.nested;

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

public class UseCounter {
    static void main() {
        final List<Counter> list = new ArrayList<>();
        list.add(new Counter(100));
        list.add(new Counter.Bidirectional(100));
        list.add(new Counter.Multi(100));

        for (final Counter c : list) {
            c.increment();
        }
    }
}

Esempio: implementazione interna di una classe separata

In una classe potrebbero servire sotto-comportamenti che debbano: implementare una data interfaccia o estendere una data classe, ma che non vogliamo esporre come classi esterne

  • Per esempio, il concetto di Range, un Iterable che deve quindi produrre un Iterator
    • Non vogliamo esporre il nostro concetto di RangeIterator pubblicamente
    • Vogliamo che sia incapsulato dentro Range

Spesso classi di questo tipo non devono essere visibili dall’esterno, quindi vengono indicate come private

Classe Range e suo RangeIterator innestato

public class RangeNested implements Iterable<Integer> {

    private final int start;
    private final int stop;

    public RangeNested(final int start, final int stop) {
        this.start = start;
        this.stop = stop;
    }

    public Iterator<Integer> iterator() { return new RangeIterator(this.start, this.stop); }

    private static class RangeIterator implements Iterator<Integer> {
        private int current;
        private final int stop;

        public RangeIterator(final int start, final int stop) {
            this.current = start;
            this.stop = stop;
        }

        public Integer next() { return this.current++; }

        public boolean hasNext() { return this.current <= this.stop; }

        public void remove() { throw new UnsupportedOperationException(); }
    }
}

Esempio tratto dal Collection Framework

  • Map, Map.Entry
  • una mappa è “osservabile” come set di entry, ossia come collezione di coppie chiave-valore
public interface Map<K,V> {

    // Query Operations
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);        // usa Object.equals
    boolean containsValue(Object value);    // usa Object.equals
    V get(Object key);                      // accesso a valore

    // Modification Operations
    V put(K key, V value);          // inserimento chiave-valore
    V remove(Object key);           // rimozione chiave(-valore)

    // Bulk Operations
    void putAll(Map<? extends K, ? extends V> m);
    void clear();                   // cancella tutti

    // Views
    Set<K> keySet();                    // set di chiavi
    Collection<V> values();             // collezione di valori
    Set<Map.Entry<K, V>> entrySet();    // set di chiavi-valore
    
    interface Entry<K,V> {...}          // public static implicito!
}

Riassunto classi innestate statiche

Principali aspetti

  • Da fuori (se pubblica) vi si accede con nome Outer.StaticNested
  • Outer e StaticNested sono co-locate: si vedono le proprietà private

Motivazione generale

  • Voglio evitare la proliferazione di classi nel package
  • Voglio sfruttare l’incapsulamento

Motivazione per il caso public

  • Voglio enfatizzare i nomi Out.C1, Out.C2,..

Motivazione per il caso private – è il caso più frequente

  • Voglio realizzare una classe a solo uso della outer, invisibile all’esterno (incapsulato)

Map.Entry

Ruolo di Map.Entry

  • Una mappa può essere vista come una collezione di coppie chiave-valore, ognuna incapsulata in un Map.Entry
  • Quindi, una mappa è composta da un set di Map.Entry
public interface Map<K,V> {
    
    ...
    
    Set<Map.Entry<K, V>> entrySet();

    interface Entry<K,V> { // public e static implicite! 

        K getKey();
        V getValue();
        V setValue(V value);
    
    }
}

Inner Class

Inner Class – idea

Principali elementi

  • Dentro una classe Outer, è possibile innestare la definizione di un’altra classe InnerClass, senza indicazione static
  • InnerClass è vista come se fosse una proprietà non-statica di Outer al pari di altri campi o metodi
  • L’effetto è che una istanza di InnerClass ha sempre un riferimento ad una istanza di Outer (enclosing instance) che ne rappresenta il contesto, accessibile con la sintassi Outer.this, e ai suoi campi (privati)
class Outer {
    ...
    class InnerClass { // Nota.. non è static!
        ...
        // ogni oggetto di InnerClass avrà un riferimento ad
        // un oggetto di Outer, denominato Outer.this
    }
}

Un semplice esempio

public class Outer {
    private final int i;

    public Outer(int i) {
        this.i = i;
    }

    public Inner createInner() {
        return new Inner();
        // oppure: return this.new Inner();
    }

    public class Inner {
        private int j = 0;

        public void update() {
            this.j = this.j + Outer.this.i; // si usa l'oggetto di outer
        }

        public int getValue() {
            return this.j;
        }
    }
}

Uso di Inner e Outer

public class UseOuter {
    static void main() {
        Outer o = new Outer(5);
        Outer.Inner in = o.new Inner();
        System.out.println(in.getValue()); // 0
        in.update();
        in.update();
        System.out.println(in.getValue()); // 5

        Outer.Inner in2 = new Outer(10).createInner();
        in2.update();
        in2.update();
        System.out.println(in2.getValue()); // 20
    }
}

Enclosing instance – istanza esterna

Gli oggetti delle inner class

  • Sono creati con espressioni: <obj-outer>.new <classe-inner>(<args>)
  • (la parte <obj-outer> è omettibile quando sarebbe this)
  • Possono accedere all’enclosing instance con notazione <classe-outer>.this

Motivazioni: quelle relative alle classi innestate statiche, ma:

  • è necessario che ogni oggetto inner tenga un riferimento all’oggetto outer
  • pragmaticamente: usato quasi esclusivamente il caso private

Esempio

  • La classe Range già vista usa una static nested class
    • Se fosse inner, avrebbe accesso diretto a start e stop tramite l’enclosing instance!

Una variante di Range

public class RangeInner implements Iterable<Integer> {
    private final int start;
    private final int stop;

    public RangeInner(final int start, final int stop) {
        this.start = start;
        this.stop = stop;
    }

    public Iterator<Integer> iterator() {
        return this.new RangeIterator();
    }

    private class RangeIterator implements Iterator<Integer> {
        private int current;

        public RangeIterator() {
            this.current = RangeInner.this.start; // this.current = start
        }

        public Integer next() { return this.current++; }

        public boolean hasNext() {
            // Accesso diretto ai campi non-statici della classe esterna!
            return this.current <= RangeInner.this.stop; // Equivalente: return current <= stop;
        }

        public void remove() { throw new UnsupportedOperationException(); }
    }
}

Classi locali

Classi locali – idea

Principali elementi

  • Dentro un metodo di una classe Outer, è possibile innestare la definizione di un’altra classe LocalClass
  • La LocalClass è a tutti gli effetti una inner class (e quindi ha enclosing instance)
    • Non è possibile creare classi static locali (innestate in un metodo)
  • In più, la LocalClass “vede” anche le variabili nello scope del metodo in cui è definita, usabili solo se final
    • o se “effectively final”, ossia il compilatore può verificare che non vengano mai modificate dopo l’inizializzazione
class Outer {
    // ...
    void m(final int x){
        final String s = /* ... */;
        class LocalClass { // Nota.. non è static!
            // ... può usare Outer.this, s e x
        }
        LocalClass c = new LocalClass(...);
    }
}

Range tramite classe locale

public class RangeLocalClass implements Iterable<Integer> {
    private final int start;
    private final int stop;

    public RangeLocalClass(final int start, final int stop) {
        this.start = start;
        this.stop = stop;
    }

    public java.util.Iterator<Integer> iterator() {
        class RangeIterator implements Iterator<Integer> {
            private int current;

            public RangeIterator() { this.current = RangeLocalClass.this.start; }

            public Integer next() { return this.current++; }

            public boolean hasNext() { return this.current <= RangeLocalClass.this.stop; }

            public void remove() { }
        }
        return new RangeIterator();
    }
}

Classi locali – motivazioni

Perché usare una classe locale invece di una inner class

  • Tale classe è necessaria solo dentro ad un metodo, e lì la si vuole confinare
  • È eventualmente utile accedere anche alle variabili del metodo

Pragmaticamente

  • Spesso usate “spot” in un solo punto
  • Difatti, il nome praticamente viene usato solo per invocare una volta il costruttore
  • Sarebbe comodo poter evitare di assegnare un nome da usare una sola volta, costruendo direttamente l’oggetto

Classi anonime

Classi anonime – idea

Principali elementi

  • Con una variante dell’istruzione new, è possibile innestare la definizione di un’altra classe senza indicarne il nome
    • In tale definizione non possono comparire costruttori
  • Viene creata al volo una classe locale, e da lì se ne crea un oggetto
    • Tale oggetto, come per le classi locali, ha enclosing instance e “vede” anche le variabili final (o di fatto finali) nello scope del metodo in cui è definita
class C {
    // ...
    Object m(final int x) {
        return new Object() {
             public String toString() { return "Valgo " + x; }
        }
    }
}
  • In pratica, una local class, ma senza nome, e con un solo oggetto istanziato.

Range tramite classe anonima – la soluzione ottimale

import java.util.Iterator;

public class RangeAnonymous implements Iterable<Integer> {
    private final int start;
    private final int stop;

    public RangeAnonymous(final int start, final int stop) {
        this.start = start;
        this.stop = stop;
    }

    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            // Non ci può essere costruttore!
            private int current = start; // o anche Range4.this.start

            public Integer next() {
                return current++;
            }

            public boolean hasNext() {
                return current <= stop; // o anche Range4.this.stop
            }

            public void remove() { }
        }; // questo è il ; del return
    }
}

Altro esempio: classe anonima da Comparable

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

public class UseSort {
    static void main() {
        final List<Integer> list = Arrays.asList(10, 40, 7, 57, 13, 19, 21, 35);
        System.out.println(list);
        // classe anonima a partire da una interfaccia
        Collections.sort(list, new Comparator<Integer>() {
            public int compare(Integer a, Integer b) {
                return Integer.compare(a, b);
            }
        });
        System.out.println(list);
        Collections.sort(list, new Comparator<Integer>() {
            public int compare(Integer a, Integer b) {
                return Integer.compare(b, a);
            }
        });
        System.out.println(list);
    }
}

Classi anonime – motivazioni

Perchè usare una classe anonima?

  • Se ne deve creare un solo oggetto, quindi è inutile anche solo nominarla
  • Si vuole evitare la proliferazione di classi
  • Tipicamente: per implementare “al volo” una interfaccia

Pragmaticamente

  • Spesso queste classi anonime hanno un solo metodo astratto!
  • Di fatto, è come se implementassimo solo quel metodo “al volo”
  • Tutta la struttura di contorno è solo “cerimonia”

Espressioni Lambda

  • Una lambda expression è una funzione anonima, creata “al volo”
    • internamente implementata come istanza di un’interfaccia funzionale (un’interfaccia che definisce un solo metodo astratto)
    • di fatto, è una sintassi più compatta per creare istanze di classi anonime con un solo metodo astratto
public class UseSortLambda {
    static void main() {
        final List<Integer> list = Arrays.asList(10, 40, 7, 57, 13, 19, 21, 35);
        System.out.println(list);
        // classe anonima a partire da una interfaccia
        Collections.sort(list, (a, b) -> Integer.compare(a, b));
        System.out.println(list);

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

Riassunto e linee guida

Inner class (e varianti)

Utili quando si vuole isolare un sotto-comportamento in una classe a sé, senza dichiararne una nuova che si affianchi alla lista di quelle fornite dal package, ma stia “dentro” una classe più importante

Se deve essere visibile alle altre classi

  • Quasi sicuramente, una static nested class

Se deve essere invisibile da fuori

  • Si sceglie uno dei quattro casi a seconda della visibilità che la inner class deve avere/dare
    1. static nested class: solo parte statica
    2. inner class: anche enclosing class, accessibile ovunque dall’outer
    3. local class: anche argomenti/variabili, accessibile da un solo metodo
    4. anonymous class: per creare un oggetto, senza un nuovo costruttore

Classi innestate

Progettazione e Sviluppo del Software

C.D.L. Tecnologie dei Sistemi Informatici

Danilo Pianini — danilo.pianini@unibo.it

Gianluca Aguzzi — gianluca.aguzzi@unibo.it

Angelo Filaseta — angelo.filaseta@unibo.it

Compiled on: 2025-12-05 — versione stampabile

back