Méthodes par défaut et statiques des interfaces Java
Ajoutez des méthodes par défaut et statiques aux interfaces Java pour faire évoluer les API sans casser les implémentations existantes.
Jusqu'à Java 7, une interface ne pouvait déclarer que des méthodes abstraites — les corps se trouvaient dans les classes implémentant l'interface. Java 8 a ajouté trois nouveaux éléments que l'on peut placer dans une interface :
- Les méthodes
default— une méthode avec un corps, héritée par les classes implémentantes qui ne la surchargent pas. - Les méthodes
static— des méthodes utilitaires appartenant à l'interface elle-même. - Les méthodes
private(Java 9) — des auxiliaires partagés entre les méthodesdefaultetstaticde l'interface.
Le cas d'usage motivant était l'évolution des API. Une fois que java.util.Collection avait existé pendant quinze ans avec des millions d'implémentations, l'équipe Java voulait ajouter stream() sans casser toutes ces implémentations. default Stream<E> stream() { ... } a fait exactement cela.
Méthodes par défaut
Une méthode default possède un corps. Les classes implémentantes l'héritent gratuitement et peuvent la surcharger si elles souhaitent un comportement différent :
public interface Greeter {
String name();
default String greet() {
return "Hello, " + name() + "!";
}
}
public class English implements Greeter {
public String name() { return "Alice"; }
}
public class Loud implements Greeter {
public String name() { return "Bob"; }
public String greet() { return "HEY " + name().toUpperCase() + "!"; } // override
}
new English().greet(); // Hello, Alice!
new Loud().greet(); // HEY BOB!default est simplement un marqueur — le mot-clé indique au compilateur « voici un corps de méthode à l'intérieur d'une interface. » Sans lui, une méthode d'interface n'a pas de corps.
Comme tout membre non private d'une interface, une méthode default est implicitement public ; vous ne pouvez pas la rendre protected ou à portée de paquetage. Il n'y a pas de mot-clé public dans les exemples ci-dessus car c'est la seule option que le compilateur autorise.
Ajouter une méthode default à une interface existante est un changement non-cassant. Les classes implémentantes qui ne la surchargent pas héritent de la valeur par défaut. C'est cette propriété qui rend l'évolution des interfaces possible.
Méthodes statiques sur les interfaces
Une méthode static sur une interface appartient à l'interface, pas aux instances. Appelez-la via le nom de l'interface :
public interface Path {
static Path of(String s) { return new SimplePath(s); }
String value();
}
Path p = Path.of("/tmp/foo");Un usage courant est celui des méthodes de fabrique qui produisent des instances de l'interface — Path.of, List.of, Map.of, Stream.of. Elles permettent aux appelants de dépendre de l'interface même lors de la construction, plutôt que d'avoir à choisir une classe d'implémentation spécifique.
Les méthodes statiques d'interface ne sont pas héritées. Vous les appelez toujours via le nom de l'interface, jamais via une sous-classe.
Méthodes privées (Java 9+)
Une méthode private sur une interface est un auxiliaire visible uniquement pour les autres méthodes de la même interface. Elle permet à deux méthodes default de partager de la logique sans exposer cette logique aux classes implémentantes :
public interface Logger {
default void info(String msg) { log("INFO", msg); }
default void warn(String msg) { log("WARN", msg); }
private void log(String level, String msg) {
System.out.println("[" + level + "] " + msg);
}
}Sans private, vous devriez soit copier le corps dans les deux méthodes default, soit exposer log comme méthode default elle-même — ce qui permettrait aux classes implémentantes de la surcharger, ce que vous ne souhaitez probablement pas.
Ce que les méthodes par défaut ne peuvent pas faire
Les méthodes default résident sur l'interface, qui n'a pas d'état. Elles peuvent appeler des méthodes abstraites et d'autres méthodes default/static, mais elles ne peuvent ni lire ni écrire des champs d'instance — il n'y en a pas à lire.
public interface Counter {
int get();
void increment();
default void incrementTwice() { // ok — only calls abstract methods
increment();
increment();
}
}Cela limite la quantité de comportement réel qu'une méthode default peut porter. Elles conviennent mieux à de fines couches de commodité au-dessus des méthodes abstraites, et non pour remplacer entièrement l'implémentation.
Conflits en losange
Lorsqu'une classe implémente deux interfaces qui fournissent toutes les deux une méthode default avec la même signature, la classe doit résoudre explicitement le conflit :
interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }
class C implements A, B {
@Override
public String hello() {
return A.super.hello() + B.super.hello(); // explicit pick
}
}A.super.hello() invoque le défaut de A ; B.super.hello() invoque celui de B. Le compilateur refuse de compiler class C implements A, B { } sans surcharge — il n'y a pas de gagnant automatique. C'est la réponse de Java au « problème du losange » de l'héritage multiple — quand il apparaît, la classe doit trancher.
Une sous-classe l'emporte également sur un défaut hérité : si une superclasse concrète fournit hello(), cela prend le dessus sur toute méthode default d'une interface.
Quand ajouter une méthode par défaut
default est le bon outil quand :
- Vous souhaitez étendre une interface existante, largement implémentée, sans casser les classes implémentantes.
- La nouvelle méthode est genuinement dérivable à partir des méthodes abstraites existantes.
- La valeur par défaut est « évidemment correcte » — la plupart des classes implémentantes en seront satisfaites.
C'est le mauvais outil quand :
- Vous êtes tenté d'y placer un vrai comportement partagé basé sur l'état (utilisez plutôt une classe abstraite).
- La valeur par défaut n'est pas réellement utile et toutes les classes implémentantes la surchargeront quand même (laissez-la abstraite).
Un exemple complet
La suite
Les interfaces et les classes abstraites concernent la relation entre les types entre plusieurs fichiers. Le prochain sous-sujet — les classes imbriquées — porte sur la relation entre les types au sein d'un même fichier : les classes déclarées à l'intérieur d'autres classes, et les quatre formes que Java propose. Continuez vers les classes imbriquées.