对 Java 8 比较器类型推断非常困惑

2022-08-31 10:28:10

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

这些是我的发现。假设我们有一个 类型 ,添加了一些歌曲,有3个标准的获取方法:ArrayListSong

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

以下是对两种有效排序方法的调用,没问题:

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

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

一旦我开始链,就会发生以下情况: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())
        );

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

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

现在来了令人困惑的部分。对于 p,即 List,这解决了以下两个调用的所有编译错误。但是,对于 ,它解决了第一个问题,而不是最后一个问题。我测试添加了几个额外的调用,它总是显示最后一个错误,除非我为参数放置。laylist1.sortthenComparingCollections.sortthenComparing(Song p1)

现在,我继续通过创建一个和使用:TreeSetObjects.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())
            );

与 中发生的情况相同,对于 ,没有编译错误,但对于显示错误的最后一次调用。TreeSetObjects.comparethenComparing

任何人都可以解释为什么会发生这种情况,以及为什么在简单地调用比较方法时根本不需要使用(无需进一步调用)。(Song p1)thenComparing

关于同一主题的另一个查询是当我对:TreeSet

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

即,从比较方法调用的第一个lambda参数中删除类型,它显示了对比较的调用和第一次调用下的语法错误,但不是对最终调用 - 几乎与上面发生的事情相反!然而,对于所有其他 3 个示例,即 with ,当我删除第一个参数类型时,它会显示所有调用的语法错误。SongthenComparingthenComparingObjects.compareList.sortCollections.sortSong

提前非常感谢。

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

Sort errors in Eclipse


答案 1

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

你的潜在问题似乎是:“为什么当我开始链接时,它会停止工作。原因是,虽然 lambda 表达式和泛型方法调用在显示为方法参数时是多元表达式(它们的类型是上下文相关的),但当它们显示为方法接收器表达式时,它们不是。

当你说

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

有足够的类型信息来求解 的类型参数 和 参数类型 。调用从 的签名获取其目标类型,因此已知它必须返回一个 ,因此必须是 。comparing()p1comparing()Collections.sortcomparing()Comparator<Song>p1Song

但是当你开始链接时:

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

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

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

  • 使用精确的方法引用(没有重载的方法),如 .然后,这会提供足够的类型信息来推断调用的类型变量,从而为其提供类型,从而继续向下链。Song::getTitlecomparing()
  • 使用显式 lambda(如示例中所示)。
  • 为调用提供类型见证:。comparing()Comparator.<Song, String>comparing(...)
  • 通过将接收方表达式强制转换为 来提供具有强制转换的显式目标类型。Comparator<Song>

答案 2

问题在于类型推断。如果不向第一个比较添加 ,则不知道输入的类型,因此默认为 Object。(Song s)comparator.comparing

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

  1. 使用新的 Java 8 方法引用语法

     Collections.sort(playlist,
                Comparator.comparing(Song::getTitle)
                .thenComparing(Song::getDuration)
                .thenComparing(Song::getArtist)
                );
    
  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)
                );
    

    编辑

  3. 强制比较器返回类型(请注意,您同时需要输入类型和比较键类型)

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

我认为“最后一个”语法错误误导了你。这实际上是整个链的类型问题,只是编译器只是将链的末端标记为语法错误,因为我想这是最终返回类型不匹配的时候。thenComparing

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


推荐