Le classi definiscono gli oggetti in termini di dati e
metodi che operano sui dati.
Le interfacce permettono di utilizzare funzionalità
offerte dall’ereditarietà
multipla. Sono essenzialmente la promessa che la classe implementerà
determinati metodi, con determinate firme.
Anche le interfacce possono essere estese, utilizzando la
parola riservata extends.
Diversamente dalle classi, un’interfaccia può
estendere più di un interfaccia:
interface inter1 extends inter2, inter3 {
//…….
}
se si considera che una classe implementi un’interfaccia ed estenda un'altra classe, ènecessaria l’ereditarietà multipla, cioè è necessaria una nuova classe possa essere utilizzata ovunque ciò sia permesso sia dai tipi della sua superclasse sia da quelli della sua superinterfaccia.
Considerando la dichiarazione seguente:
interface W {}
interface X exteds W {}
class Y implementssW {}
class Z exteds Y implements X {}
essa si avvicina moltissimo al concetto di ereditarietà a diamante. Tuttavia non possono sorgeer dubbi sull’uso dei campi d iX o di campi di Y, in quanto X, essendo un’interfaccia, è priva di campi, e dunque solo i campi di Y sono disponibili.
Le interfacce, contrariamente alle classi, non hanno al vertice della gerarchia un’unica interfaccia, che sia l’analogo della classe Object. Tuttavia è possibile un’espressione di qualsiasi tipo di interfaccia a un metodo che abbia un parametro di tipo Object, in quanto tale oggetto deve essere di qualche classe, e ogni classe è una sottoclasse di Object.
Il conflitto dei nomi sorge quando un metodo
con lo stesso nome compare in più di un interfaccia.
![]() |
Se le interfacce X e Y contengono metodi con lo stesso nome, ma numero o tipi di parametri differenti, nell’interfaccia Z (che estende X e Y)saranno utilizzabili, grazie al sovraccaricamento, entrambi i metodi con lo stesso nome ma con diffrenti signature. |
![]() |
Se i due metodi hanno la stessa signatura, l’interfaccia Z avrà un unico metodo con tale segnatura. |
![]() |
Se i metodi differiscono solo dal tipo di ritorno , esse non sono implementabili. |
![]() |
Se i due metodi differiscono solo nei tipi delle eccezioni che essi possono generare, la classe deve soddisfare le dichiarazioni dei due metodi con la stessa firma(nome e parametri), salvo far sorgere eccezioni differenti. Ma due metodi di una classe non possono differire solo nelle eccezioni che essi lanciano, pertanto ci deve essere un’ unica implementazione che soddisfi entrambe le clausole throws. |
Ecco un esempio:
interface X {
void setup() throws SomeException;
}
interface Y {
void setup();
}
class Z implements X, Y {
public void setup() {
//…
}
}
la classe Z può fornire un'unica implementazione che soddisfi sia X.setup() che Y.setup().
Un metodo può lanciare meno eccezioni di quelle dichiararte dalla sua superclasse: non occorre che Z.setup() lanci un eccezione di tipo SomeException come invece è previsto dalla dichiarazione di X.setup().
![]() |
Se i due metodi si differenziano per la lista di eccezioni, in modo che non sia possibile trovare un’implementazione che sìoddisfi entrambe le segnature delle interfacce dei metodi, le due interfacce non sono estendibili con un’interfaccia e una classe non può neppure implementare le due interfacce. |
Le costanti delle interfacce possono avere lo stesso
nome e possono essere unite nel grafo dell’ereditarietà, utilizzando però il
nome delle costanti in modo qualificato: se due interfaccie hanno una stessa
costante con valori differenti e una classe che implementi entrambe le
interfacce, è necessario utilizzare i nomi espliciti Interfaccia1.costante o
Interfaccia2.costante pre distinguerli, in quanto il nome semplice della
costante potrebbe essere ambiguo.
Nelle interfacce i nomi sono descritti in forma
astratta; tuttavia le interfacce sono interessanti solo se vengono
implementate.
Alcune interfacce sono puramente astratte; esse non hanno un’implementazione generale utile, ma devono avere un’implementazione originale per ogni nuova classe. Molte interfacce, tuttavia, possono avere diverse implementazioni utili.
Ecco un esempio, una semplice implementazione di Attributed che utilizza la classe java.util.Hashtable:
import java.util.* ;
class AttributedImpl implements Attributed
{
protected Hashtable attrTable = new Hashtable();
public void add( Attr newAttr ) {
attrTable.put( newAttr.nameOf() , newAttr );
}
public Attr find( String name ) {
return (Attr)attrTable.get(name);
}
public Attr remove( String name ) {
return (Attr)attrTable.remove(name);
}
public Enumeration attrs(){
return attrTable.elements();
}
}
l’inizializzatore per attrTable crea un oggetto Hashtable per memorizzare gli attributi. La classe Hashtable utilizza il metodo hashCode dell’oggetto per calcolare il codice hash di qualunque oggetto sia dato come chiave. Non è necessario nessun metodo hashCode esplicito, poiché un’implementazione adatta di hashCode viene già fornita da String.
Quando si aggiunge un nuovo attributo, l’oggetto Attr viene memorizzato nella tabella hash secondo il suo nome; da quel momento è possibile utilizzare facilmente la tabella, in base al nome, per trovare o rimuovere attributi.
Il metodo attrs restituisce una classe Enumeration che elenca tutti gli attributi presenti nell’insieme. La classe Enumeration è una superclasse astratta definita in java.util per classi di collezioni come Hashtable. L’uso di Enumeration ha un vantaggio: è più facile implementare Attributed utilizzando una clase standard di di collezioni come Hashtable che utilizza Enumeration, in modo che il suo valore di ritorno sia utilizzabile direttamente.
Vi sono due differenze fondamentali tra classi astratte
e interfacce:
le interfacce forniscono una forma di ereditarietà multipla, poiché è possibile implementare interfacce mutiple. Una clsse può estendere solo un'altra classe, anche se quella classe contiene solo metodi abstract.
Una classe abstract può avere un’implementazione parziale, parti protected, metodi static, e così via, mentre le interfacce possono utilizzare solo metodi public, privi di implementazione, e costanti.