.Min () et .max () du flux Java 8: pourquoi cela se compile-il?

215

Remarque: cette question provient d'un lien mort qui était une question SO précédente, mais voici ...

Voir ce code ( note: je sais que ce code ne "fonctionnera" pas et qu'il Integer::comparedevrait être utilisé - je viens de l'extraire de la question liée ):

final ArrayList <Integer> list 
    = IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

System.out.println(list.stream().max(Integer::max).get());
System.out.println(list.stream().min(Integer::min).get());

Selon le javadoc de .min()et .max(), l'argument des deux devrait être a Comparator. Pourtant, ici, les références de méthode sont des méthodes statiques de la Integerclasse.

Alors, pourquoi cela se compile-il?

fge
la source
6
Notez qu'il ne fonctionne pas correctement, il devrait utiliser à la Integer::compareplace de Integer::maxet Integer::min.
Christoffer Hammarström
@ ChristofferHammarström Je le sais; notez comment j'ai dit avant l'extrait de code "je sais, c'est absurde"
fge
3
Je n'essayais pas de te corriger, je le dis aux gens en général. Vous avez donné l'impression que vous pensiez que la partie qui est absurde est que les méthodes de Integerne sont pas des méthodes de Comparator.
Christoffer Hammarström

Réponses:

242

Permettez-moi d'expliquer ce qui se passe ici, car ce n'est pas évident!

Tout d'abord, Stream.max()accepte une instance de Comparatorsorte que les éléments du flux puissent être comparés les uns aux autres pour trouver le minimum ou le maximum, dans un ordre optimal dont vous n'avez pas trop à vous soucier.

La question est donc, bien sûr, pourquoi est-il Integer::maxaccepté? Après tout, ce n'est pas un comparateur!

La réponse réside dans la façon dont la nouvelle fonctionnalité lambda fonctionne en Java 8. Elle repose sur un concept qui est connu de manière informelle sous le nom d'interfaces «méthode abstraite unique» ou interfaces «SAM». L'idée est que toute interface avec une méthode abstraite peut être implémentée automatiquement par n'importe quelle lambda - ou référence de méthode - dont la signature de méthode correspond à la seule méthode de l'interface. Examinons donc l' Comparatorinterface (version simple):

public Comparator<T> {
    T compare(T o1, T o2);
}

Si une méthode recherche un Comparator<Integer>, elle recherche essentiellement cette signature:

int xxx(Integer o1, Integer o2);

J'utilise "xxx" car le nom de la méthode n'est pas utilisé à des fins de correspondance .

Par conséquent, les deux Integer.min(int a, int b)et Integer.max(int a, int b)sont suffisamment proches pour que l'autoboxing permette que cela apparaisse comme un Comparator<Integer>dans un contexte de méthode.

David M. Lloyd
la source
28
ou bien: list.stream().mapToInt(i -> i).max().get().
assylias
13
@assylias Vous souhaitez utiliser .getAsInt()plutôt get()que, car vous avez affaire à un OptionalInt.
skiwi
... alors que nous essayons seulement de fournir un comparateur personnalisé à une max()fonction!
Manu343726
Il convient de noter que cette "interface SAM" est en fait appelée une "interface fonctionnelle" et qu'en regardant la Comparatordocumentation, nous pouvons voir qu'elle est décorée avec l'annotation @FunctionalInterface. Ce décorateur est la magie qui permet Integer::maxet Integer::minse transforme en Comparator.
Chris Kerekes
2
@ChrisKerekes le décorateur @FunctionalInterfaceest principalement à des fins de documentation uniquement, car le compilateur peut le faire avec n'importe quelle interface avec une seule méthode abstraite.
errantlinguist du
117

Comparatorest une interface fonctionnelle , et Integer::maxest conforme à cette interface (après prise en compte de l'autoboxing / unboxing). Il prend deux intvaleurs et renvoie unint - tout comme vous vous attendez à un Comparator<Integer>(encore une fois, loucher pour ignorer la différence Integer / int).

Cependant, je ne m'attendrais pas à ce qu'il fasse la bonne chose, étant donné que Integer.maxcela ne respecte pas la sémantique de Comparator.compare. Et en effet, cela ne fonctionne pas vraiment en général. Par exemple, apportez une petite modification:

for (int i = 1; i <= 20; i++)
    list.add(-i);

... et maintenant la maxvaleur est -20 et lemin valeur est -1.

Au lieu de cela, les deux appels doivent utiliser Integer::compare:

System.out.println(list.stream().max(Integer::compare).get());
System.out.println(list.stream().min(Integer::compare).get());
Jon Skeet
la source
1
Je connais les interfaces fonctionnelles; et je sais pourquoi cela donne de mauvais résultats. Je me demande juste comment
diable
6
@fge: Eh bien, était-ce le déballage dont vous ne saviez pas? (Je n'ai pas examiné exactement cette partie.) A Comparator<Integer>aurait int compare(Integer, Integer)... ce n'est pas ahurissant que Java permette une référence de méthode de int max(int, int)se convertir à cela ...
Jon Skeet
7
@fge: Le compilateur est-il censé connaître la sémantique de Integer::max? De son point de vue, vous avez passé dans une fonction qui répondait à ses spécifications, c'est tout ce qu'il peut vraiment continuer.
Mark Peters
6
@fge: En particulier, si vous comprenez une partie de ce qui se passe, mais que vous êtes intrigué par un aspect spécifique, cela vaut la peine de le préciser dans la question pour éviter que les gens ne perdent leur temps à expliquer les éléments que vous connaissez déjà.
Jon Skeet
20
Je pense que le problème sous-jacent est la signature de type de Comparator.compare. Il doit renvoyer un enumde {LessThan, GreaterThan, Equal}, pas un int. De cette façon, l'interface fonctionnelle ne correspondrait pas réellement et vous obtiendriez une erreur de compilation. IOW: la signature de type de Comparator.comparene capture pas correctement la sémantique de ce que signifie comparer deux objets, et donc d'autres interfaces qui n'ont absolument rien à voir avec la comparaison accidentelle d'objets ont la même signature de type.
Jörg W Mittag
19

Cela fonctionne car Integer::minrésout une implémentation de l' Comparator<Integer>interface.

La référence de méthode de Integer::minrésout Integer.min(int a, int b), résolu IntBinaryOperatoret vraisemblablement autoboxe se produit quelque part, ce qui en fait un BinaryOperator<Integer>.

Et les méthodes min()resp max()de Stream<Integer>demander l' Comparator<Integer>interface à implémenter.
Maintenant, cela se résout à la méthode unique Integer compareTo(Integer o1, Integer o2). Qui est de type BinaryOperator<Integer>.

Et donc la magie s'est produite car les deux méthodes sont a BinaryOperator<Integer>.

skiwi
la source
Il n'est pas tout à fait exact de dire que cela Integer::minmet en œuvre Comparable. Ce n'est pas un type qui peut implémenter quoi que ce soit. Mais il est évalué en un objet qui implémente Comparable.
Lii
1
@Lii Merci, je l'ai corrigé tout à l'heure.
skiwi
Comparator<Integer>est une interface à méthode abstraite unique (aka "fonctionnelle"), et Integer::minremplit son contrat, donc le lambda peut être interprété comme ceci. Je ne sais pas comment vous voyez BinaryOperator entrer en jeu ici (ou IntBinaryOperator, soit) - il n'y a pas de relation de sous-typage entre cela et Comparator.
Paŭlo Ebermann du
2

Outre les informations fournies par David M. Lloyd, on pourrait ajouter que le mécanisme qui permet cela s'appelle le typage cible .

L'idée est que le type que le compilateur assigne à une expression lambda ou à une référence de méthode ne dépend pas seulement de l'expression elle-même, mais aussi de l'endroit où elle est utilisée.

La cible d'une expression est la variable à laquelle son résultat est affecté ou le paramètre auquel son résultat est transmis.

Les expressions lambda et les références de méthode se voient attribuer un type qui correspond au type de leur cible, si un tel type peut être trouvé.

Consultez la section Inférence de type dans le didacticiel Java pour plus d'informations.

Lii
la source
1

J'ai eu une erreur avec un tableau obtenant le max et le min donc ma solution était:

int max = Arrays.stream(arrayWithInts).max().getAsInt();
int min = Arrays.stream(arrayWithInts).min().getAsInt();
JPRLCol
la source