Méthodes statiques et par défaut dans les interfaces en Java

1. Vue d'ensemble

Java 8 a apporté à la table quelques nouvelles fonctionnalités, notamment des expressions lambda, des interfaces fonctionnelles, des références de méthodes, des flux, des méthodes facultatives et statiques et par défaut dans les interfaces.

Certains d'entre eux ont déjà été traités dans cet article. Néanmoins, les méthodes statiques et par défaut des interfaces méritent à elles seules un examen plus approfondi.

Dans cet article, nous discuterons en profondeur de l'utilisation des méthodes statiques et par défaut dans les interfaces et passerons en revue certains cas d'utilisation où elles peuvent être utiles.

2. Pourquoi les méthodes par défaut dans les interfaces sont-elles nécessaires?

Comme les méthodes d'interface normales, les méthodes par défaut sont implicitement publiques - il n'est pas nécessaire de spécifier le modificateur public .

Contrairement aux méthodes d'interface classiques, elles sont déclarées avec le mot-clé par défaut au début de la signature de la méthode et fournissent une implémentation .

Voyons un exemple simple:

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

La raison pour laquelle les méthodes par défaut ont été incluses dans la version Java 8 est assez évidente.

Dans une conception typique basée sur des abstractions, où une interface a une ou plusieurs implémentations, si une ou plusieurs méthodes sont ajoutées à l'interface, toutes les implémentations seront obligées de les implémenter également. Sinon, la conception s'effondrera.

Les méthodes d'interface par défaut sont un moyen efficace de résoudre ce problème. Ils nous permettent d'ajouter de nouvelles méthodes à une interface qui sont automatiquement disponibles dans les implémentations . Ainsi, il n'est pas nécessaire de modifier les classes d'implémentation.

De cette manière, la compatibilité descendante est parfaitement préservée sans avoir à refactoriser les implémenteurs.

3. Méthodes d'interface par défaut en action

Pour mieux comprendre la fonctionnalité des méthodes d'interface par défaut , créons un exemple simple.

Disons que nous avons une interface Véhicule naïve et une seule implémentation. Il pourrait y en avoir plus, mais gardons les choses simples:

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."; } }

Et écrivons la classe d'implémentation:

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."; } } 

Enfin, définissons une classe principale typique , qui crée une instance de Car et appelle ses méthodes:

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()); }

Veuillez noter que les méthodes par défaut turnAlarmOn () et turnAlarmOff () de notre interface Véhicule sont automatiquement disponibles dans la classe Car .

De plus, si à un moment donné nous décidons d'ajouter plus de méthodes par défaut à l' interface Vehicle , l'application continuera à fonctionner, et nous n'aurons pas à forcer la classe à fournir des implémentations pour les nouvelles méthodes.

L'utilisation la plus courante des méthodes par défaut dans les interfaces consiste à fournir de manière incrémentielle des fonctionnalités supplémentaires à un type donné sans décomposer les classes d'implémentation.

De plus, ils peuvent être utilisés pour fournir des fonctionnalités supplémentaires autour d'une méthode abstraite existante :

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

4. Règles d'héritage d'interfaces multiples

Les méthodes d'interface par défaut sont en effet une fonctionnalité très intéressante, mais avec quelques mises en garde qui méritent d'être mentionnées. Étant donné que Java permet aux classes d'implémenter plusieurs interfaces, il est important de savoir ce qui se passe lorsqu'une classe implémente plusieurs interfaces qui définissent les mêmes méthodes par défaut .

Pour mieux comprendre ce scénario, définissons une nouvelle interface d' alarme et refactorisons la classe Car :

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

Avec cette nouvelle interface définissant son propre ensemble de méthodes par défaut , la classe Car implémenterait à la fois Vehicle et Alarm :

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

Dans ce cas, le code ne se compilera tout simplement pas, car il y a un conflit causé par l'héritage de plusieurs interfaces (alias le problème du diamant). La classe Car hériterait des deux ensembles de méthodes par défaut . Lesquels devraient alors être appelés?

Pour résoudre cette ambiguïté, nous devons explicitement fournir une implémentation pour les méthodes:

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

Nous pouvons également demander à notre classe d'utiliser les méthodes par défaut de l'une des interfaces .

Voyons un exemple qui utilise les méthodes par défaut de l' interface Véhicule :

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

De même, nous pouvons demander à la classe d'utiliser les méthodes par défaut définies dans l' interface d' alarme :

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

De plus, il est même possible de faire en sorte que la classe Car utilise les deux ensembles de méthodes par défaut :

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

5. Méthodes d'interface statique

En plus de pouvoir déclarer des méthodes par défaut dans les interfaces, Java 8 nous permet de définir et d'implémenter des méthodes statiques dans les interfaces .

Since static methods don't belong to a particular object, they are not part of the API of the classes implementing the interface, and they have to be called by using the interface name preceding the method name.

To understand how static methods work in interfaces, let's refactor the Vehicle interface and add to it a static utility method:

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

Defining a static method within an interface is identical to defining one in a class. Moreover, a static method can be invoked within other static and default methods.

Now, say that we want to calculate the horsepower of a given vehicle's engine. We just call the getHorsePower() method:

Vehicle.getHorsePower(2500, 480)); 

The idea behind static interface methods is to provide a simple mechanism that allows us to increase the degree of cohesion of a design by putting together related methods in one single place without having to create an object.

Pretty much the same can be done with abstract classes. The main difference lies in the fact that abstract classes can have constructors, state, and behavior.

Furthermore, static methods in interfaces make possible to group related utility methods, without having to create artificial utility classes that are simply placeholders for static methods.

6. Conclusion

Dans cet article, nous avons exploré en profondeur l'utilisation des méthodes d'interface statiques et par défaut dans Java 8. À première vue, cette fonctionnalité peut sembler un peu bâclée, en particulier d'un point de vue puriste orienté objet. Idéalement, les interfaces ne devraient pas encapsuler le comportement et devraient être utilisées uniquement pour définir l'API publique d'un certain type.

Cependant, lorsqu'il s'agit de maintenir la compatibilité descendante avec le code existant, les méthodes statiques et par défaut sont un bon compromis.

Et, comme d'habitude, tous les exemples de code présentés dans cet article sont disponibles à l'adresse over sur GitHub.