web-dev-qa-db-fra.com

Très confus par l'inférence de type Comparateur Java 8

J'ai étudié la différence entre Collections.sort et list.sort, en particulier en ce qui concerne l'utilisation des méthodes statiques Comparator et si les types param sont requis dans les expressions lambda. Avant de commencer, je sais que je pourrais utiliser des références de méthode, par exemple. Song::getTitle pour résoudre mes problèmes, mais ma requête ici n’est pas tant quelque chose que je veux corriger, mais une chose à laquelle je veux une réponse, c’est-à-dire pourquoi le compilateur Java le gère-t-il de cette façon. 

Ce sont mes conclusions. Supposons que nous ayons une ArrayList de type Song, avec quelques chansons ajoutées, il existe 3 méthodes get standard:

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

Voici un appel aux deux types de méthode de tri qui fonctionne, pas de problème:

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

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

Dès que je commence à chaîner thenComparing, voici ce qui se passe:

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

c'est-à-dire des erreurs de syntaxe car il ne connaît plus le type de p1. Donc, pour résoudre ce problème, j'ajoute le type Song au premier paramètre (de comparaison):

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

Maintenant, voici la partie CONFUSING. Pour playlist1.sort, c'est-à-dire la liste, cela résout toutes les erreurs de compilation, pour les deux appels thenComparing suivants. Cependant, pour Collections.sort, il le résout pour le premier, mais pas le dernier. J'ai testé ajouté plusieurs appels supplémentaires à thenComparing et il indique toujours une erreur pour le dernier, à moins que je mette (Song p1) pour le paramètre.

Maintenant, j'ai continué à tester ceci en créant une TreeSet et en utilisant 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())
            );

La même chose se passe comme dans, pour la TreeSet, il n'y a pas d'erreur de compilation, mais pour Objects.compare, le dernier appel à thenComparing indique une erreur.

Quelqu'un peut-il expliquer pourquoi cela se produit et pourquoi il n'est pas nécessaire d'utiliser (Song p1) du tout lorsque vous appelez simplement la méthode de comparaison (sans autre appel thenComparing).

Une autre requête sur le même sujet est quand je fais ceci à la TreeSet:

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

c’est-à-dire supprimer le type Song du premier paramètre lambda pour l’appel de la méthode de comparaison, il affiche les erreurs de syntaxe lors de l’appel à comparer et du premier appel à thenComparing mais pas à l’appel final à thenComparing - presque le contraire de ce qui se passait précédemment Alors que, pour tous les 3 autres exemples, c’est-à-dire avec Objects.compare, List.sort et Collections.sort, lorsque je supprime ce premier type param Song, il affiche des erreurs de syntaxe pour tous les appels.

Merci d'avance.

Modifié pour inclure la capture d’écran des erreurs que je recevais dans Eclipse Kepler SR2, que j’ai trouvé depuis, sont spécifiques à Eclipse, car lorsqu’il est compilé à l’aide du compilateur Java JDK8, il compile bien. 

Sort errors in Eclipse

65
Tranquility

Premièrement, tous les exemples que vous dites provoquent des erreurs se compilent correctement avec l’implémentation de référence (javac de JDK 8.) Ils fonctionnent également correctement dans IntelliJ. Il est donc tout à fait possible que les erreurs que vous voyez soient spécifiques à Eclipse. 

Votre question sous-jacente semble être: "Pourquoi ça cesse de fonctionner quand je commence à enchaîner". La raison en est que, bien que les expressions lambda et les invocations de méthodes génériques soient poly expressions (leur type dépend du contexte) lorsqu'elles apparaissent en tant que paramètres de méthode, lorsqu'elles apparaissent plutôt en tant qu'expresseurs de méthode, elles ne le sont pas. 

Quand tu dis

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

il y a suffisamment d'informations de type à résoudre pour l'argument de type comparing() et l'argument de type p1. L'appel comparing() tire son type de cible de la signature Collections.sort; ainsi, comparing() doit renvoyer un Comparator<Song>, et donc p1 doit être Song

Mais quand vous commencez à enchaîner:

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

maintenant nous avons un problème. Nous savons que l'expression composée comparing(...).thenComparing(...) a un type de cible de Comparator<Song>, mais comme l'expression du destinataire pour la chaîne, comparing(p -> p.getTitle()), est un appel à une méthode générique et que nous ne pouvons pas déduire ses paramètres de type de ses autres arguments, nous sommes en quelque sorte pas de chance. Puisque nous ne connaissons pas le type de cette expression, nous ne savons pas qu'elle a une méthode thenComparing, etc. 

Il existe plusieurs façons de résoudre ce problème, toutes impliquant l'injection de davantage d'informations de type afin que l'objet initial de la chaîne puisse être correctement typé. Les voici, par ordre approximatif de désir décroissant et d’intrusion croissante:

  • Utilisez une référence de méthode exacte (sans surcharge), telle que Song::getTitle. Cela donne alors suffisamment d’informations sur le type pour déduire les variables de type pour l’appel comparing(), et donc lui donner un type, et donc continuer le long de la chaîne.
  • Utilisez un lambda explicite (comme vous l'avez fait dans votre exemple).
  • Fournissez un témoin de type pour l'appel comparing(): Comparator.<Song, String>comparing(...)
  • Fournissez un type de cible explicite avec une conversion en convertissant l'expression du destinataire en Comparator<Song>
79
Brian Goetz

Le problème est l'inférence de type. Sans ajouter un (Song s) à la première comparaison, comparator.comparing ne connaît pas le type de l'entrée, il utilise donc par défaut Object.

Vous pouvez résoudre ce problème de 1 à 3 manières:

  1. Utiliser la nouvelle syntaxe de référence de la méthode Java 8

     Collections.sort(playlist,
                Comparator.comparing(Song::getTitle)
                .thenComparing(Song::getDuration)
                .thenComparing(Song::getArtist)
                );
    
  2. Extrayez chaque étape de comparaison dans une référence locale

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

    MODIFIER

  3. Forcer le type renvoyé par le comparateur (notez que vous avez besoin du type d'entrée et du type de clé de comparaison)

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

Je pense que la "dernière" erreur de syntaxe thenComparing vous induit en erreur. C'est en fait un problème de type avec toute la chaîne, c'est juste que le compilateur marque la fin de la chaîne comme une erreur de syntaxe car c'est le moment où le type de retour final ne correspond pas, je suppose.

Je ne suis pas sûr de savoir pourquoi List fait un meilleur travail d'inférence que Collection puisqu'il devrait faire le même type de capture mais apparemment pas.

19
dkatzel

Une autre façon de traiter cette erreur de compilation:

Lancez explicitement la variable de votre première fonction de comparaison, puis lancez-vous. J'ai trier la liste d'objet org.bson.Documents. Veuillez regarder un exemple de code

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());
0
Rajni Gangwar

playlist1.sort(...) crée une limite de Song pour la variable de type E, à partir de la déclaration de playlist1, qui "ondule" au comparateur. 

Dans Collections.sort(...), il n'y a pas de telle borne et l'inférence du type du premier comparateur n'est pas suffisante pour que le compilateur puisse déduire le reste. 

Je pense que vous obtiendrez le comportement "correct" de Collections.<Song>sort(...), mais vous ne devez pas installer Java 8 pour le tester à votre place.

0
amalloy