Meccanismi avanzati: classi innestate (materiale di supporto)

Progettazione e Sviluppo del Software

C.D.L. Tecnologie dei Sistemi Informatici

Gianluca Aguzzi — gianluca.aguzzi@unibo.it

Angelo Filaseta — angelo.filaseta@unibo.it

versione stampabile

Riconoscimenti

  • Questo materiale è ampiamente basato su quello realizzato dai Prof. Mirko Viroli e Roberto Casadei, che ringrazio.

  • Ogni errore riscontratovi è esclusiva responsabilità degli autori di questo documento.

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: tipo, per 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
}

Classi innestate statiche – accesso

Uso

  • L’accesso alle classi/interfacce innestate statiche avviene con sintassi Outer.StaticNested (ovvero, come se fosse un membro 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)
  • … meglio comunque non abusare di questo meccanismo

Caso 1

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

Esempio

  • Counter, Counter.Bidirectional, Counter.Multi

Note

Un sintomo della possibilità di usare le classi nested per questo caso è quando ci si trova a costruire classi diverse costuite da un nome composto con una parte comune (Counter, BiCounter, MultiCounter)

Classe Counter e specializzazioni innestate (1/2)

public class Counter {
	private int value; // o protected..

	public Counter(int initialValue) {
		this.value = initialValue;
	}

	public void increment() {
		this.value++;
	}

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

	public static class Multi extends Counter {
	    ... // solito codice
	}

	public static class Bidirectional extends Counter {
	    ... // solito codice
	}
}

Classe Counter e specializzazioni innestate (2/2)

public class Counter {

	...
	// Codice della classe senza modifiche..
	public static class Multi extends Counter {

		public Multi(int initialValue) {
			super(initialValue);
		}

		public void multiIncrement(int n) {
			for (int i = 0; i < n; i++) {
				this.increment();
			}
		}
	}...

	public static class Bidirectional extends Counter{
	    ... // solito codice
	}
}

Uso di Counter e specializzazioni innestate

public class UseCounter {
	public static void main(String[] args) {
		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();
		}
	}
}

Caso 2

Necessità di una classe separata ai fini di ereditarietà

In una classe potrebbero servire sotto-comportamenti che debbano:

  • implementare una data interfaccia
  • estendere una data classe

Esempio

  • Range, Range.Iterator

Nota

In tal caso spesso tale classe separata non deve essere visibile dall’esterno, quindi viene indicata come private

Classe Range e suo iteratore (1/2)

public class Range implements Iterable<Integer> {

    final private int start;
    final private int stop;

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

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

    private static class Iterator 
              implements java.util.Iterator<Integer>{
        ...
    }
}

Classe Range e suo iteratore (2/2)

public class Range implements Iterable<Integer> {
    ...
    private static class Iterator 
              implements java.util.Iterator<Integer> {
        private int current;
        private final int stop;
        
        public Iterator(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() { }
    }
}

Uso di Range

public class UseRange {
    public static void main(String[] s) {
    	for (final int i: new Range(5,12)) {
    	    System.out.println(i);
    	    // 5 6 7 8 9 10 11 12
    	}
    }
}

Caso 3

Necessità di comporre una o più classi diverse

  • Ognuna realizzi un sotto-comportamento
  • Per suddividere lo stato dell’oggetto
  • Tali classi non utilizzabili indipendentemente dalla outer

Esempio tratto dal Collection Framework

  • Map, Map.Entry
  • (una mappa è “osservabile” come set di entry)

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 alle altre classi del package

Il caso delle java.util.Map

JCF – struttura semplificata

Map

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!
}

Implementazione mappe – UML

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);
    
    }
}

Uso di Map.Entry

public class UseMap2 {	
	public static void main(String[] args) {
		// Al solito, uso una incarnazione, ma poi lavoro sull'interfaccia
		final Map<Integer, String> map = new HashMap<>();
		// Una mappa è una funzione discreta
		map.put(345211, "Bianchi");
		map.put(345122, "Rossi");
		map.put(243001, "Verdi");

		for (final Map.Entry<Integer, String> entry : map.entrySet()) {
			System.out.println(entry.getClass());
			System.out.println(entry.getKey());
			System.out.println(entry.getValue());
			entry.setValue(entry.getValue()+"...");
		}
		System.out.println(map);
		// {345211=null, 243001=null, 345122=null}
	}
}

La classe AbstractMap

In modo simile a AbstractSet

  • Fornisce una implementazione scheletro per una mappa
  • Necessita di un solo metodo da implementare: entrySet()
  • Così facendo si ottiene una mappa iterabile e non modificabile
  • Per fare modifiche è necessario ridefinire altri metodi..

Una semplice specializzazione di AbstractMap

public class CapitalsMap extends AbstractMap<String,String>{

	private static final Set<Map.Entry<String,String>> set;
	
	// inizializzatore statico.. 
	// usato per inizializzare i campi statici
	static{ 
		// costruisce il valore di set
		set = new HashSet<>();
		set.add(new AbstractMap.SimpleEntry<>("Italy","Rome"));
		set.add(new AbstractMap.SimpleEntry<>("France","Paris"));
		set.add(new AbstractMap.SimpleEntry<>("Germany","Berlin"));
	}
	
	public CapitalsMap(){}
	
	// Questo è l'unico metodo che è necessario implementare
	public Set<java.util.Map.Entry<String, String>> entrySet() {
		return set; 
	}
	
}

UseCapitalsMap

public class UseCapitalsMap {
	public static void main(String[] args){
		CapitalsMap cmap = new CapitalsMap();
		System.out.println("Capital of Italy: "+cmap.get("Italy"));
		System.out.println("Capital of Spain: "+cmap.get("Spain"));
		System.out.println("All CapitalsMap: "+cmap);
		
		// Iterazione "lenta" su una mappa
		for (final String key: cmap.keySet()){
			System.out.println("K,V: "+key+" "+cmap.get(key));
		}
		
		// Iterazione veloce su una mappa
		for (final Map.Entry<String, String> entry: cmap.entrySet()){
			System.out.println("E: "+entry+" "+entry.getKey()+" "+entry.getValue());
		}
	}
}

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 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(){
			// si usa l'oggetto di outer..
			this.j = this.j + Outer.this.i;
		}
		
		public int getValue(){
			return this.j;
		}
	}
}

Uso di Inner e Outer

public class UseOuter {
	public static void main(String[] args) {
		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, più..

  • …quando è 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, che però ben usufruirebbe del riferimento all’oggetto di Range che l’ha generata

Una variante di Range

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

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

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

	private class Iterator implements java.util.Iterator<Integer> {
		private int current;

		public Iterator() {
			this.current = Range2.this.start; // this.current = start
		}

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

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

		public void remove() { }
	}
}

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)
  • In più, la LocalClass “vede” anche le variabili nello scope del metodo in cui è definita, usabili solo se final, o se “di fatto finali”
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 Range3 implements Iterable<Integer> {
    private final int start;
    private final int stop;

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

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

            public Iterator() {
                this.current = Range3.this.start;
            }

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

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

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

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

  • Mai viste usarle.. si usano invece le classi anonime

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; }
        }
    }
}

Range tramite classe anonima – la soluzione ottimale

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

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

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

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

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

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

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

Altro esempio: classe anonima da Comparable

public class UseSort {
	public static void main(String[] args) {
		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);
	}
}

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