非常困惑的 Java8比较器类型推断

我一直在研究 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 编译器编译时,它的编译是 OK 的。

Sort errors in Eclipse

36134 次浏览

The problem is type inferencing. Without adding a (Song s) to the first comparison, comparator.comparing doesn't know the type of the input so it defaults to Object.

You can fix this problem 1 of 3 ways:

  1. Use the new Java 8 method reference syntax

     Collections.sort(playlist,
    Comparator.comparing(Song::getTitle)
    .thenComparing(Song::getDuration)
    .thenComparing(Song::getArtist)
    );
    
  2. Pull out each comparison step into a local reference

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

    EDIT

  3. Forcing the type returned by the Comparator (note you need both the input type and the comparison key type)

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

I think the "last" thenComparing syntax error is misleading you. It's actually a type problem with the whole chain, it's just the compiler only marking the end of the chain as a syntax error because that's when the final return type doesn't match I guess.

I'm not sure why List is doing a better inferencing job than Collection since it should do the same capture type but apparently not.

playlist1.sort(...) creates a bound of Song for the type variable E, from the declaration of playlist1, which "ripples" to the comparator.

In Collections.sort(...), there is no such bound, and the inference from the type of the first comparator is not enough for the compiler to infer the rest.

I think you would get "correct" behavior from Collections.<Song>sort(...), but don't have a java 8 install to test it out for you.

First, all the examples you say cause errors compile fine with the reference implementation (javac from JDK 8.) They also work fine in IntelliJ, so its quite possible the errors you're seeing are Eclipse-specific.

Your underlying question seems to be: "why does it stop working when I start chaining." The reason is, while lambda expressions and generic method invocations are poly expressions (their type is context-sensitive) when they appear as method parameters, when they appear instead as method receiver expressions, they are not.

When you say

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

there is enough type information to solve for both the type argument of comparing() and the argument type p1. The comparing() call gets its target type from the signature of Collections.sort, so it is known comparing() must return a Comparator<Song>, and therefore p1 must be Song.

But when you start chaining:

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

now we've got a problem. We know that the compound expression comparing(...).thenComparing(...) has a target type of Comparator<Song>, but because the receiver expression for the chain, comparing(p -> p.getTitle()), is a generic method call, and we can't infer its type parameters from its other arguments, we're kind of out of luck. Since we don't know the type of this expression, we don't know that it has a thenComparing method, etc.

There are several ways to fix this, all of which involve injecting more type information so that the initial object in the chain can be properly typed. Here they are, in rough order of decreasing desirability and increasing intrusiveness:

  • Use an exact method reference (one with no overloads), like Song::getTitle. This then gives enough type information to infer the type variables for the comparing() call, and therefore give it a type, and therefore continue down the chain.
  • Use an explicit lambda (as you did in your example).
  • Provide a type witness for the comparing() call: Comparator.<Song, String>comparing(...).
  • Provide an explicit target type with a cast, by casting the receiver expression to Comparator<Song>.

Another way to deal with this compile time error:

Cast your first comparing function's variable explicitly and then good to go. I have sort the list of org.bson.Documents object. Please look at sample 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());