Muy confundido por la inferencia de tipo de Java 8 Comparator

84

He estado observando la diferencia entre Collections.sorty list.sort, específicamente con respecto al uso de Comparatormétodos estáticos y si se requieren tipos de parámetros en las expresiones lambda. Antes de comenzar, sé que podría usar referencias de métodos, por ejemplo, Song::getTitlepara superar mis problemas, pero mi consulta aquí no es tanto algo que quiero arreglar sino algo a lo que quiero una respuesta, es decir, ¿por qué el compilador de Java lo maneja de esta manera? .

Estos son mis hallazgos. Supongamos que tenemos un ArrayListtipo de Song, con algunas canciones agregadas, hay 3 métodos de obtención estándar:

    ArrayList<Song> playlist1 = new ArrayList<Song>();

    //add some new Song objects
    playlist.addSong( new Song("Only Girl (In The World)", 235, "Rhianna") );
    playlist.addSong( new Song("Thinking of Me", 206, "Olly Murs") );
    playlist.addSong( new Song("Raise Your Glass", 202,"P!nk") );

Aquí hay una llamada a ambos tipos de método de clasificación que funciona, no hay problema:

Collections.sort(playlist1, 
            Comparator.comparing(p1 -> p1.getTitle()));

playlist1.sort(
            Comparator.comparing(p1 -> p1.getTitle()));

Tan pronto como empiezo a encadenar thenComparing, sucede lo siguiente:

Collections.sort(playlist1,
            Comparator.comparing(p1 -> p1.getTitle())
            .thenComparing(p1 -> p1.getDuration())
            .thenComparing(p1 -> p1.getArtist())
            );

playlist1.sort(
        Comparator.comparing(p1 -> p1.getTitle())
        .thenComparing(p1 -> p1.getDuration())
        .thenComparing(p1 -> p1.getArtist())
        );

es decir, errores de sintaxis porque ya no conoce el tipo de p1. Entonces, para solucionar esto, agrego el tipo Songal primer parámetro (de comparación):

Collections.sort(playlist1,
            Comparator.comparing((Song p1) -> p1.getTitle())
            .thenComparing(p1 -> p1.getDuration())
            .thenComparing(p1 -> p1.getArtist())
            );

playlist1.sort(
        Comparator.comparing((Song p1) -> p1.getTitle())
        .thenComparing(p1 -> p1.getDuration())
        .thenComparing(p1 -> p1.getArtist())
        );

Ahora aquí viene la parte CONFUSA. Para p laylist1.sort, es decir, la Lista, esto resuelve todos los errores de compilación, para las dos thenComparingllamadas siguientes . Sin embargo, Collections.sortlo resuelve para el primero, pero no para el último. Probé, agregué varias llamadas adicionales thenComparingy siempre muestra un error para la última, a menos que puse (Song p1)el parámetro.

Ahora continué probando esto con la creación de un TreeSety con el uso Objects.compare:

int x = Objects.compare(t1, t2, 
                Comparator.comparing((Song p1) -> p1.getTitle())
                .thenComparing(p1 -> p1.getDuration())
                .thenComparing(p1 -> p1.getArtist())
                );


    Set<Song> set = new TreeSet<Song>(
            Comparator.comparing((Song p1) -> p1.getTitle())
            .thenComparing(p1 -> p1.getDuration())
            .thenComparing(p1 -> p1.getArtist())
            );

Ocurre lo mismo que en, para el TreeSet, no hay errores de compilación pero para Objects.comparela última llamada a thenComparingmuestra un error.

¿Alguien puede explicar por qué está sucediendo esto y también por qué no es necesario usar (Song p1)nada cuando simplemente se llama al método de comparación (sin más thenComparingllamadas)?

Otra consulta sobre el mismo tema es cuando hago esto con TreeSet:

Set<Song> set = new TreeSet<Song>(
            Comparator.comparing(p1 -> p1.getTitle())
            .thenComparing(p1 -> p1.getDuration())
            .thenComparing(p1 -> p1.getArtist())
            );

es decir, elimine el tipo Songdel primer parámetro lambda para la llamada al método de comparación, muestra errores de sintaxis bajo la llamada a comparar y la primera llamada a, thenComparingpero no a la llamada final thenComparing, ¡casi lo contrario de lo que estaba sucediendo arriba! Considerando que, para los otros 3 ejemplos, es decir Objects.compare, con List.sorty Collections.sortcuando elimino ese primer Songtipo de parámetro, muestra errores de sintaxis para todas las llamadas.

Muchas gracias de antemano.

Editado para incluir una captura de pantalla de los errores que recibía en Eclipse Kepler SR2, que desde entonces he descubierto que son específicos de Eclipse porque cuando se compila con el compilador java JDK8 en la línea de comandos, se compila correctamente.

Ordenar errores en Eclipse

Tranquilidad
fuente
Sería útil si incluyera en su pregunta todos los mensajes de error de compilación que recibe en todas sus pruebas.
Eran
1
Para ser honesto, creo que sería más fácil para alguien ver cuáles son los problemas ejecutando el código fuente.
Tranquility
¿Cuáles son los tipos de t1y t2en el Objects.compareejemplo? Estoy tratando de inferirlos, pero superponer mi inferencia de tipo sobre la inferencia de tipo del compilador es intratable. :-)
Stuart Marks
1
Además, ¿qué compilador estás usando?
Stuart Marks
1
Tienes dos problemas separados aquí. Uno de los que respondieron señaló que podría usar referencias de métodos, que en cierto modo descartó. Así como las lambdas vienen en sabores tanto "tipados explícitamente" como "tipados implícitamente", las referencias a métodos vienen en sabores "exactos" (una sobrecarga) e "inexactos" (sobrecargas múltiples). Se puede utilizar una referencia de método exacta o una lambda explícita para proporcionar información de tipado adicional si no está presente. (También se pueden usar testigos y yesos de tipo explícito, pero a menudo son martillos más grandes.)
Brian Goetz

Respuestas:

105

Primero, todos los ejemplos que dice que causan errores se compilan bien con la implementación de referencia (javac de JDK 8.) También funcionan bien en IntelliJ, por lo que es muy posible que los errores que está viendo sean específicos de Eclipse.

Su pregunta subyacente parece ser: "¿por qué deja de funcionar cuando empiezo a encadenar?". La razón es que, mientras que las expresiones lambda y las invocaciones de métodos genéricos son poli-expresiones (su tipo es sensible al contexto) cuando aparecen como parámetros de método, cuando aparecen como expresiones de receptor de método, no lo son.

Cuando tu dices

Collections.sort(playlist1, comparing(p1 -> p1.getTitle()));

hay suficiente información de tipo para resolver tanto el tipo de argumento comparing()como el tipo de argumento p1. La comparing()llamada obtiene su tipo de destino de la firma de Collections.sort, por lo que se sabe que comparing()debe devolver un Comparator<Song>y, por p1lo tanto, debe ser Song.

Pero cuando empiezas a encadenar:

Collections.sort(playlist1,
                 comparing(p1 -> p1.getTitle())
                     .thenComparing(p1 -> p1.getDuration())
                     .thenComparing(p1 -> p1.getArtist()));

ahora tenemos un problema. Sabemos que la expresión compuesta comparing(...).thenComparing(...)tiene un tipo objetivo de Comparator<Song>, pero debido a que la expresión receptora para la cadena,, comparing(p -> p.getTitle())es una llamada a un método genérico, y no podemos inferir sus parámetros de tipo a partir de sus otros argumentos, no tenemos suerte. . Como no conocemos el tipo de esta expresión, no sabemos que tiene un thenComparingmétodo, etc.

Hay varias formas de solucionar este problema, todas las cuales implican inyectar más información de tipo para que el objeto inicial de la cadena se pueda escribir correctamente. Aquí están, en orden aproximado de conveniencia decreciente y creciente intrusión:

  • Utilice una referencia de método exacta (una sin sobrecargas), como Song::getTitle. Esto proporciona suficiente información de tipo para inferir las variables de tipo para la comparing()llamada y, por lo tanto, darle un tipo y, por lo tanto, continuar en la cadena.
  • Utilice una lambda explícita (como hizo en su ejemplo).
  • Proporcionar un testigo tipo para la comparing()llamada: Comparator.<Song, String>comparing(...).
  • Proporcione un tipo de destino explícito con una conversión, convirtiendo la expresión del receptor en Comparator<Song>.
Brian Goetz
fuente
13
+1 por responder realmente al OP "¿por qué no puede el compilador inferir esto?" En lugar de simplemente dar soluciones provisionales.
Joffrey
Gracias por tu respuesta Brian. Sin embargo, todavía encuentro algo sin respuesta, ¿por qué List.sort se comporta de manera diferente a Collections.sort, ya que el primero solo requiere que la primera lambda contenga el tipo de parámetro, pero el último también requiere que el último, por ejemplo, si tengo una comparación seguido de 5 llamadas de thenComparing, tendría que poner (Song p1) en la comparación y en la última, entoncesComparing. También en mi publicación original, verá el ejemplo inferior del TreeSet donde elimino todos los tipos de parámetros y, sin embargo, la última llamada a thenComparing está bien, pero las otras no, por lo que esto se comporta de manera diferente.
Tranquility
3
@ user3780370 ¿Sigues usando el compilador Eclipse? No he visto este comportamiento, si entiendo correctamente su pregunta. ¿Puedes (a) probarlo con javac desde JDK 8 y (b) si aún falla, publicar el código?
Brian Goetz
@BrianGoetz Gracias por esta sugerencia. Acabo de compilarlo dentro de la ventana de comandos usando javac y se compila como dijiste. Parece ser un problema de Eclipse. Todavía no he actualizado a Eclipse Luna, que está especialmente diseñado para JDK8, así que espero que se solucione en eso. De hecho, tengo una captura de pantalla para mostrarte lo que estaba sucediendo en Eclipse, pero no sé cómo publicar aquí.
Tranquilidad
2
Creo que te refieres Comparator.<Song, String>comparing(...).
shmosel
23

El problema es la inferencia de tipos. Sin agregar (Song s)a la primera comparación, comparator.comparingno conoce el tipo de entrada, por lo que el valor predeterminado es Object.

Puede solucionar este problema de 1 de 3 formas:

  1. Utilice la nueva sintaxis de referencia del método Java 8

     Collections.sort(playlist,
                Comparator.comparing(Song::getTitle)
                .thenComparing(Song::getDuration)
                .thenComparing(Song::getArtist)
                );
    
  2. Extraiga cada paso de comparación en una referencia local

      Comparator<Song> byName = (s1, s2) -> s1.getArtist().compareTo(s2.getArtist());
    
      Comparator<Song> byDuration = (s1, s2) -> Integer.compare(s1.getDuration(), s2.getDuration());
    
        Collections.sort(playlist,
                byName
                .thenComparing(byDuration)
                );
    

    EDITAR

  3. Forzar el tipo devuelto por el Comparador (tenga en cuenta que necesita tanto el tipo de entrada como el tipo de clave de comparación)

    sort(
      Comparator.<Song, String>comparing((s) -> s.getTitle())
                .thenComparing(p1 -> p1.getDuration())
                .thenComparing(p1 -> p1.getArtist())
                );
    

Creo que el "último" thenComparingerror de sintaxis te está engañando. En realidad, es un problema de tipo con toda la cadena, es solo que el compilador solo marca el final de la cadena como un error de sintaxis porque es entonces cuando el tipo de retorno final no coincide, supongo.

No estoy seguro de por qué Listestá haciendo un mejor trabajo de inferencia que Collectionporque debería hacer el mismo tipo de captura, pero aparentemente no.

dkatzel
fuente
¿Por qué lo conoce ArrayListpara la Collectionssolución pero no para la solución (dado que la primera llamada en la cadena tiene un Songparámetro)?
Sotirios Delimanolis
4
Gracias por su respuesta, sin embargo, si lee mi publicación, verá que dije: "Antes de comenzar, sé que podría usar referencias de métodos, por ejemplo, Song :: getTitle para superar mis problemas, pero mi consulta aquí no es tanto algo que quiero arreglar pero algo a lo que quiero una respuesta, es decir, por qué el compilador de Java lo está manejando de esta manera ".
Tranquilidad
Quiero una respuesta de por qué el compilador se comporta de esa manera cuando uso expresiones lambda. Acepta comparar (s -> s.getArtist ()) pero luego cuando encadeno .thenComparing (s -> s.getDuration ()), por ejemplo, me da errores de sintaxis para ambas llamadas, si luego agrego un tipo explícito en la llamada de comparación, por ejemplo, comparando ((Song s) -> s.getArtist ()) entonces esto soluciona ese problema y para List.sort y TreeSet también resuelve todos los errores de compilación adicionales sin tener que agregar tipos de parámetros adicionales, sin embargo, para the Collections.sort & Objects.compare ejemplos el último entonces La comparación todavía falla
Tranquility
1

Otra forma de lidiar con este error de tiempo de compilación:

Emite la variable de tu primera función de comparación explícitamente y luego listo. He ordenado la lista de objetos org.bson.Documents. Mire el código de muestra

Comparator<Document> comparator = Comparator.comparing((Document hist) -> (String) hist.get("orderLineStatus"), reverseOrder())
                       .thenComparing(hist -> (Date) hist.get("promisedShipDate"))
                       .thenComparing(hist -> (Date) hist.get("lastShipDate"));
list = list.stream().sorted(comparator).collect(Collectors.toList());
Rajni Gangwar
fuente
0

playlist1.sort(...) crea un límite de Song para la variable de tipo E, desde la declaración de playlist1, que "ondula" al comparador.

En Collections.sort(...), no existe tal límite, y la inferencia del tipo del primer comparador no es suficiente para que el compilador infiera el resto.

Creo que obtendrás un comportamiento "correcto" Collections.<Song>sort(...), pero no tienes una instalación de Java 8 para probarlo.

amalloy
fuente
hola, sí, tiene razón en que agregar Colecciones. <Canción> elimina el error de la última llamada de comparación
Tranquilidad