Metodi statici e predefiniti nelle interfacce in Java

1. Panoramica

Java 8 ha portato in tavola alcune nuove funzionalità, incluse espressioni lambda, interfacce funzionali, riferimenti a metodi, flussi, metodi opzionali e statici e predefiniti nelle interfacce.

Alcuni di questi sono già stati trattati in questo articolo. Tuttavia, i metodi statici e predefiniti nelle interfacce meritano uno sguardo più approfondito da soli.

In questo articolo, discuteremo in profondità come utilizzare metodi statici e predefiniti nelle interfacce e analizzeremo alcuni casi d'uso in cui possono essere utili.

2. Perché sono necessari metodi predefiniti nelle interfacce

Come i normali metodi di interfaccia, i metodi predefiniti sono implicitamente pubblici : non è necessario specificare il modificatore public .

A differenza dei normali metodi di interfaccia, vengono dichiarati con la parola chiave predefinita all'inizio della firma del metodo e forniscono un'implementazione .

Vediamo un semplice esempio:

public interface MyInterface { // regular interface methods default void defaultMethod() { // default method implementation } }

Il motivo per cui i metodi predefiniti sono stati inclusi nella versione Java 8 è piuttosto ovvio.

In una tipica progettazione basata su astrazioni, dove un'interfaccia ha una o più implementazioni, se uno o più metodi vengono aggiunti all'interfaccia, anche tutte le implementazioni saranno obbligate a implementarli. Altrimenti, il design si romperà.

I metodi di interfaccia predefiniti sono un modo efficiente per affrontare questo problema. Ci consentono di aggiungere nuovi metodi a un'interfaccia che sono automaticamente disponibili nelle implementazioni . Pertanto, non è necessario modificare le classi di implementazione.

In questo modo, la compatibilità con le versioni precedenti viene preservata in modo ordinato senza dover effettuare il refactoring degli implementatori.

3. Metodi di interfaccia predefiniti in azione

Per comprendere meglio la funzionalità dei metodi di interfaccia predefiniti , creiamo un semplice esempio.

Diciamo che abbiamo un'interfaccia veicolo ingenua e una sola implementazione. Potrebbe esserci di più, ma cerchiamo di mantenerlo semplice:

public interface Vehicle { String getBrand(); String speedUp(); String slowDown(); default String turnAlarmOn() { return "Turning the vehicle alarm on."; } default String turnAlarmOff() { return "Turning the vehicle alarm off."; } }

E scriviamo la classe di implementazione:

public class Car implements Vehicle { private String brand; // constructors/getters @Override public String getBrand() { return brand; } @Override public String speedUp() { return "The car is speeding up."; } @Override public String slowDown() { return "The car is slowing down."; } } 

Infine, definiamo una tipica classe principale , che crea un'istanza di Car e chiama i suoi metodi:

public static void main(String[] args) { Vehicle car = new Car("BMW"); System.out.println(car.getBrand()); System.out.println(car.speedUp()); System.out.println(car.slowDown()); System.out.println(car.turnAlarmOn()); System.out.println(car.turnAlarmOff()); }

Si prega di notare come i metodi predefiniti turnAlarmOn () e turnAlarmOff () dalla nostra interfaccia Veicolo siano automaticamente disponibili nella classe Car .

Inoltre, se ad un certo punto decidiamo di aggiungere più metodi predefiniti all'interfaccia del veicolo , l'applicazione continuerà a funzionare e non dovremo forzare la classe a fornire implementazioni per i nuovi metodi.

L'utilizzo più tipico dei metodi predefiniti nelle interfacce consiste nel fornire in modo incrementale funzionalità aggiuntive a un determinato tipo senza scomporre le classi di implementazione.

Inoltre, possono essere utilizzati per fornire funzionalità aggiuntive attorno a un metodo astratto esistente :

public interface Vehicle { // additional interface methods double getSpeed(); default double getSpeedInKMH(double speed) { // conversion } }

4. Regole di ereditarietà dell'interfaccia multipla

I metodi di interfaccia predefiniti sono davvero una caratteristica carina, ma con alcuni avvertimenti che vale la pena menzionare. Poiché Java consente alle classi di implementare più interfacce, è importante sapere cosa succede quando una classe implementa diverse interfacce che definiscono gli stessi metodi predefiniti .

Per comprendere meglio questo scenario, definiamo una nuova interfaccia di allarme e refactoring della classe Car :

public interface Alarm { default String turnAlarmOn() { return "Turning the alarm on."; } default String turnAlarmOff() { return "Turning the alarm off."; } }

Con questa nuova interfaccia che definisce il proprio set di metodi predefiniti , la classe Car implementerebbe sia Vehicle che Alarm :

public class Car implements Vehicle, Alarm { // ... }

In questo caso, il codice semplicemente non verrà compilato, poiché c'è un conflitto causato dall'ereditarietà di più interfacce (ovvero il problema del diamante). La classe Car erediterà entrambi i set di metodi predefiniti . Quali dovrebbero essere chiamati allora?

Per risolvere questa ambiguità, dobbiamo fornire esplicitamente un'implementazione per i metodi:

@Override public String turnAlarmOn() { // custom implementation } @Override public String turnAlarmOff() { // custom implementation }

Possiamo anche fare in modo che la nostra classe utilizzi i metodi predefiniti di una delle interfacce .

Vediamo un esempio che utilizza i metodi predefiniti dall'interfaccia del veicolo :

@Override public String turnAlarmOn() { return Vehicle.super.turnAlarmOn(); } @Override public String turnAlarmOff() { return Vehicle.super.turnAlarmOff(); } 

Allo stesso modo, possiamo fare in modo che la classe utilizzi i metodi predefiniti definiti nell'interfaccia di allarme :

@Override public String turnAlarmOn() { return Alarm.super.turnAlarmOn(); } @Override public String turnAlarmOff() { return Alarm.super.turnAlarmOff(); } 

Inoltre, è anche possibile fare in modo che la classe Car utilizzi entrambi i set di metodi predefiniti :

@Override public String turnAlarmOn() { return Vehicle.super.turnAlarmOn() + " " + Alarm.super.turnAlarmOn(); } @Override public String turnAlarmOff() { return Vehicle.super.turnAlarmOff() + " " + Alarm.super.turnAlarmOff(); } 

5. Metodi di interfaccia statica

Oltre a poter dichiarare metodi predefiniti nelle interfacce, Java 8 ci consente di definire e implementare metodi statici nelle interfacce .

Poiché i metodi statici non appartengono a un particolare oggetto, non fanno parte dell'API delle classi che implementano l'interfaccia e devono essere chiamati utilizzando il nome dell'interfaccia che precede il nome del metodo .

Per capire come funzionano i metodi statici nelle interfacce, eseguiamo il refactoring dell'interfaccia del veicolo e aggiungiamo ad essa un metodo di utilità statico :

public interface Vehicle { // regular / default interface methods static int getHorsePower(int rpm, int torque) { return (rpm * torque) / 5252; } } 

La definizione di un metodo statico all'interno di un'interfaccia è identica alla definizione di uno in una classe. Inoltre, un metodo statico può essere richiamato all'interno di altri metodi statici e predefiniti .

Ora, diciamo che vogliamo calcolare la potenza del motore di un dato veicolo. Chiamiamo semplicemente il metodo getHorsePower () :

Vehicle.getHorsePower(2500, 480)); 

L'idea alla base dei metodi di interfaccia statica è di fornire un semplice meccanismo che ci consenta di aumentare il grado di coesione di un progetto mettendo insieme metodi correlati in un unico posto senza dover creare un oggetto.

Più o meno lo stesso può essere fatto con le classi astratte. La differenza principale sta nel fatto che le classi astratte possono avere costruttori, stato e comportamento .

Inoltre, i metodi statici nelle interfacce rendono possibile raggruppare metodi di utilità correlati, senza dover creare classi di utilità artificiali che sono semplicemente segnaposto per metodi statici.

6. Conclusione

In questo articolo, abbiamo esplorato in profondità l'uso di metodi di interfaccia statici e predefiniti in Java 8. A prima vista, questa caratteristica potrebbe sembrare un po 'sciatta, in particolare da una prospettiva purista orientata agli oggetti. Idealmente, le interfacce non dovrebbero incapsulare il comportamento e dovrebbero essere utilizzate solo per definire l'API pubblica di un certo tipo.

Quando si tratta di mantenere la compatibilità con il codice esistente, tuttavia, i metodi statici e predefiniti sono un buon compromesso.

E, come al solito, tutti gli esempi di codice mostrati in questo articolo sono disponibili su GitHub.