Introduzione alle Interfacce in Java

Concetti Chiave
  • Le interfacce in Java permettono di definire un contratto che le classi devono seguire, senza specificare l'implementazione.
  • Le interfacce possono essere implementate da più classi, consentendo il polimorfismo.
  • Le classi possono implementare più interfacce, permettendo una maggiore flessibilità nella progettazione del software.

Interfacce

Usando la parola chiave interface, è possibile astrarre completamente l'interfaccia di una classe dalla sua implementazione.

Cioè, usando interface, possiamo specificare cosa deve fare una classe, ma non come lo fa. Le interfacce sono sintatticamente simili alle classi, ma non hanno variabili di istanza e, come regola generale, i loro metodi sono dichiarati senza alcun corpo. In pratica, ciò significa che possiamo definire interfacce che non fanno assunzioni su come sono implementate. Una volta definita, qualsiasi numero di classi può implementare un'interfaccia. Inoltre, una classe può implementare un numero qualsiasi di interfacce.

Per implementare un'interfaccia, una classe deve fornire l'insieme completo dei metodi richiesti dall'interfaccia. Tuttavia, ogni classe è libera di determinare i dettagli della propria implementazione. Fornendo la parola chiave interface, Java consente di utilizzare appieno il principio del polimorfismo: una sola interfaccia, molte implementazioni.

Le interfacce sono progettate per supportare la risoluzione dinamica dei metodi a tempo di esecuzione.

Normalmente, affinché un metodo possa essere chiamato da una classe a un'altra, entrambe le classi devono essere presenti al momento della compilazione affinché il compilatore Java possa controllare che le firme dei metodi siano compatibili.

Questo requisito, di per sé, crea un ambiente statico e non estensibile. Inevitabilmente, in un sistema di questo tipo, questa funzionalità viene spinta sempre più in alto nella gerarchia delle classi affinché i meccanismi siano disponibili per sempre più sottoclassi. Le interfacce sono progettate per evitare questo problema.

Disconnettono la definizione di un metodo o di un insieme di metodi dalla gerarchia di ereditarietà. Poiché le interfacce si trovano in una gerarchia diversa dalle classi, è possibile che classi non correlate nella gerarchia delle classi implementino la stessa interfaccia. È qui che si realizza il vero potere delle interfacce.

Definire un'interfaccia

Un'interfaccia è definita in modo molto simile a una classe. Questa è una forma generale semplificata di un'interfaccia:

access interface nome {
    return-type nomeMetodo1(parametri);
    return-type nomeMetodo2(parametri);

    type final-nomeVar1 = valore;
    type final-nomeVar2 = valore;
    //...
    return-type nomeMetodoN(parametri);
    type final-nomeVarN = valore;
}

Quando non è incluso alcun modificatore di accesso, il risultato predefinito è l'accesso di default, e l'interfaccia è disponibile solo per gli altri membri del package in cui è dichiarata.

Quando viene dichiarata come public, l'interfaccia può essere utilizzata da codice al di fuori del proprio package. In questo caso, l'interfaccia deve essere l'unica interfaccia public dichiarata nel file, e il file deve avere lo stesso nome dell'interfaccia.

nome è il nome dell'interfaccia, e può essere qualsiasi identificatore valido. Si noti che i metodi dichiarati non hanno corpo. Terminano con un punto e virgola dopo l'elenco dei parametri. Sono, essenzialmente, metodi astratti. Ogni classe che include una tale interfaccia deve implementare tutti i metodi.

Consiglio

Implementazioni di default e metodi statici

Prima di continuare, è necessario notare un punto importante.

JDK 8 ha aggiunto una funzionalità alle interfacce che ha apportato un cambiamento significativo alle loro capacità.

Prima di JDK 8, un'interfaccia non poteva definire alcuna implementazione. Questo è il tipo di interfaccia mostrato nell'esempio semplificato precedente, in cui nessuna dichiarazione di metodo fornisce un corpo. Quindi, prima di JDK 8, un'interfaccia poteva definire solo cosa, ma non come.

JDK 8 ha cambiato questo aspetto. A partire da JDK 8, è possibile aggiungere un'implementazione di default a un metodo dell'interfaccia. Inoltre, JDK 8 ha anche aggiunto metodi statici all'interfaccia, e a partire da JDK 9, un'interfaccia può includere metodi private.

È ora quindi possibile per le interfacce specificare un certo comportamento. Tuttavia, tali metodi costituiscono, in sostanza, funzionalità di uso speciale, e l'intento originario dietro le interfacce rimane. Pertanto, come regola generale, continueremo spesso a creare e usare interfacce senza fare uso di queste nuove funzionalità.

Per questo motivo, si inizierà descrivendo l'interfaccia nella sua forma tradizionale. Le nuove funzionalità delle interfacce sono descritte nelle prossime lezioni.

Come mostra la forma generale, le variabili possono essere dichiarate all'interno delle dichiarazioni di interfaccia. Sono implicitamente final e static, il che significa che non possono essere modificate dalla classe che implementa. Devono anche essere inizializzate. Tutti i metodi e le variabili sono implicitamente public.

Ecco un esempio di definizione di interfaccia. Dichiara un'interfaccia semplice che contiene un metodo chiamato callback() che accetta un singolo parametro intero.

interface Callback {
    void callback(int parametro);
}

Implementare le Interfacce

Una volta che un'interfaccia è stata definita, una o più classi possono implementarla.

Per implementare un'interfaccia, si include la clausola implements in una definizione di classe e poi si creano i metodi richiesti dall'interfaccia.

La forma generale di una classe che include la clausola implements è la seguente:

class nomeClasse [extends superClasse] [implements interfaccia [,interfaccia...]] {
    // corpo della classe
}

Se una classe implementa più di un'interfaccia, le interfacce sono separate da una virgola.

Se una classe implementa due interfacce che dichiarano lo stesso metodo, allora lo stesso metodo sarà usato dalle due interfacce. I metodi che implementano un'interfaccia devono essere dichiarati public. Inoltre, la firma del tipo del metodo implementato deve corrispondere esattamente alla firma del tipo specificata nella definizione dell'interfaccia.

Ecco un piccolo esempio di classe che implementa l'interfaccia Callback vista in precedenza:

class Cliente implements Callback {
    // Implementa l'interfaccia di Callback
    public void callback(int p) {
        System.out.println("callback chiamato con " + p);
    }
}

Si noti che callback() è dichiarato usando il modificatore di accesso public.

Nota

I metodi di interfaccia devono essere public

Quando si implementa un metodo di un'interfaccia, deve essere dichiarato come public.

È sia consentito che comune per le classi che implementano interfacce definire membri aggiuntivi propri.

Ad esempio, la seguente versione di Cliente implementa callback() e aggiunge il metodo nonInterfacciaMetodo():

class Cliente implements Callback {
    // Implementa l'interfaccia di Callback
    public void callback(int p) {
        System.out.println("callback chiamato con " + p);
    }

    void nonInterfacciaMetodo() {
        System.out.println("Le classi che implementano interfacce " +
                           "possono anche definire altri membri, anch'essi.");
    }
}

Accedere alle Implementazioni tramite Riferimenti a Interfaccia

È possibile dichiarare variabili come riferimenti a oggetti che usano un'interfaccia piuttosto che un tipo di classe.

Qualsiasi istanza di una classe che implementa l'interfaccia dichiarata può essere riferita da tale variabile.

Quando si chiama un metodo tramite uno di questi riferimenti, la versione corretta sarà chiamata in base all'effettiva istanza dell'interfaccia a cui si riferisce.

Questa è una delle caratteristiche chiave delle interfacce. Il metodo da eseguire viene cercato dinamicamente a tempo di esecuzione, permettendo che le classi siano create più tardi rispetto al codice che chiama i metodi su di esse.

Il codice chiamante può invocare tramite un'interfaccia senza sapere nulla su chi è il chiamato.

Questo processo è simile all'uso di un riferimento alla superclasse per accedere a un oggetto di sottoclasse, come descritto nelle lezioni precedenti.

L'esempio seguente chiama il metodo callback() tramite una variabile di riferimento a interfaccia:

class TestInterfaccia {
    public static void main(String[] args) {
        Callback c = new Cliente();
        c.callback(42);
    }
}

L'output di questo programma è il seguente:

callback chiamato con 42

Si noti che la variabile c è dichiarata essere del tipo interfaccia Callback, ma le è stata assegnata un'istanza di Cliente.

Anche se c può essere usata per accedere al metodo callback(), non può accedere ad altri membri della classe Cliente.

Una variabile di riferimento a interfaccia ha conoscenza solo dei metodi dichiarati dalla sua dichiarazione di interfaccia.

Quindi, c non può essere usata per accedere a nonInterfacciaMetodo(), poiché è definito da Cliente ma non da Callback.

Sebbene l'esempio precedente mostri, meccanicamente, come una variabile di riferimento a interfaccia possa accedere a un oggetto di implementazione, non dimostra la potenza polimorfica dell'uso di un tale riferimento. Per osservare tale uso, si crea prima la seconda implementazione di Callback, mostrata qui:

// Altra implementazione di Callback.
class AltroCliente implements Callback {
    // Implementa l'interfaccia di Callback
    public void callback(int p) {
        System.out.println("Un'altra versione di callback");
        System.out.println("p al quadrato è " + (p*p));
    }
}

Ora, si provi la seguente classe:

class TestInterfaccia2 {
    public static void main(String[] args) {
        Callback c = new Cliente();
        AltroCliente ob = new AltroCliente();

        c.callback(42);

        c = ob; // c ora si riferisce a un oggetto AltroCliente
        c.callback(42);
    }
}

L'output da questo programma è il seguente:

callback chiamato con 42
Un'altra versione di callback
p al quadrato è 1764

Come si può vedere, la versione di callback() che viene chiamata è determinata dal tipo di oggetto a cui c si riferisce a tempo di esecuzione. Sebbene questo sia un esempio molto semplice, se ne vedrà un altro, più pratico, a breve.

Implementazioni Parziali

Se una classe include un'interfaccia ma non implementa completamente i metodi richiesti da quell'interfaccia, allora tale classe deve essere dichiarata come abstract. Ad esempio:

abstract class Incompleto implements Callback {
    int a, b;

    void mostra() {
        System.out.println(a + " " + b);
    }
    // ...
}

Qui, la classe Incompleto non implementa callback() e deve essere dichiarata come abstract.

Qualsiasi classe che eredita da Incompleto deve implementare callback() o essere dichiarata abstract a sua volta.