Java 8 Comparator类型推断非常困惑

Tra*_*ity 74 java sorting lambda comparator java-8

我一直在研究Collections.sort和之间的区别list.sort,特别是关于使用Comparator静态方法以及lambda表达式中是否需要param类型.在我们开始之前,我知道我可以使用方法引用,例如Song::getTitle克服我的问题,但我的查询并不是我想修复的东西,而是我想要的答案,即为什么Java编译器以这种方式处理它.

这是我的发现.假设我们有一个ArrayList类型Song,添加了一些歌曲,有3种标准的get方法:

    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") );
Run Code Online (Sandbox Code Playgroud)

这里调用两种类型的排序方法,没问题:

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

playlist1.sort(
            Comparator.comparing(p1 -> p1.getTitle()));
Run Code Online (Sandbox Code Playgroud)

一旦我开始连锁thenComparing,就会发生以下情况:

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())
        );
Run Code Online (Sandbox Code Playgroud)

即语法错误,因为它不再知道类型p1.所以为了解决这个问题,我将类型添加Song到第一个参数(比较):

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())
        );
Run Code Online (Sandbox Code Playgroud)

现在来到ConfUSING部分.对于p laylist1.sort,即List,这解决了以下thenComparing调用的所有编译错误.但是,因为Collections.sort它解决了第一个问题,而不是最后一个问题.我测试了添加了几个额外的调用thenComparing,它总是显示最后一个错误,除非我(Song p1)输入参数.

现在我继续进行测试,创建一个TreeSet并使用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())
            );
Run Code Online (Sandbox Code Playgroud)

同样的事情发生在,因为TreeSet,没有编译错误,但Objects.compare最后一次调用thenComparing显示错误.

任何人都可以解释为什么会发生这种情况,以及为什么(Song p1)在简单地调用比较方法时没有必要使用它(没有进一步的thenComparing调用).

关于同一主题的另一个问题是当我这样做时TreeSet:

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

Song从比较方法调用的第一个lambda参数中删除类型,它显示在比较调用和第一次调用thenComparing但不是最终调用的语法错误thenComparing- 几乎与上面发生的相反!然而,所有其他3个例子,即有Objects.compare,List.sort而且Collections.sort当我删除第一个Song参数类型它显示了所有调用的语法错误.

提前谢谢了.

编辑包括我在Eclipse Kepler SR2中收到的错误的屏幕截图,我现在发现这些错误是特定于Eclipse的,因为当在命令行上使用JDK8 java编译器进行编译时,它编译正常.

在Eclipse中排序错误

Bri*_*etz 87

首先,您说的所有示例都会导致错误,使用参考实现(JDK 8中的javac)编译良好.它们在IntelliJ中也可以正常工作,因此您看到的错误很可能是特定于Eclipse的.

您的基本问题似乎是:"当我开始链接时,为什么它会停止工作." 原因是,虽然lambda表达式和泛型方法调用是多重表达式(它们的类型是上下文相关的),当它们作为方法参数出现时,当它们显示为方法接收器表达式时,它们不是.

当你说

Collections.sort(playlist1, comparing(p1 -> p1.getTitle()));
Run Code Online (Sandbox Code Playgroud)

有足够的类型信息来解决类型参数comparing()和参数类型p1.该comparing()调用从签名中获取其目标类型Collections.sort,因此它comparing()必须返回一个Comparator<Song>,因此p1必须是Song.

但是当你开始链接时:

Collections.sort(playlist1,
                 comparing(p1 -> p1.getTitle())
                     .thenComparing(p1 -> p1.getDuration())
                     .thenComparing(p1 -> p1.getArtist()));
Run Code Online (Sandbox Code Playgroud)

现在我们遇到了问题.我们知道复合表达式comparing(...).thenComparing(...)的目标类型是Comparator<Song>,但因为链的接收器表达式comparing(p -> p.getTitle())是一个泛型方法调用,而我们无法从其他参数中推断出它的类型参数,所以我们运气不好.由于我们不知道这个表达式的类型,我们不知道它有一个thenComparing方法等.

有几种方法可以解决这个问题,所有这些方法都涉及注入更多类型信息,以便可以正确键入链中的初始对象.在这里,它们按照降低需求和增加侵入性的粗略顺序:

  • 使用精确的方法引用(没有重载的方法),比如Song::getTitle.然后,它会提供足够的类型信息来推断comparing()调用的类型变量,从而为其提供一个类型,从而继续沿着链继续.
  • 使用显式lambda(如您在示例中所做的那样).
  • comparing()呼叫提供类型见证:Comparator.<Song, String>comparing(...).
  • 通过将接收器表达式强制转换为提供具有强制转换的显式目标类型Comparator<Song>.

  • +1实际上回答了OP"为什么编译器无法推断出这个",而不仅仅是给出解决方法/解决方案. (12认同)
  • @ user3780370你还在使用Eclipse编译器吗?如果我正确理解你的问题,我还没有看到这种行为.您是否可以(a)使用JDK 8中的javac和(b)如果仍然失败,请发布代码? (3认同)
  • 我认为你的意思是"比较者."歌曲,字符串>比较(...)`. (2认同)

dka*_*zel 21

问题是类型推断.在不添加(Song s)第一个比较的情况下,comparator.comparing不知道输入的类型,因此它默认为Object.

您可以通过以下3种方法解决此问题:

  1. 使用新的Java 8方法参考语法

     Collections.sort(playlist,
                Comparator.comparing(Song::getTitle)
                .thenComparing(Song::getDuration)
                .thenComparing(Song::getArtist)
                );
    
    Run Code Online (Sandbox Code Playgroud)
  2. 将每个比较步骤拉出到本地参考中

      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)
                );
    
    Run Code Online (Sandbox Code Playgroud)

    编辑

  3. 强制比较器返回的类型(注意你需要输入类型和比较键类型)

    sort(
      Comparator.<Song, String>comparing((s) -> s.getTitle())
                .thenComparing(p1 -> p1.getDuration())
                .thenComparing(p1 -> p1.getArtist())
                );
    
    Run Code Online (Sandbox Code Playgroud)

我认为"最后" thenComparing语法错误会误导你.它实际上是整个链的类型问题,只是编译器只将链的末尾标记为语法错误,因为当最终返回类型与我猜不匹配时.

我不确定为什么List做一个更好的推理工作,Collection因为它应该做相同的捕获类型,但显然不是.

  • 感谢您的回复,但是,如果您阅读我的帖子,您会看到我说:"在我们开始之前,我知道我可以使用方法引用,例如Song :: getTitle来克服我的问题,但我的查询不是那么多我想要解决的问题,但我想要回答的问题,即为什么Java编译器会以这种方式处理它." (4认同)