Very confused by Java 8 Comparator type inference
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:
Use the new Java 8 method reference syntax
Collections.sort(playlist, Comparator.comparing(Song::getTitle) .thenComparing(Song::getDuration) .thenComparing(Song::getArtist) );
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
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.
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 thecomparing()
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>
.