Sei sulla pagina 1di 7

Estensione di Java Featherweight con le Interfacce

Antonio Intrieri, Stefano Zanini

14 febbraio 2011

Il linguaggio Java nella sua versione completa mette a disposizione oltre


alle classi anche le interfacce, che specicano i tipi dei metodi ma non la loro
implementazione. Le interfacce sono utili perché consentono una relazione di
sottotipo più ricca e non necessariamente strutturata ad albero: ogni classe
ha un'unica superclasse da cui eredita le instanze delle variabili e i corpi dei
metodi ma può implementare anche un numero qualsiasi di interfacce.
La presenza delle interfacce in Java costringe alla scelta di una presentazione
algoritmica della relazione di tipo, che dà ad ogni termine tipabile un unico
tipo (minimale). La ragione è l'interazione tra le espressioni condizionali e
le interfacce.
Lo scopo di questo approfondimento è mostrare come estendere Java Fea-
therweight (d'ora in poi FJ) con le interfacce in stile Java. Si vuole anche
mostrare che in presenza di interfacce la relazione di sottotipo non è neces-
sariamente chiusa rispetto ai tipi join.

1 Sintassi

La sintassi di FJ diventa la seguente:


T ::= CL | I
CL ::= class C1 extends C2 implements I {Tf; K M}
M ::= SG{ return t; }
K ::= C(Tf){super(f); this.fi = fj }
I ::= interface I1 extends I {SG}
SG ::= T m(Tx)
t ::= x | t.f | t.m(t) | new C(t) | (C)t
v ::= new C(v)
Si introduce dunque la metavariabile T che include sia le classi che le inter-
facce. T può essere usata laddove venga richiesta una dichiarazione di tipo.
Nella dichiarazione di una classe si aggiunge la possibilità di implementare
un qualsiasi numero di interfacce, e le interfacce stesse possono estenderne

1
altre. Inoltre, si aggiunge la metavariabile SG che contiene le signature dei
metodi.

2 Relazione di sottotipo

La relazione di sottotipo viene modicata come segue:


CT(C) = class C extends C1 implements I
C <: C1 C <: Ii ∀Ii ∈ I
mentre per le interfacce vale:
CT(I) = interface I extends I
I <: Ii ∀Ii ∈ I
Analogamente al tipo Object, è necessario denire un'interfaccia vuota I0
che non appartiene alla classtable e che è sopratipo di tutte le interfacce e
non ha nessun sopratipo

3 Estensione del Type System

3.1 Lookup delle signatures


Si aggiunge la funzione Sig che data un'interfaccia restituisce l'insieme delle
sue signatures e di quelle delle interfacce che estende.
CT(I) = interface I extends I1 . . . In {SG}
Sig(I) = SG ∪ Sig(I1 ) ∪ . . . ∪ Sig(In )
Per garantire la correttezza di tale funzione è necessario denire il caso
dell'interfaccia vuota I0 :
Sig(I0 ) = ∅

3.2 Funzioni elds, mtype e mbody


Le funzioni elds, mtype e mbody sono analoghe a quelle di FJ senza inter-
facce:
elds(Object) = •
CT(C) = class C extends D implements I {Tf; K M} elds(D) = Rg
elds(C) = Rg,Tf
CT(C) = class C extends D implements I {Tf; K M} R m(Rx){return t;} ∈ M
mtype(m,C) = R → R
CT(I) = interface I extends I {SG} R m(Rx) ∈ SG
mtype(m,I) = R → R

2
CT(C) = class C extends D implements I {Tf; K M} m is not dened in M
mtype(m,C) = mtype(m,D)
Si noti che non può vericarsi il caso in cui un metodo m non sia denito
nella signature della classe ma si trovi nelle signature delle interfacce che essa
implementa: infatti, ogni classe deve implementare tutti i metodi delle sue
interfacce. Inoltre, non è necessario fornire una regola per mtype di metodi
non presenti nell'interfaccia ma deniti in un suo sopratipo, dal momento
che la denizione di insieme delle signature comprende anche le signature
denite nelle soprainterfacce.
Per la funzione mbody l'aggiunta delle interfacce non modica signicati-
vamente le regole: l'unica dierenza infatti è di carattere sintattico. Anche
per il predicato di overriding l'unica dierenza consiste nell'includere nella
denizione la categoria sintattica T che raccoglie le interfacce e le classi.

3.3 Vincoli sull'implementazione di interfacce


CT(C) = class C extends D implements I{Tf, K M}
∀hT m(Tx)i ∈ Sig(I) mtype(m,C) = T → T
C OK-IMPL I
Una classe C deve implementare correttamente tutti i metodi delle interfacce
che dichiara di implementare.

3.4 Predicato di well-formedness


Un insieme di signature S è ben formato se vale il seguente predicato:
T m(Tx) ∈ S ∧ T' m(T'x) ∈ S ⇒ T' = T ∧ T' = T
S is well formed
Tale predicato vale se in presenza di due metodi con lo stesso nome essi
hanno anche la stessa signature. Tale eventualità può vericarsi per via
dell'ereditarietà multipla dei metodi delle interfacce.

3.5 Type system


Le regole di typing rimangono sostanzialmente invariate: l'unica dierenza
consiste nel fatto che i tipi dei campi, dei parametri formali e i tipi di ritorno
dei metodi possono anche essere interfacce, così come si dà la possibilità di
eettuare cast a interfacce. Vengono comunque riportate per completezza.
x:C ∈ Γ
Γ ` x:C
Γ ` t0 :C0 elds(C0 ) = T f
Γ ` t0 .fi :Ti

3
Γ ` t0 :T0 mtype(m,T0 ) = R → T Γ ` t:T T <: R
Γ ` t0 .m(t):T
elds(C)=R f Γ ` t:T T <: R
Γ ` new C(t):C
Γ ` t0 :R R<:T
Γ ` (T)t0 :T
Γ ` t0 :R T<:R T 6= R
Γ ` (T)t0 :T
Γ ` t0 :R T ≮:R R ≮:T stupid warning
Γ ` (T)t0 :T
Il sistema di tipo viene adeguato modicando le regole che deniscono la
correttezza dei metodi e delle dichiarazioni delle classi. In particolare, la
correttezza delle classi ha come ulteriore ipotesi la correttezza delle signature
dei metodi dichiarati nelle interfacce, e la presenza dell'implementazione di
tali metodi nel corpo della classe.
x : T, this:C ` t0 : R0 R0 <: T0
CT(C) = class C extends D implements I{Tf, K M} override(m, D, T → T0 )
T0 m(T x) {return t0 ;} OK in C
K = C(R g, T f ) {super(g);this.f = f;}
elds(D) = R g M OK in C Sig(I) is well formed C OK-IMPL I
class C extends D implements I{T f; K M} OK
La correttezza di un'interfaccia dipende invece solo dalla well formedness
delle signature dei metodi che dichiara e che eredita:
Sig(I) is well formed
Interface I extends I {SG} OK

4 Typing dell'espressione condizionale

4.1 Chiusura della relazione di sottotipo


Dal momento che in presenza di interfacce è possibile avere una relazione di
sottotipo più complessa e non vincolata dalla struttura ad albero, si pone
il problema di capire qual è il minimo sopratipo comune di due classi. In
FJ senza interfacce invece questo è banale, dal momento che nell'albero dei
tipi il minimo sopratipo comune a due classi è la radice del sottoalbero di
lunghezza minima che le contiene entrambe. In particolare, tale problema è
rilevante in presenza del costrutto condizionale, che richiede l'introduzione
dell'operazione di join tra tipi, che appunto restituisce il minimo sopratipo

4
Figura 1: Esempio di relazione di sottotipo tra interfacce

comune agli operandi. Si può dimostrare infatti che l'introduzione di inter-


facce compromette la chiusura della relazione di sottotipo rispetto al join.
E' facile trovare due controesempi. Nella situazione in gura 1, i sottotipi di
I1 sono I3 , I4 e I1 stesso: anché la relazione di sottotipo sia chiusa rispetto
al join, sarebbe necessario che join(I3 ,I4 ) avesse come risultato un sottoti-
po di I1 . Così non è, dato che il risultato è I0 . Nella situazione in gura

Figura 2: Esempio di relazione di sottotipo tra interfacce

2, join(I3 ,I4 ) non è denito, dal momento che non c'è un minimo sopratipo
comune.
Java risolve il problema tipando staticamente il condizionale con un tipo
join che sostanzialmente è l'intersezione tra i minimi sopratipi delle classi o
interfacce coinvolte. Considerando l'esempio in gura 2, per Java il tipo di
b?(I3 )T:(I4 )R sarebbe l'insieme delle signature comuni alle interfacce I1 e
I2 .
Formalmente, deniamo un operatore di intersezione nel seguente modo:
CT(I1 ) = I1 implements I1 {...}, CT(I2 ) = I2 implements I2 {...}
I1 ∩ I2 = {I : I ∈ I1 ∧ I ∈ I2 }
Questo operatore restituisce un insieme che contiene le interfacce estese da
entrambi gli argomenti. La regola di typing per le espressioni condizionali in
Java diventa quindi:
Γ ` e:boolean, Γ ` A:I1 , Γ ` B:I2
Γ ` (e?A:B) : I1 ∩ I2

5
In pratica, l'espressione assume un nuovo tipo che contiene l'unione dei meto-
di presenti nelle interfacce comuni. Tale regola di typing può essere vericata
col seguente esempio:

1 public interface I1 {
2 public String m1 ();
3 }
4
5 public interface I2 {
6 public String m2 ();
7 }
8
9 public interface I3 extends I2 , I1 {
10 public String m ();
11 public String m3 ();
12 }
13
14 public interface I4 extends I2 , I1 {
15 public String m ();
16 public String m4 ();
17 }
18
19 public class A implements I3 {
20 public String m (){ return "A" ; }
21 public String m1 (){ return "A "; }
22 public String m2 (){ return "A "; }
23 public String m3 (){ return "A "; }
24 }
25
26 public class B implements I4 {
27 public String m (){ return "B" ; }
28 public String m1 (){ return "B "; }
29 public String m2 (){ return "B "; }
30 public String m4 (){ return "B "; }
31 }

Con tale struttura di classi (v. gura 2) l'esecuzione del seguente programma
non segnala errori statici:

6
1 public class Main {
2 public static void main ( String args []){
3 ( true ? new A (): new B ()). m1 ();
4 ( false ? new A (): new B ()). m2 ();
5 }
6 }

In questo caso, ad esempio, il tipo che Java crea per tipare l'espressione
condizionale è Object&I2&I1. Si noti che, sebbene sia in I3 che in I4 è
presente il metodo m con la medesima signature, esso non può essere invocato
sulle espressioni condizionali di cui sopra perché di fatto tale metodo non è
lo stesso nelle due interfacce. Si noti anche che in mancanza di interfacce
comuni i metodi disponibili per l'espressione condizionale sono soltanto quelli
di Object.

Potrebbero piacerti anche