Une interface fonctionnelle ne contient qu'une seule méthode abstraite. Elle peut toutefois inclure des méthodes par défaut ou des méthodes héritées de Object, telles que equals, hashCode ou toString.
@FunctionalInterface
interface NumberProcessor<T extends Number> {
T compute(List<T> values);
}
L'annotation @FunctionalInterface est facultative, mais elle invite le compilateur à vérifier que l'interface respecte bien la définiiton. Si une interface déclare deux méthodes abstraites, elle ne peut pas être fonctionnelle.
@FunctionalInterface
interface Validator<T> {
boolean check(T input);
default Validator<T> andThen(Validator<T> next) {
return value -> this.check(value) && next.check(value);
}
}
Syntaxe d'une expression lambda
Une lambda adopte l'une des deux formes suivantes :
- Expression unique :
paramètres -> expression - Bloc d'instructions :
paramètres -> { instructions; }
List<String> names = Arrays.asList("Ada", "Grace", "Linus");
names.forEach(name -> System.out.println(name));
names.forEach(name -> {
String upper = name.toUpperCase();
System.out.println(upper);
});
Chaque lambda correspond à l'unique méthode abstraite d'une interface fonctionnelle. Dans l'exemple précédent, forEach attend un Consumer<T>, dont la méthode abstraite est accept(T t).
Affectation à une interface fonctionnelle
Une lambda peut être stockée dans une variable dont le type est une interface fonctionnelle. Le résultat est une instance implémentant cette interface.
NumberProcessor<Integer> total = values -> {
int sum = 0;
for (Integer value : values) {
sum += value;
}
return sum;
};
Integer result = total.compute(Arrays.asList(10, 20, 30)); // 60
Règles de conformité
Inférence et déclaration des types
Les types des paramètres peuvent être inférés. Si l'on déclare explicitement un type, il faut le faire pour tous les paramètres.
@FunctionalInterface
interface Divisibility {
boolean isDivisible(int dividend, int divisor);
}
Divisibility implicit = (a, b) -> a % b == 0;
Divisibility explicit = (int a, int b) -> a % b == 0;
// Divisibility mixed = (int a, b) -> ... ; // interdit
Compatibilité des exceptions
L'exception levée dans une lambda doit être compatible avec celle déclarée par la méthode abstraite de l'interface fonctionnelle.
@FunctionalInterface
interface FileLoader {
String load(Path path) throws IOException;
}
FileLoader loader = path -> {
if (!Files.exists(path)) {
throw new IOException("Fichier absent");
}
return Files.readString(path);
};
La lambda ne peut pas lever une exception plus générale que celle déclarée par la méthode de l'interface.
Compatibilité des types de paramètres
Les types des paramètres de la lambda doivent correspondre à ceux de la méthode abstraite de l'interface fonctionnelle cible.
Références de méthode
Une référence de méthode permet de construire une lambda à partir d'une méthode existante. Elle s'écrit avec l'opérateur ::.
List<String> items = Arrays.asList("alpha", "beta", "gamma");
items.forEach(System.out::println);
Ce code est équivalent à item -> System.out.println(item). La référence de méthode peut cibler une méthode statique, une méthode d'instance d'un objet particulier, ou encore une méthode d'instance sur un objet arbitraire.