Java 8 Comparator 类型推断非常困惑

Posted

技术标签:

【中文标题】Java 8 Comparator 类型推断非常困惑【英文标题】:Very confused by Java 8 Comparator type inference 【发布时间】:2014-08-17 16:20:50 【问题描述】:

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

这些是我的发现。假设我们有一个Song 类型的ArrayList,加上一些歌曲,有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") );

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

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

即语法错误,因为它不再知道 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())
        );

现在是令人困惑的部分。对于 playlist1.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())
            );

同样的事情发生在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())
            );

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

非常感谢。

已编辑以包含我在 Eclipse Kepler SR2 中收到的错误截图,我现在发现它是 Eclipse 特定的,因为在命令行上使用 JDK8 java 编译器编译时,它可以编译。

【问题讨论】:

如果您在问题中包含您在所有测试中收到的所有编译错误消息,将会很有帮助。 老实说,我认为通过自己运行源代码来了解问题所在最容易。 t1t2 示例中的t2 的类型是什么?我试图推断它们,但是将我的类型推断分层到编译器的类型推断上是难以处理的。 :-) 另外你用的是什么编译器? 这里有两个不同的问题。其中一位回答者指出您可以使用方法引用,但您对此略过不屑一顾。正如 lambda 有“显式类型”和“隐式类型”两种风格一样,方法引用也有“精确”(一个重载)和“不精确”(多个重载)两种风格。如果不存在,则可以使用精确方法 ref 或显式 lambda 来提供额外的类型信息。 (也可以使用显式类型的证人和演员表,但通常是更大的锤子。) 【参考方案1】:

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

您的基本问题似乎是:“为什么当我开始链接时它停止工作。”原因是,当 lambda 表达式和泛型方法调用作为方法参数出现时,它们是 poly 表达式(它们的类型是上下文相关的),而当它们作为方法接收器表达式出现时,它们不是。

当你说

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

有足够的类型信息来解决comparing() 的类型参数和p1 的参数类型。 comparing() 调用从Collections.sort 的签名中获取其目标类型,因此已知comparing() 必须返回Comparator&lt;Song&gt;,因此p1 必须是Song

但是当你开始链接时:

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

现在我们遇到了问题。我们知道复合表达式comparing(...).thenComparing(...) 的目标类型是Comparator&lt;Song&gt;,但是因为链的接收者表达式comparing(p -&gt; p.getTitle()) 是一个泛型方法调用,我们不能从它的其他参数推断它的类型参数,我们有点不走运。由于我们不知道这个表达式的类型,所以也不知道它有thenComparing方法等。

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

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

【讨论】:

+1 用于实际回答 OP“为什么编译器不能推断这一点”,而不仅仅是给出解决方法/解决方案。 谢谢你的回答布赖恩。但是,我仍然发现一些未解决的问题,为什么 List.sort 的行为与 Collections.sort 不同,因为前者只需要第一个 lambda 来包含参数类型,但后者还需要最后一个,例如如果我有一个比较,然后是 5 个 thenComparing 调用,我必须将 (Song p1) 放在比较中,最后放在 thenComparing 中。同样在我的原始帖子中,您将看到 TreeSet 的底部示例,在该示例中我删除了所有参数类型,但最后一次调用 thenComparing 是可以的,但其他的不是 - 所以它的行为不同。 @user3780370 你还在使用 Eclipse 编译器吗?如果我正确理解您的问题,我还没有看到这种行为。您能否(a)使用 JDK 8 中的 javac 进行尝试,(b)如果仍然失败,请发布代码? @BrianGoetz 感谢您的建议。我刚刚使用 javac 在命令行窗口中编译了它,它按照您所说的进行编译。这似乎是一个 Eclipse 问题。我还没有更新到 Eclipse Luna,它是专门为 JDK8 构建的,所以希望它可以修复。实际上,我有一个屏幕截图可以向您展示 Eclipse 中发生的事情,但不知道如何在此处发布。 我想你的意思是Comparator.&lt;Song, String&gt;comparing(...)【参考方案2】:

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

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

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

     Collections.sort(playlist,
                Comparator.comparing(Song::getTitle)
                .thenComparing(Song::getDuration)
                .thenComparing(Song::getArtist)
                );
    

    将每个比较步骤提取到本地参考

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

    编辑

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

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

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

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

【讨论】:

为什么它知道ArrayList 而不是Collections 解决方案(假设链中的第一个调用有Song 参数)? 感谢您的回复,但是,如果您阅读我的帖子,您会看到我说:“在我们开始之前,我知道我可以使用方法引用,例如 Song::getTitle 来解决我的问题,但我在这里的查询并不是我想要修复的问题,而是我想要回答的问题,即为什么 Java 编译器会以这种方式处理它。” 我想知道为什么当我使用 lambda 表达式时编译器会这样。它接受比较(s -> s.getArtist()) 但是当我链接 .thenComparing(s -> s.getDuration()) 例如它给我两个调用的语法错误,如果我然后添加一个显式类型比较调用,例如compare((Song s) -> s.getArtist()) 那么这解决了这个问题,对于 List.sort 和 TreeSet 它还解决了所有进一步的编译错误,而无需添加额外的参数类型,但是对于 Collections.sort & Objects.compare 示例最后 thenComparing 仍然失败【参考方案3】:

处理这个编译时错误的另一种方法:

显式地转换你的第一个比较函数的变量,然后就可以开始了。我已经对 org.bson.Documents 对象的列表进行了排序。请看示例代码

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

【讨论】:

【参考方案4】:

playlist1.sort(...) 从 playlist1 的声明中为类型变量 E 创建一个 Song 的边界,它“涟漪”到比较器。

Collections.sort(...) 中,没有这样的界限,从第一个比较器的类型推断,编译器还不足以推断其余部分。

我认为您会从 Collections.&lt;Song&gt;sort(...) 获得“正确”的行为,但没有安装 java 8 来为您测试。

【讨论】:

嗨,是的,您在添加收藏方面是正确的。 确实消除了最后一次 thenComparing 调用的错误

以上是关于Java 8 Comparator 类型推断非常困惑的主要内容,如果未能解决你的问题,请参考以下文章

java图中代码改用Lambda表达式实现Comparator接口?

死磕Lambda表达式:更简洁的Lambda

基于实现 Comparator 或 ToIntFunction、Java 8 的流排序、反转绝对值

Java 8 新特性:Comparator.naturalOrder | 自然排序

JDK将新增局部变量类型推断功能

Java 8 进阶手册(XX):使用 Comparator 对列表进行排序