Java 8 Comparator类型推论非常困惑


84

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

这些是我的发现。假设我们有一个ArrayListtype 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") );

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

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

现在到了混淆部分。对于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())
            );

与中发生的情况相同,对于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())
            );

例如,Song从用于比较方法调用的第一个lambda参数中删除类型,它显示了在进行比较的调用和对的第一个调用(thenComparing但对最终调用没有)下的语法错误thenComparing-几乎与上述情况相反!然而,所有其他3个例子,即有Objects.compareList.sort而且Collections.sort当我删除第一个Song参数类型它显示语法错误的所有电话。

提前谢谢了。

经过编辑,其中包括我在Eclipse Kepler SR2中收到的错误的屏幕截图,此后我现在发现它们是针对Eclipse的,因为在命令行上使用JDK8 java编译器进行编译时,它可以编译。

Eclipse中的排序错误


如果您在问题中包括所有测试中得到的所有编译错误消息,将很有帮助。
伊兰2014年

1
老实说,我认为通过自己运行源代码,最容易让人看到问题所在。
宁静

在示例中,t1和的类型是什么?我试图推断它们,但是将类型推断放在编译器的类型推断上很难。:-)t2Objects.compare
Stuart Marks

1
您还在使用什么编译器?
Stuart Marks 2014年

1
这里有两个独立的问题。一位回答者指出,您可以使用方法引用,您可以将其引用。正如lambda既有“显式类型”又有“隐式类型”风格一样,方法引用也有“精确”(一个重载)和“不精确”(多个重载)两种风格。如果不存在,则可以使用精确方法ref或显式lambda来提供其他类型的信息。(也可以使用明确类型的见证人和演员,但通常使用更大的锤子。)
Brian Goetz 2014年

Answers:


105

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

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

当你说

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

有足够的类型信息可以解决的类型参数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()));

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

有几种方法可以解决此问题,所有方法都涉及注入更多类型信息,以便可以正确键入链中的初始对象。在这里,它们按照减少期望和增加干扰的大致顺序排列:

  • 使用精确的方法引用(一个没有重载的方法引用),例如Song::getTitle。然后,这会提供足够的类型信息以推断出该comparing()调用的类型变量,从而为其提供类型,并因此继续进行下去。
  • 使用显式的lambda(如您在示例中所做的那样)。
  • 提供comparing()呼叫的见证人:Comparator.<Song, String>comparing(...)
  • 通过将接收者表达式强制转换为,为显式目标类型提供强制类型转换Comparator<Song>

13
+1用于实际回答OP“编译器为什么不能推断出这一点”,而不仅仅是给出解决方法/解决方案。
乔佛里2014年

感谢您的回答,Brian。但是,我仍然找不到答案,为什么List.sort的行为与Collections.sort不同,因为前者只需要第一个lambda包含参数类型,而后者也需要最后一个lambda即可,例如,如果我有一个比较接下来是5个thenComparing调用,我必须将(Song p1)放在比较中,在最后一个thenComparing中。同样在我的原始帖子中,您将看到TreeSet的底部示例,其中删除了所有参数类型,但是最后一次对thenComparing的调用是可以的,但其他调用则不行-因此,其行为有所不同。
宁静2014年

3
@ user3780370您还在使用Eclipse编译器吗?如果我正确理解了您的问题,则没有看到这种现象。您可以(a)使用JDK 8中的javac尝试,并且(b)如果仍然失败,请发布代码?
Brian Goetz 2014年

@BrianGoetz感谢您的建议。我刚刚使用javac在命令窗口中对其进行了编译,并且按照您所说的进行编译。这似乎是Eclipse问题。我尚未更新到Eclipse Luna,它是为JDK8专门构建的,因此希望可以在其中进行修复。我实际上有一个截图,向您显示Eclipse中发生了什么,但不知道如何在此处发布。
宁静2014年

2
我想你的意思是Comparator.<Song, String>comparing(...)
shmosel

23

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

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

  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语法错误会误导您。这实际上是整个链的类型问题,只是编译器仅将链的末端标记为语法错误,因为那是我猜想最终返回类型不匹配的时候。

我不确定为什么List要比Collection在相同的捕获类型上做得更好,但是显然不能。


为什么它对解决方案ArrayList却不知道Collections(对于链中的第一个调用有一个Song参数)呢?
Sotirios Delimanolis 2014年

4
谢谢您的答复,但是,如果您阅读我的文章,您会看到我说:“在开始之前,我知道我可以使用方法引用,例如Song :: getTitle来解决我的问题,但是这里的查询不是很多我想修复但又想得到答案的东西,即Java编译器为什么以这种方式处理它。”
宁静2014年

我想要一个答案,为什么当我使用lambda表达式时编译器会以这种方式运行。它接受compare(s-> s.getArtist()),但是当我链接.thenComparing(s-> s.getDuration())时,例如,如果我随后在其中添加显式类型,它将给我两个调用语法错误比较调用,例如compare((Song s)-> s.getArtist())然后解决了该问题,对于List.sort和TreeSet,它还解决了所有进一步的编译错误,而不必添加其他参数类型。最后的Collections.sort&Objects.compare示例比较仍然失败
宁静

1

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

显式地转换第一个比较函数的变量,然后再进行转换。我对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());

0

playlist1.sort(...) 从播放列表1的声明为类型变量E创建Song的界限,该声明“涟漪”到比较器。

在中Collections.sort(...),没有这样的界限,并且从第一个比较器的类型进行的推断不足以使编译器推断出其余的。

我认为您会从中获得“正确”的行为Collections.<Song>sort(...),但是没有安装Java 8可以为您进行测试。


嗨,是的,您的正确之处在于添加了Collections。<Song>确实摆脱了上一个thenComparing调用的错误
宁静
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.