Java 8 stream's .min () y .max (): ¿por qué esto compila?

215

Nota: esta pregunta se origina en un enlace inactivo que era una pregunta SO anterior, pero aquí va ...

Vea este código ( nota: sé que este código no "funcionará" y que Integer::comparedebería usarse, lo acabo de extraer de la pregunta vinculada ):

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());

Según el javadoc de .min()y .max(), el argumento de ambos debería ser a Comparator. Sin embargo, aquí las referencias de métodos son a métodos estáticos de la Integerclase.

Entonces, ¿por qué esto compila en absoluto?

fge
fuente
66
Tenga en cuenta que no funciona correctamente, debería estar usando en Integer::comparelugar de Integer::maxy Integer::min.
Christoffer Hammarström
@ ChristofferHammarström lo sé; tenga en cuenta cómo dije antes del extracto del código "Lo sé, es absurdo"
fge
3
No estaba tratando de corregirte, le digo a la gente en general. Lo hiciste sonar como si pensaras que la parte que es absurda es que los métodos de Integerno son métodos de Comparator.
Christoffer Hammarström

Respuestas:

242

Déjame explicarte lo que está sucediendo aquí, ¡porque no es obvio!

Primero, Stream.max()acepta una instancia de Comparatormodo que los elementos en la secuencia se puedan comparar entre sí para encontrar el mínimo o el máximo, en un orden óptimo del que no tenga que preocuparse demasiado.

Entonces la pregunta es, por supuesto, ¿por qué se Integer::maxacepta? ¡Después de todo no es un comparador!

La respuesta está en la forma en que la nueva funcionalidad lambda funciona en Java 8. Se basa en un concepto que se conoce informalmente como interfaces de "método abstracto único" o interfaces "SAM". La idea es que cualquier interfaz con un método abstracto puede implementarse automáticamente por cualquier lambda, o referencia de método, cuya firma de método coincida con el método en la interfaz. Entonces, examinando la Comparatorinterfaz (versión simple):

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

Si un método está buscando un Comparator<Integer>, entonces esencialmente está buscando esta firma:

int xxx(Integer o1, Integer o2);

Yo uso "xxx" porque el nombre del método no se utiliza con fines coincidentes .

Por lo tanto, tanto Integer.min(int a, int b)y Integer.max(int a, int b)son tan cerca que autoboxing permitirá que esto aparezca como una Comparator<Integer>en un contexto método.

David M. Lloyd
fuente
28
o alternativamente: list.stream().mapToInt(i -> i).max().get().
Assylias
13
@assylias Desea usar en .getAsInt()lugar de get()sin embargo, ya que se trata de un OptionalInt.
skiwi
... cuando lo que solo intentamos es proporcionar un comparador personalizado para una max()función!
Manu343726
Vale la pena señalar que esta "interfaz SAM" en realidad se llama una "interfaz funcional" y que al mirar la Comparatordocumentación podemos ver que está decorada con la anotación @FunctionalInterface. Este decorador es la magia que permite Integer::maxy Integer::minse convierte en a Comparator.
Chris Kerekes
2
@ChrisKerekes el decorador @FunctionalInterfacees principalmente para fines de documentación, ya que el compilador puede hacerlo felizmente con cualquier interfaz con un solo método abstracto.
errantlinguist
117

Comparatores una interfaz funcional y Integer::maxcumple con esa interfaz (después de tomar en cuenta el autoboxing / unboxing). Toma dos intvalores y devuelve un int- tal como esperaría un Comparator<Integer>a (nuevamente, entrecerrando los ojos para ignorar la diferencia Entero / int).

Sin embargo, no esperaría que hiciera lo correcto, dado que Integer.maxno cumple con la semántica de Comparator.compare. Y, de hecho, en realidad no funciona en general. Por ejemplo, haga un pequeño cambio:

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

... y ahora el maxvalor es -20 y el minvalor es -1.

En cambio, ambas llamadas deben usar Integer::compare:

System.out.println(list.stream().max(Integer::compare).get());
System.out.println(list.stream().min(Integer::compare).get());
Jon Skeet
fuente
1
Sé sobre las interfaces funcionales; y sé por qué da resultados incorrectos. Me pregunto cómo demonios el compilador no me
gritó
66
@fge: Bueno, ¿fue el unboxing del que no estabas claro? (No he examinado exactamente esa parte.) A Comparator<Integer>tendría int compare(Integer, Integer)... no es alucinante que Java permita una referencia de método int max(int, int)para convertir a eso ...
Jon Skeet
77
@fge: ¿Se supone que el compilador conoce la semántica de Integer::max? Desde su perspectiva, usted pasó una función que cumplió con sus especificaciones, eso es todo lo que realmente puede continuar.
Mark Peters
66
@fge: En particular, si comprende parte de lo que está sucediendo, pero está intrigado por un aspecto específico del mismo, vale la pena aclararlo en la pregunta para evitar que las personas pierdan su tiempo explicando lo que ya sabe.
Jon Skeet
20
Creo que el problema subyacente es el tipo de firma de Comparator.compare. Debería devolver un enumde {LessThan, GreaterThan, Equal}, no un int. De esa manera, la interfaz funcional no coincidiría realmente y obtendría un error de compilación. IOW: la firma de tipo de Comparator.compareno captura adecuadamente la semántica de lo que significa comparar dos objetos y, por lo tanto, otras interfaces que no tienen absolutamente nada que ver con la comparación de objetos tienen accidentalmente la misma firma de tipo.
Jörg W Mittag
19

Esto funciona porque Integer::minresuelve una implementación de la Comparator<Integer>interfaz.

La referencia de método de Integer::minresuelve Integer.min(int a, int b), resuelve IntBinaryOperatory presumiblemente autoboxing ocurre en algún lugar, lo que lo convierte en un BinaryOperator<Integer>.

Y los métodos de min()respuesta solicitan la implementación de la interfaz. Ahora esto se resuelve con el método único . Cuál es de tipo .max()Stream<Integer>Comparator<Integer>
Integer compareTo(Integer o1, Integer o2)BinaryOperator<Integer>

Y así, la magia ha sucedido ya que ambos métodos son a BinaryOperator<Integer>.

skiwi
fuente
No es del todo correcto decir que se Integer::minimplementa Comparable. No es un tipo que pueda implementar nada. Pero se evalúa en un objeto que implementa Comparable.
Lii
1
@Lii Gracias, lo arreglé hace un momento.
skiwi
Comparator<Integer>es una interfaz de método abstracto único (también conocido como "funcional") y Integer::mincumple con su contrato, por lo que la lambda se puede interpretar como esto. No sé cómo ves que BinaryOperator entra en juego aquí (o IntBinaryOperator, tampoco): no hay una relación de subtipo entre eso y Comparator.
Paŭlo Ebermann
2

Además de la información proporcionada por David M. Lloyd, se podría agregar que el mecanismo que permite esto se llama tipificación de objetivos .

La idea es que el tipo que el compilador asigna a las expresiones lambda o referencias de un método no depende solo de la expresión en sí, sino también de dónde se usa.

El objetivo de una expresión es la variable a la que se asigna su resultado o el parámetro al que se pasa su resultado.

Las expresiones Lambda y las referencias de métodos tienen asignado un tipo que coincide con el tipo de su destino, si se puede encontrar dicho tipo.

Consulte la sección Inferencia de tipo en el Tutorial de Java para obtener más información.

Lii
fuente
1

Tuve un error con una matriz obteniendo el máximo y el mínimo, por lo que mi solución fue:

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