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. |