Références de méthodes Java
Utilisez l'opérateur :: en Java pour référencer des méthodes statiques, d'instance, liées et des constructeurs en tant que lambdas.
Une référence de méthode est une syntaxe abrégée pour un lambda dont le corps ne fait rien d'autre qu'appeler une méthode existante. Lorsqu'un lambda est littéralement x -> SomeClass.foo(x) ou (a, b) -> a.bar(b), l'opérateur :: vous permet de l'écrire comme SomeClass::foo ou Some::bar. Le compilateur produit la même valeur dans les deux cas — une instance de l'interface fonctionnelle appropriée — donc partout où un lambda convient, une référence de méthode correspondante convient aussi.
Function<String, Integer> len1 = s -> s.length();
Function<String, Integer> len2 = String::length; // identical at runtime
List<String> names = List.of("Bob", "Alice");
names.forEach(s -> System.out.println(s)); // lambda
names.forEach(System.out::println); // method referenceLes quatre formes ci-dessous couvrent toutes les références de méthodes que vous écrirez. La compétence unique consiste à reconnaître quelle forme convient à un site d'appel donné.
Forme 1 : référence de méthode statique — ClassName::staticMethod
La méthode est une méthode static sur une classe. La référence devient un lambda dont les paramètres sont les paramètres de la méthode statique :
Function<String, Integer> parse = Integer::parseInt; // s -> Integer.parseInt(s)
BinaryOperator<Integer> max = Math::max; // (a, b) -> Math.max(a, b)
Function<Object, String> toStr = String::valueOf; // o -> String.valueOf(o)C'est la forme qui apparaît dans le code de flux comme nums.stream().reduce(0, Integer::sum) — Integer.sum(int, int) est statique, donc Integer::sum est un BinaryOperator<Integer> (un BiFunction<Integer, Integer, Integer>).
Forme 2 : référence de méthode d'instance liée — instance::method
La méthode est une méthode d'instance sur un objet spécifique et nommé. La référence devient un lambda dont les paramètres sont les paramètres de la méthode (l'instance est capturée) :
PrintStream out = System.out;
Consumer<String> print = out::println; // s -> out.println(s)
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat; // name -> prefix.concat(name)
List<String> log = new ArrayList<>();
Consumer<String> record = log::add; // msg -> log.add(msg)Le récepteur lié fournit l'emplacement sans argument : parce que prefix est déjà capturé, greet n'a besoin que de l'argument passé à concat, donc c'est une Function<String, String> plutôt qu'une BiFunction. De même, log::add garde log fixe et n'expose que l'élément à ajouter, donnant un Consumer<String>.
L'instance capturée est conservée par l'objet résultant, de la même façon qu'un lambda capture des variables locales effectively final. Les références liées sont la façon de dire "utilise la méthode de cet objet comme rappel" — Logger::info sur un logger particulier, event::handle sur un gestionnaire particulier.
Forme 3 : référence de méthode d'instance non liée — ClassName::method
La méthode est une méthode d'instance, mais vous la référencez par la classe plutôt que par une instance spécifique. La référence devient un lambda dont le premier paramètre est le récepteur, et le reste sont les propres paramètres de la méthode :
Function<String, Integer> len = String::length; // s -> s.length() — first param is the receiver
Function<String, String> upper = String::toUpperCase; // s -> s.toUpperCase()
BiPredicate<String, String> starts = String::startsWith; // (s, prefix) -> s.startsWith(prefix)C'est la forme sur laquelle les gens trébuchent. String::length semble pouvoir signifier "la méthode length sur la classe String" — mais il n'existe pas de telle méthode statique. Cela signifie réellement "étant donné n'importe quelle String, appelle sa méthode d'instance length() — le récepteur est le premier paramètre du lambda." C'est pourquoi String::length est une Function<String, Integer> (une entrée, une sortie) et String::startsWith est un BiPredicate<String, String> (la deuxième entrée est le préfixe que le récepteur teste).
Cette forme est le moteur derrière presque chaque pipeline de flux :
people.stream()
.map(Person::name) // unbound: p -> p.name()
.filter(s -> s.startsWith("A"))
.map(String::toUpperCase) // unbound: s -> s.toUpperCase()
.forEach(System.out::println); // bound: s -> out.println(s)Forme 4 : référence de constructeur — ClassName::new
Référence un constructeur comme une fonction. Le lambda résultant prend les paramètres du constructeur et retourne une nouvelle instance :
Supplier<List<String>> listOf = ArrayList::new; // () -> new ArrayList<>()
Function<Integer, ArrayList<?>> sized = ArrayList::new; // n -> new ArrayList<>(n)
Function<String, BigDecimal> toBig = BigDecimal::new; // s -> new BigDecimal(s)
BiFunction<String, Integer, AbstractMap.SimpleEntry<String, Integer>> entry =
AbstractMap.SimpleEntry::new;Les références de constructeur permettent à Collectors.toCollection(TreeSet::new) de choisir un type de destination, et à Stream.generate(Random::new) de produire des objets Random indépendants à chaque appel de get().
Les tableaux ont une forme spéciale : String[]::new est une IntFunction<String[]> — n -> new String[n]. C'est ce qu'utilise stream.toArray(String[]::new).
Référence de méthode vs lambda — quand chacun l'emporte
Une référence de méthode est le bon choix lorsque le corps du lambda est exactement un seul appel de méthode avec les paramètres passés dans l'ordre :
| Lambda | Référence de méthode |
|---|---|
s -> s.length() | String::length |
s -> System.out.println(s) | System.out::println |
(a, b) -> a.compareTo(b) | String::compareTo |
() -> new ArrayList<>() | ArrayList::new |
Un lambda est le bon choix lorsque le corps fait autre chose :
- Appelle plus d'une méthode :
s -> s.trim().toUpperCase()(pas de référence pour la chaîne). - A une transformation d'argument :
s -> System.out.println("[" + s + "]"). - A un flux de contrôle :
n -> n < 0 ? 0 : n. - Réordonne ou duplique les arguments :
(a, b) -> b.compareTo(a)(comparateur inversé).
L'optimisation ne concerne pas vraiment la vitesse d'exécution — les deux se compilent vers le même bootstrap invokedynamic. Il s'agit de lisibilité. Person::name saute aux yeux comme "le champ name," alors que p -> p.name() vous oblige à lire trois jetons. Quand la référence convient, préférez-la ; quand ce n'est pas le cas, ne tordez pas le code pour la faire tenir.
Un piège avec les références de constructeur : surcharges ambiguës
ClassName::new fonctionne bien quand il y a un seul constructeur correspondant à l'interface cible. Quand il y en a plusieurs, le compilateur choisit en fonction du nombre de paramètres et des types du type cible. La plupart du temps ça fonctionne ; parfois non, et vous devez disambiguïser en typant la variable explicitement ou en revenant à un lambda :
// ArrayList has constructors: (), (int), (Collection)
Supplier<ArrayList<String>> a = ArrayList::new; // picks the no-arg
Function<Integer, ArrayList<String>> b = ArrayList::new; // picks the (int) one
Function<List<String>, ArrayList<String>> c = ArrayList::new; // picks the (Collection) one
// var inference can't disambiguate — this would not compile:
// var ambiguous = ArrayList::new;La solution est de garder le type cible explicite, comme dans a, b, c ci-dessus.
Un exemple complet : les quatre formes en un seul programme
Le programme ci-dessous construit et utilise une référence de méthode de chaque forme, démontre comment String::length (non lié) devient une Function<String, Integer>, et montre l'astuce de référence de constructeur qui alimente stream().toArray(T[]::new).
Ce qu'il faut retenir de l'exécution :
- Les quatre formes se compilent en instances d'interfaces fonctionnelles ordinaires —
parseest uneFunction<String, Integer>que vous ayez écrits -> Integer.parseInt(s)ouInteger::parseInt. L'abréviation est purement syntaxique. - Les références non liées
String::lengthetString::toUpperCaseont toutes deux un récepteur comme premier paramètre. C'est pourquoiString::lengthest uneFunction<String, Integer>etString::startsWithest unBiPredicate<String, String>— le récepteur est un emplacement, le paramètre explicite l'autre. - La référence de constructeur
String[]::newa produit uneIntFunction<String[]>— la forme qu'attendstream().toArray(...). Les références de constructeur permettent d'indiquer à un flux "voici le type de destination." - Le comparateur de longueur inversée ne pouvait pas être écrit comme une référence de méthode : le récepteur et le paramètre s'échangent, et les références de méthodes ne peuvent pas réordonner les arguments. C'est exactement le type de cas où un lambda reste le bon choix.
La suite
Vous pouvez maintenant écrire un pipeline de flux presque entièrement en références de méthodes et laisser les quelques transformations qui nécessitent vraiment une mise en forme vivre dans de petits lambdas. Ce style est la transition naturelle vers le point central de cette partie : les flux. Le prochain chapitre, Introduction aux flux Java, présente l'API Stream<T> — ce qu'elle est, à quoi ressemble un pipeline de flux, pourquoi il est paresseux, pourquoi il ne peut être utilisé qu'une seule fois, et comment il s'articule avec les lambdas, interfaces fonctionnelles et références de méthodes que vous venez d'apprendre.