Comment fonctionnent les interfaces fonctionnelles en Java?

Plus tôt dans le chapitre, nous avons déclaré l’interface CheckTrait, qui possède exactement une méthode que les implémenteurs doivent écrire. Les lambdas ont une relation spéciale avec de telles interfaces. En fait, ces interfaces ont un nom. Une interface fonctionnelle est une interface qui contient une seule méthode abstraite. Votre ami Samuel peut vous aider à vous en souvenir car elle est officiellement connue sous le nom de règle SAM (Single Abstract Method).

Définir une Interface Fonctionnelle

Examinons un exemple d’interface fonctionnelle et d’une classe qui l’implémente :

@FunctionalInterface
public interface Sprint {
    public void sprint(int vitesse);
}

public class Tigre implements Sprint {
    public void sprint(int vitesse) {
        System.out.println("L'animal court très vite ! " + vitesse);
    }
}

Dans cet exemple, l’interface Sprint est une interface fonctionnelle car elle contient exactement une méthode abstraite, et la classe Tigre est une classe valide qui implémente l’interface.

L’Annotation @FunctionalInterface

L’annotation @FunctionalInterface indique au compilateur que vous souhaitez que le code soit une interface fonctionnelle. Si l’interface ne suit pas les règles d’une interface fonctionnelle, le compilateur vous donnera une erreur.

@FunctionalInterface
public interface Danse {  // NE COMPILE PAS
    void bouger();
    void reposer();
}

Java inclut @FunctionalInterface sur certaines interfaces fonctionnelles, mais pas toutes. Cette annotation signifie que les auteurs de l’interface promettent qu’elle pourra être utilisée en toute sécurité dans une lambda à l’avenir. Cependant, le fait que vous ne voyiez pas l’annotation ne signifie pas que ce n’est pas une interface fonctionnelle. N’oubliez pas que c’est le fait d’avoir exactement une méthode abstraite qui en fait une interface fonctionnelle, pas l’annotation.

Considérez les quatre interfaces suivantes. Étant donné notre précédente interface fonctionnelle Sprint, lesquelles sont des interfaces fonctionnelles ?

public interface Course extends Sprint {}

public interface Sauter extends Sprint {
    void sauter();
}

public interface Dormir {
    private void ronfler() {}
    default int getZzz() { return 1; }
}

public interface Grimper {
    void atteindre();
    default void tomber() {}
    static int seRelever() { return 100; }
    private static boolean verifierHauteur() { return true; }
}

Ces quatre interfaces sont valides, mais elles ne sont pas toutes des interfaces fonctionnelles. L’interface Course est une interface fonctionnelle car elle étend l’interface Sprint et hérite de la seule méthode abstraite sprint(). L’interface Sauter n’est pas une interface fonctionnelle valide car elle possède deux méthodes abstraites : la méthode héritée sprint() et la méthode déclarée sauter().

L’interface Dormir n’est pas non plus une interface fonctionnelle valide. Ni ronfler() ni getZzz() ne répondent aux critères d’une méthode abstraite unique. Même si les méthodes default fonctionnent comme des méthodes abstraites, en ce sens qu’elles peuvent être remplacées dans une classe implémentant l’interface, elles ne suffisent pas pour satisfaire à l’exigence d’une méthode abstraite unique.

Enfin, l’interface Grimper est une interface fonctionnelle. Malgré la définition de plusieurs méthodes, elle ne contient qu’une seule méthode abstraite : atteindre().

Ajout de Méthodes d’Object

Toutes les classes héritent de certaines méthodes de Object. Vous devriez connaître les signatures de méthodes Object suivantes :

  • public String toString()
  • public boolean equals(Object)
  • public int hashCode()

Nous abordons ce sujet maintenant car il existe une exception à la règle de la méthode abstraite unique que vous devriez connaître. Si une interface fonctionnelle inclut une méthode abstraite avec la même signature qu’une méthode publique trouvée dans Object, ces méthodes ne comptent pas dans le test de la méthode abstraite unique. La motivation derrière cette règle est que toute classe qui implémente l’interface héritera d’Object, comme toutes les classes le font, et implémentera donc toujours ces méthodes.

Puisque Java suppose que toutes les classes étendent Object, vous ne pouvez pas non plus déclarer une méthode d’interface incompatible avec Object. Par exemple, déclarer une méthode abstraite int toString() dans une interface ne compilerait pas car la version de la méthode dans Object renvoie un String.

Examinons un exemple. La classe Planer est-elle une interface fonctionnelle ?

public interface Planer {
    abstract String toString();
}

Non. Puisque toString() est une méthode publique implémentée dans Object, elle ne compte pas dans le test de la méthode abstraite unique. En revanche, l’implémentation suivante de Plonger est une interface fonctionnelle :

public interface Plonger {
    String toString();
    public boolean equals(Object o);
    public abstract int hashCode();
    public void plonger();
}

La méthode plonger() est la seule méthode abstraite, tandis que les autres ne sont pas comptées car ce sont des méthodes publiques définies dans la classe Object.

Méfiez-vous des exemples qui ressemblent à des méthodes de la classe Object mais qui ne sont pas réellement définies dans la classe Object. Voyez-vous pourquoi ce qui suit n’est pas une interface fonctionnelle valide ?

public interface Hiberner {
    String toString();
    public boolean equals(Hiberner o);
    public abstract int hashCode();
    public void reposer();
}

Bien qu’elle ressemble beaucoup à notre interface Plonger, l’interface Hiberner utilise equals(Hiberner) au lieu de equals(Object). Comme cela ne correspond pas à la signature de la méthode equals(Object) définie dans la classe Object, cette interface est considérée comme contenant deux méthodes abstraites : equals(Hiberner) et reposer().