MokaByte Numero 26  -  Febbraio  1999
Classi annidate
strani innesti
  di
Piergiuseppe
Spinelli
Un'analisi semi-filosofica sulle classi annidate: tra ereditarietà ed aggregazione, un nuovo modo di incasinarsi le idee!


L'odierna diatriba filosofica sul polimorfismo è piuttosto complicata. Nella battaglia tra i puristi dell'ereditarietà multipla ed i pragmatisti (o semplificatori) che si contentano di quella singola offerta da Java si sono presto inserite nuove fazioni. Prima tra tutti Microsoft, con OLE/COM, ha sentenziato: vada per il polimorfismo multiplo (interfacce) ma si bandisca totalmente la complicazione dell'ereditarietà, le funzioni vanno tutte composte per aggregazione. Per tacere dei template e di tutti gli altri meccanismi di estensione o restringimento che ruotano attorno ai sistemi ad oggetti.
Forse per non buttare altra benzina sul fuoco della confusione attorno alle moderne tecniche di polimorfismo, SUN non ha voluto evidenziare tutte le potenzialità offerte dall'introduzione delle classi annidate nel proprio linguaggio. Uno sguardo alla documentazione fornita dal rilascio del JDK 1.1 basta per catalogare questa feature come una mera utility atta a semplificare la gestione degli eventi, in particolare quelli utilizzati dall'AWT. 
Ma le cose stanno davvero così? 

Campi a condivisione controllata con le inner-classes

Prima della versione 1.1 di Java, due oggetti appartenenti alla stessa classe avevano solo due possibilità rispetto alla condivisione di dati:

  • Nessuna condivisione: campi istanza (public, private, protected o con visibilità package)
  • Visibiltà globale per tutte le istanze della classe (campi static di classe)
Per differenziare i metodi di istanze della stessa classe o condividere il comportamento di oggetti di classi diverse non restava che ricorrere a reference ad istanze comuni di altre classi. Questa tecnica, in fondo, è solo l'emulazione ad oggetti dell'uso dei puntatori a funzione:
 

public abstract class Shared{
    public abstract static void sharedMethod();
}


public class Class1{
  private Shared ref;
        ...
        public Class1(Shared ref){this.ref=ref;}
        public void method(){ref.sharedMethod();}
        ...
}


public class Class2{
  private Shared ref;
      ...
      public Class2(Shared ref){this.ref=ref;}
      public void method(){ref.sharedMethod();}
      ...
}

Creando classi derivate da Shared, è possibile istanziare oggetti di classi diverse (Class1 e Class2) che condividono parte del proprio comportamento se inizializzate con reference alla stessa classe:
 

class Executor extends Shared{

public static void sharedMethod(){System.out.printf("Method of executor called!");}
}

...
Executor ex = new Executor();
Class1 c1 = new Class1(ex);
Class2 c2 = new Class2(ex);
...
c1.method();
c2.method();
...

Nell'esempio precedente le chiamate a c1.method() e c2.method() eseguono effettivamente lo stesso codice, ma il minimo che si possa dire di tale soluzione è che non è una tecnica ad oggetti (per quanto potrebbe far leva sulla nostalgia negli affezionati del linguaggio C).
Con l’introduzione delle classi annidate è possibile un maggior controllo sulla condivisione di campi e metodi, consentendo ad esempio la visibilità di alcuni dati solo a determinati oggetti.
Infatti un'oggetto di una inner class può essere creato solo nel contesto di una determinata istanza della classe outer. In altre parole non è possibile per una classe qualsiasi, anche appartenente al medesimo package, creare oggetti di una classe inner se non servendosi di un oggetto della classe outer che funge effettivamente da factory (per il concetto di factory vedere i pattern creazionali in http://st-www.cs.uiuc.edu/cgi-bin/wikic/wikic?DesignPatterns).
Questa particolarità ha introdotto, un po' silenziosamente visto che la documentazione SUN non lo sottolinea affatto, un concetto che definirei come fabbricazione, ovvero oggetti appartenenti alla stessa classe o anche a classi diverse riuniti in insiemi determinati da una fabbrica comune. Le categorie di condivisione passano, quindi, dalle classiche due:
  • class (condiviso da tutti gli oggetti di una classe)
  • instance (proprio del singolo oggetto)
a tre, con l'aggiunta di:
  • factory (proprio di tutti gli oggetti creati dalla stessa istanza di outer class).
Non si tratta, in effetti, di sottoinsiemi di oggetti della stessa classe in quanto una outer class può definire diverse classi annidate che, comunque, condivideranno:
  • tutti i campi propri dell'oggetto factory
  • tutti i campi statici della classe dell'oggetto factory.
  • i metodi (statici e non) della particolare istanza della classe factory (o di una sua sottoclasse) con tutte le regole di polimorfismo applicate
Le tre categorie di condivisione sono esemplificate nella seguente tabella:
 
Da Java 1.0 Da Java 1.1
public class NoShared{
 
 

 public int campo1;
 
 

 protected int campo2;
 
 

 private int campo3;
 
 

 int campo4;
 
 

 …
 
 

}

INSTANCE:

Campi visibili solo ai singoli oggetti

public class GroupFactory{
 
 

 static int campoS;
 
 

 public int campo1;
 
 

 protected int campo2;
 
 

 private int campo2;
 
 

 …
 
 

 public Grouped getGrouped(){

   return new Grouped();

 }

 public class Grouped {

   …

   public void modificaDatiComuni(){

       synchronized(Factory.this){

       ...

       }

   }

 }

}

FACTORY:

Campi visibili da tutti gli oggertti creati dalla stessa GroupFactory

public class Shared{
 
 

 public static int campo1;
 
 

 protected static int campo2;
 
 

 private static int campo3;
 
 

 static int campo4;
 
 

 …
 
 

}

CLASS:

Campi visibili da tutti gli oggetti appartenenti alla classe

Ogni outer class è, di fatto, una factory anche senza implementare un metodo come getGrouped: è infatti sufficiente vestire l'operatore new con il nome della outer class:
 

GroupFactory gf=new GroupFactory();
GroupFactory.Grouped = gf.new Grouped();
In caso di omonimia tra campi di classi innestate tra loro, è possibile utilizzare la sintassi OuterClass.this:
 

public class GroupFactory{
        int campo;
        …
        public class Grouped {
                int campo;

               public void m(){
       System.out.println("Inner: "+campo);
System.out.println("Outer: "+GroupFactory.this.campo);
                }
        }
}

Utilizzando codice in versione 1.2, vediamo come è possibile creare una siutazione di polimorfismo trasversale usando una tecnica mista tra l'ereditarietà e l'inclusione stile OLE, dove istanze della stessa classe Inner1 eseguono codice diverso per il metodo changeStatus() mentre oggetti di classi diverse (Inner1 ed Inner2) ereditano il polimorfismo delle rispettive Outer Classes.
Il paradosso è rilevabile nelle seguenti due proprietà:
  • il metodo changeStatus() è comune a oggetti di classi diverse allocate nel contesto di una stessa istanza dell'Outer Class
  • il metodo changeStatus() è diverso in oggetti appartenenti alla stessa classe creati da due istanze di Outer Class in rapporto di ereditarietà tra loro.
Ecco il codice che implementa questo apparente paraddosso:
 

public class Factory {
    protected String SharedMethod(){
     return "Shared from 'Factory'";
    }

public class Inner1{
 String name;
  Inner1(String name){this.name=name;}
  public String getInnerName(){return "Inner1: '"+name+"'";}
  public final void method(){
    System.out.print(getInnerName());
    System.out.println(" - Behaviour: " + SharedMethod());
  }
}
 

public class Inner2{
  String name;
  Inner2(String name){this.name=name;}
  public String getInnerName(){return "Inner2: '"+name+"'";}
  public final void method(){

  System.out.print(getInnerName());
  System.out.println(" - Behaviour: " + SharedMethod());
}
}
}

Segue una classe di test con, a destra, il relativo output:
 
Codice di Test Output
class OtherFactory extends Factory{

        protected String SharedMethod(){
      return "Shared from 'OtherFactory'";
        }
}

public class InnerTest{
        public static void main(String[] pars){
        Factory f=new Factory();
        Factory of=(Factory)new OtherFactory();
        Factory.Inner1 if1=f.new Inner1("if1");
        Factory.Inner2 if2=f.new Inner2("if2");
        Factory.Inner1 iof1=of.new Inner1("iof1");
        Factory.Inner2 iof2=of.new Inner2("iof2");
         if1.method();
          if2.method();
          iof1.method();
           iof2.method();
       }

}

C:\JavaClasses>java InnerTest

Inner1: 'if1' - Behavior: Shared from 'Factory'

Inner2: 'if2' - Behavior: Shared from 'Factory'

Inner1: iof1 - Behavior: Shared from 'OtherFactory'

Inner2: iof2 - Behavior: Shared from 'OtherFactory'

C:\JavaClasses>

 

In altre parole, potremmo definire polimorfismo incrociato (nel contesto di lunguaggi con ereditarietà singola), le capacita di istanziare:

  • oggetti appartenenti alla stessa classe con comportamento parzialmente diversificato
  • oggetti appartenenti a classi diverse con comportamento parzialmente condiviso.
Il mio articolo di novembre, Caffè e Prezzemolo, utilizza una classe ConnectionTask annidata dentro una classe Twins (entrambe nel contesto di una classe Proxy). In questo caso la classe Twins presenta un'interfaccia in grado di sostenere due comunicazioni parallele (con un client ed un server); per far questo compone (per usare una terminologia OLE) due oggetti di tipo ConnectionTask, ognuno in grado di occuparsi di uno dei due lati della comunicazione. La particolarità sta nel fatto che la classe ConnectionTask eredita da Thread. Quindi una classe innestata può ereditare da qualsiasi altra classe e contemporaneamente condividere tutti i methodi della propria outer class: in senso lato si tratta di una forma multipla di polimorfismo che resta a metà tra l'ereditarietà e la composizione!
Viste sotto questa luce le classi annidate appaiono come una non piccola innovazione, certo più profonda che una semplice utility per semplifitare la gestione degli eventi della AWT, anche se il loro contributo immediato alla questione del polimorfismo potrebbe essere un ulteriore aumento della confusione.

 
Piergiuseppe Spinelli svolge attività di analista/programmatore dal 1980. Si è occupato di training e di sviluppo di sistemi, particolarmente nel campo della supervisione di processo. Ha lavorato per aziende dei gruppi Saint Gobaing, Angelini, Procter&Gamble, Alcatel/Telettra, SIV e per vari enti pubblici e privati. 
E' contattabile all'indirizzo spinellip@sgol.it

 
 


MokaByte Web  1999 - www.mokabyte.it

MokaByte ricerca nuovi collaboratori. 
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it