Articolo originale: Java Interfaces Explained with Examples

Un'interfaccia in Java è un po' come una classe, ma con una differenza significativa: una interface può avere solo le firme dei metodi, attributi e metodi di default. A partire da Java 8, puoi anche creare metodi di default. Nel prossimo blocco di codice puoi vedere un esempio di interfaccia:

public interface Veicolo {
    public String targa = "";
    public float maxVel;
    public void accendi();
    public void spegni();
    default void suonaClacson(){
      System.out.println("Sto suonando il clacson");
   }
}

L'interfaccia qui in alto contiene due attributi, due metodi e un metodo di default. Da sola, non è molto utile, ma di solito vengono usate insieme alle Classi. In che modo? Semplice, devi assicurarti che delle classi la implementino, usando la parola chiave implements.

public class Macchina implements Veicolo {
    public void accendi() {
        System.out.println("avvio del motore...");
    }
    public void spegni() {
        System.out.println("spegnendo il motore...");
    }
}

Ora, c'è una regola di base: la classe deve implementare tutti i metodi dell'interfaccia. I metodi devono avere la stessa identica firma (nome, parametri ed eccezioni) così com'è descritta nell'interfaccia. La classe, però, non ha bisogno di dichiarare gli attributi, solo i metodi.

Istanze di un'Interfaccia

Una volta creata una classe di Java che implementa una qualsiasi Interfaccia, l'istanza dell'oggetto deve essere referenziata come istanza dell'interfaccia. Questo concetto è simile a quello dell'istanziazione ereditaria.

// facendo seguito al nostro esempio di prima

Veicolo tesla = new Macchina();

tesla.parti(); // avvio del motore ...

Un'interfaccia non può contenere un costruttore. Per questo, non puoi creare un'istanza dell'interfaccia stessa. Devi creare un'istanza di una classe implementando un'interfaccia per poterla referenziare.

Pensa alle interfacce come a moduli di contratto vuoti, o come template.

Che cosa puoi fare con questa funzionalità? Polimorfismo! Puoi usare solo le interfacce per riferirti alle istanze dell'oggetto!

class Camion implements Veicolo {
    public void avvio() {
        System.out.println("avvio del motore del camion...");
    }
    public void spegni() {
        System.out.println("spegnimento del motore del camion...");
    }
}

class Starter {
    // metodo statico, può essere invocato senza istanziare la classe
    public static void avviaMotore(Veicolo veicolo) {
        veicolo.avvio();
    }
}

Veicolo tesla = new Macchina();
Veicolo tata = new Camion();

Starter.avviaMotore(tesla); // avvio del motore ...
Starter.avviaMotore(tata); // avvio del motore del camion ...

E per quanto riguarda molteplici interfacce?

Sì, puoi implementare molteplici interfacce in una sola classe. Mentre nell'ereditarietà tra classi sei costretto ad ereditare da una sola classe, qui puoi estendere un numero qualsiasi di interfacce. Ma non dimenticare di implementare tutti i metodi di tutte le Interfacce, altrimenti la compilazione fallirà!

public interface GPS {
    public void ottieniCoordinate();
}

public interface Radio {
    public void accendiRadio();
    public void spegniRadio();
}

public class Smartphone implements GPS,Radio {
    public void ottieniCoordinate() {
        // return delle coordinate
    }
    public void accendiRadio() {
      // accendi Radio
    }
    public void spegniRadio() {
        // spegni Radio
    }
}

Alcune caratteristiche delle Interfacce

  • Puoi inserire delle variabili all'interno di un'Interfaccia, anche se non sarebbe una decisione sensata poiché le Classi non sono tenute ad avere la stessa variabile. In breve, evita di inserire delle variabili!
  • Tutte le variabili e i metodi in un'Interfaccia sono pubblici, anche se non metti la parola chiave public.
  • Un'Interfaccia non può specificare l'implementazione di un metodo in particolare. Sta alle Classi farlo. Anche se c'è stata una recente eccezione (vedi più avanti).
  • Se una Classe implementa più Interfacce, allora c'è una remota possibilità che la firma di un metodo coincida con un'altra. Poiché Java non permette di avere più metodi con la stessa firma, questo può causare problemi. Dai un'occhiata a questa domanda per maggiori informazioni.

Metodi di Default delle Interfacce

Prima di Java 8, non c'era nessun modo per far sì che un'interfaccia avesse una specifica implementazione di un metodo. Questo porta ad un sacco di confusione ed errori di codice nel caso in cui la definizione di un'interfaccia venga cambiata.

Ipotizza di aver scritto una libreria open source, che contiene un'Interfaccia. Supponiamo che i tuoi clienti, ovvero praticamente tutti gli sviluppatori del mondo, ne stiano facendo ampio uso e siano felici. Adesso hai dovuto aggiornare la libreria aggiungendo una nuova definizione di un metodo dell'Interfaccia per supportare una nuova funzionalità. Ma questo romperebbe tutti i build, poiché tutte le Classi che implementano quell'Interfaccia devono essere cambiate. Che catastrofe!

Fortunatamente, Java 8 adesso ci mette a disposizione dei metodi default per le Interfacce. Un metodo default può contenere una sua implementazione direttamente all'interno dell'Interfaccia. Dunque, se una Classe non implementa il metodo di default, il compilatore prenderà l'implementazione menzionata nell'Interfaccia. Comodo, vero? Dunque, nella tua libreria, potrai aggiungere qualsiasi numero di metodi di default nelle interfacce senza temere di rompere nulla!

public interface GPS {
    public void ottieniCoordinate();
    default public void ottieniCoordinateApprossimative() {
        // implementazione per restituire coordinate da fonti approssimative
        // come il wifi o il telefono
        System.out.println("Prendo coordinate approssimative...");
    }
}

public interface Radio {
    public void accendiRadio();
    public void spegniRadio();
}

public class Smartphone implements GPS,Radio {
    public void ottieniCoordinate() {
        // return delle coordinate
    }
    public void accendiRadio() {
      // accendi Radio
    }
    public void spegniRadio() {
        // spegni Radio
    }

    // nessuna implementazione per ottieniCoordinateApprossimative()
}

Smartphone motoG = new Smartphone();
motog.ottieniCoordinateApprossimative(); // Prendo coordinate approssimative...

Ma cosa succede se due interfacce hanno la stessa firma di un metodo?

Ottima domanda. In quel caso, se non fornisci un'implementazione all'interno della Classe, il povero compilatore si confonderà e, semplicemente, fallirà! Devi fornire un'implementazione di metodo di default anche all'interno della Classe. C'è anche un modo elegante di farlo usando super per chiamare l'implementazione che preferisci:

public interface Radio {
    // public void accendiRadio();
    // public void spegniRadio();

    default public void prossima() {
        System.out.println("Prossima canzone dalla Radio");
    }
}

public interface LettoreMusicale {
    // public void accendi();
    // public void pausa();
    // public void spegni();

    default public void prossima() {
        System.out.println("Prossima canzone dal lettore musicale");
    }
}

public class Smartphone implements Radio, LettoreMusicale {
    public void prossima() {
        // Supponiamo che tu voglia chiamare prossima() del LettoreMusicale
        LettoreMusicale.super.prossima();
    }
}

Smartphone motoG = new Smartphone();
motoG.prossima(); // Prossima canzone dal lettore musicale

Metodi Statici nelle Interfacce

Un'altra novità in Java 8 è la possibilità di aggiungere metodi statici alle interfacce. I metodi statici delle interfacce sono quasi identici ai metodi statici delle classi concrete. L'unica grande differenza è che i metodi static non sono ereditati nelle classi che implementano l'interfaccia. Ciò significa che quando si chiama il metodo statico viene referenziata l'interfaccia, non la classe che lo implementa.

interface LettoreMusicale {
  public static void spotPubblicitario(String sponsor) {
    System.out.println("Adesso una promozione da " + sponsor);
  }
  
  public void riproduci();
}


class Smartphone implements LettoreMusicale {
	public void riproduci() {
		System.out.println("Riproduco da smartphone");
	}
}

class Main {
  public static void main(String[] args) {
    Smartphone motoG = new Smartphone();
    MusicPlayer.spotPubblicitario("Motorola"); // Chiamato dall'interfaccia, non dalla classe che la implementa
    // motoG.spotPubblicitario("Motorola"); // Questo causerebbe un errore di compilazione
  }
}

Ereditarietà nelle interfacce

È inoltre possibile, in Java, che un'interfaccia erediti un'altra interfaccia usando, come hai già immaginato, la parola chiave extends:

public interface Lettore {
    public void accendi();
    public void pausa();
    public void spegni();
}

public interface LettoreMusicale extends Lettore {
    default public void prossima() {
        System.out.println("Prossima canzone dal lettore musicale");
    }
}

Ciò significa che la Classe che implementa l'Interfaccia LettoreMusicale deve implementare tutti i metodi di LettoreMusicale così come quelli di Lettore:

public class SmartPhone implements LettoreMusicale {
    public void accendi() {
        System.out.println("Accensione");
    }
    public void spegni() {
        System.out.println("Spegnimento");
    }
    public void pausa() {
        System.out.println("Messo in pausa");
    }
}

Adesso hai una visione più chiara delle interfacce di Java! Dai un'occhiata alle Classi Astratte per vedere un altro modo in cui Java ti permette di definire contratti.