Le interfacce

Un metodo astratto è un metodo senza corpo, con un ";" dopo l'intestazione.

Una interfaccia  (interface) in Java ha una struttura simile a una classe, ma può contenere SOLO metodi d'istanza astratti e costanti (quindi non può contenere costruttori, variabili statiche, variabili di istanza e metodi statici).

Ad esempio, questa è la dichiarazione dell'interfaccia java.lang.Comparable

public interface Comparable<T>{
    public int compareTo(T  o);
}

Spesso l'interfaccia comprende anche una descrizione informale del significato dei metodi (che chiameremo specifica o contratto).

Ad esempio, nella API di Comparable, si vede che al metodo compareTo è associata una precisa interpretazione: esso definisce un ordinamento totale sugli oggetti della classe.




Implementare un'interfaccia

Si può dichiarare che una classe implementa (implements) una data interfaccia: in questo caso deve realizzare tutti i suoi metodi astratti, fornendo dei metodi con la stessa intestazione (e con il corpo, naturalmente). La realizzazione di un metodo deve rispettare la specifica del corrispondente metodo astratto.

Un primo esempio è l'interfaccia Comparable<T>.

public interface Comparable<T>{

    int compareTo(T o);

}

Comparable è un'interfaccia generica, poiché ha un parametro di tipo T.

Un'interfaccia generica può essere istanziata come nelle seguenti dichiarazioni di classi.

public class Mela implements Comparable<Mela>{
    private int grammi;
    private String tipo;

    public Mela(String tipo, int grammi){
        this.tipo = tipo;
        this.grammi = grammi;
    }

    public int compareTo(Mela m){
        return grammi - m.grammi;
    }
}

public class Pera implements Comparable<Pera>{
    private int grammi;
    private String tipo;

    public Pera(String tipo, int grammi){
        this.tipo = tipo;
        this.grammi = grammi;
    }

    public int compareTo(Pera p){
        return grammi - p.grammi;
    }
}

public class TestMelaPera{

    public static void main (String [] args){
        Mela mela = new Mela("golden",135);
        Pera pera = new Pera("williams",120);
        int comp = pera.compareTo(mela); // errore di compilazione
    }
}




A che servono le interfacce?

Le interfacce possono essere utilizzate:
  • per definire Tipi di Dati Astratti (visti nel corso di MP); si pensi al TDA degli insiemi, definiti come entità matematiche caratterizzate dalle usuali operazioni (unione, appartenenza, ...), e a diverse possibili implementazioni del TDA (liste con/senza ripetizioni, array, tabelle hash, alberi binari di ricerca, ...);
  • come contratto tra chi implementa una classe e chi la usa: le due parti possono essere sviluppate e compilate separatamente;

  • per evidenziare funzionalità comuni a più classi, sopperendo alle limitazioni dell'ereditarietà singola (come nell'esempio di Comparable);

 

Interfacce e Classi Astratte

A prima vista potrebbe non essere chiara le necessità di disporre sia di interfacce che di classi astratte.

Si noti che:

  • Una classe (astratta o concreta) può implementare più interfacce ma estendere solo una classe (astratta o concreta)
  • Una classe (astratta o concreta) può essere usata per "fattorizzare" codice comune alle sue sottoclassi, una interfaccia non può contenere codice
  • Una classe astratta può contenere chiamate di metodi astratti prescindendo dalla loro implementazione, una classe concreta non può usare metodi astratti, una interfaccia non può contenere codice




Regole per l'uso di interfacce

Nell'uso delle interfacce in un programma, ricordarsi delle seguenti regole:
  • Possiamo dichiarare una variabile indicando come tipo un'interfaccia:
  • Non possiamo istanziare un'interfaccia: 
  • Ad una variabile di tipo interfaccia possiamo assegnare solo istanze di classi che implementano l'interfaccia:
  • Su di una variabile di tipo interfaccia possiamo invocare solo metodi dichiarati nell'interfaccia (o nelle sue "super-interfacce").



Dichiarazione di interfacce

Si dichiara come una classe, ma con la parola chiave interface
 
public interface <Int> [extends <Int1>, <Int2>, ...] {
   <tipo1> <var1>= <val1>;

   ...

   <tipoN> <varN> = <valN>;

   <tipo-res1> <metodo1> ( <lista-parametri1> );

   ...

   <tipo-resM> <metodoM> ( <lista-parametriM> );

}

Si noti che:

  • Le variabili devono essere inizializzate e non possono essere modificate successivamente: anche se non sono dichiarate final di fatto sono delle costanti;
  • I metodi sono tutti astratti: infatti al posto del corpo c'è solo un punto e virgola;

  • I metodi dichiarati in una interfaccia sono sempre public. Di conseguenza, i corrispondenti metodi di una classe che implementa l'interfaccia devono essere public.

  • Una interfaccia può estendere una o più interfacce (non classi), indicate dopo la parola chiave extends. Per le interfacce non vale la restrizione di "ereditarietà singola" che vale per le classi.




Le relazioni extends e implements

  • Ogni classe estende (extends) una sola altra classe (Object se non specificata);
  • Una interfaccia può estendere (extends) una o più interfacce:

public interface <Int> extends <Int1>,<Int2>, ...{
     ...
}
  • La gerarchia di ereditarietà singola delle classi e la gerarchia di ereditarietà multipla delle interfacce sono completamente disgiunte.
  • Una classe può implementare (implements) una o più interfacce:


public class <nomeClasse> extends <nomeSuperClasse>
       implements <Int1>, <Int2>, ..., <Intn> {

    ...
}
 

  • In questo caso <nomeClasse> deve fornire una realizzazione per tutti i metodi delle interfacce <Int1>, <Int2>, ... che implementa, nonché per i metodi di eventuali super-interfacce da cui queste ereditano. 



Interfacce come tipi di dati astratti

Come sappiamo, in Java ogni classe definisce un tipo di dati, i cui elementi sono le istanze della classe (la cui struttura è determinata dalle variabili d'istanza), e le cui operazioni sono i metodi. 

Una interfaccia definisce invece un tipo di dati astratto, di cui fornisce la specifica delle operazioni: la struttura dei suoi elementi e il modo in cui le operazioni sono effettivamente definite verrà determinato dal tipo di dati (la classe) che realizza (implements) l'interfaccia.

Struttura della realizzazione di un tipo di dato nelle API di Java

La definizione di un tipo di dato risulta estremamante flessibile ed elegante se si usa una particolare struttura di classi:

  • un'interfaccia che definisce il nome del tipo, le firme dei metodi e le eventuali costanti pubbliche;

  • una classe astratta che
    • implementa l'interfaccia,
    • contiene le variabili di istanza comuni a tutte le implementazioni
    • e realizza i metodi comuni a tutte le implementazioni;
  • la classi concrete che estendono la classe astratta e implementano le varie realizzazioni
  • una classe non istanziabile che realizza i tutti e soli i metodi statici.
N.B. per rendere una classe concreta xxx non istanziabile basta mettere come unico costruttore

         private xxx (){}

Esempio: i numeri complessi

Il tipo Numero Complesso può essere implementato correttamente in Java nel modo seguente:

  • un'interfaccia Complex che definisce le firme dei metodi re(), im(), abs(), arg() e conjugate();
  • una classe astratta AbstractComplex che implementa Complex e realizza il metodo toString() e altri metodi comuni a tutte le implementazioni;
  • due classi concrete CartesianComplex e TrigComplex che estendono AbstractComplex e rappresentano i complessi rispettivamente come parte reale e parte immaginaria oppure come modulo e argomento.
  • una classe Complexes non istanziabile che realizza i metodi statici.

Si veda il documento PDF nei Complementi.

Esempio: Java Collection

Nei complementi è riportato parte di codice sorgente java.util (2003).

Può essere interessante vedere come è implementata la struttura delle classi schematizzata nel seguente diagramma.




Interfacce come "contratto"

Le interfacce forniscono un supporto linguistico alla nozione di contratto tra chi usa gli oggetti di una classe, e chi realizza la classe stessa.
  • Chi scrive la porzione di programma che usa gli oggetti della classe ne ha una visione astratta: conosce solo le firme dei metodi da usare e una descrizione (informale) delle operazioni, indipendentemente dalla realizzazione. 
  • Chi scrive la classe deve fornire una realizzazione dei metodi dell'interfaccia, fornendo una rappresentazione concreta degli oggetti.

Questo permette di procedere in parallelo alla scrittura delle due parti, e di integrarle alla fine.

Inoltre la classe che implementa l'interfaccia può essere sostituita con un'altra più efficiente (o più conveniente...), senza modificare il programma che la utilizza.