Search code examples
javagenericstype-inference

Why does Comparator.thenComparing() work when storing the comparators in variables but not when inlineing them?


I have two comparators which I chain using thenCompare():

Comparator<Edge> byAX = Comparator.comparing(e -> e.a.x());
Comparator<Edge> byBX = Comparator.comparing(e -> e.b.x());
Comparator<Edge> myComparator = byAX.thenComparing(byBX);

This works perfectly fine.

However, if I inline this code by replacing byAX and byBX with their corresponding definitions, the code no longer compiles:

Comparator<Edge> myComparator = Comparator.comparing(e -> e.a.x()).thenComparing(Comparator.comparing(e -> e.b.x()));

In my understanding I could omit the second Comparator.comaring() and the code should be equivalent but this does not work either:

Comparator<Edge> myComparator = Comparator.comparing(e -> e.a.x()).thenComparing(e -> e.b.x());

I get these compiler errors:

java: cannot find symbol
  symbol:   variable a
  location: variable e of type java.lang.Object

java: cannot find symbol
  symbol:   variable b
  location: variable e of type java.lang.Object

java: incompatible types: no instance(s) of type variable(s) U exist so that java.util.Comparator<java.lang.Object> conforms to java.util.Comparator<CompGeom.Edge>

From the error message I gather that this has something to do with generics and how type inference works but I don't understand what exactly is going on here. Could somebody please explain?


Solution

  • Java lambdas ((arg1, arg2) -> code;) are somewhat weirdly type inferred. Inference goes inside out, then outside in, then inside out again.

    The reason is that java's functional typing system is based off of so-called functional interfaces. You don't, in java, just have an expression of type (int, int) -> String.

    That explains why in java you can write listOfStrings.sort(Comparator.comparing(x -> x.length()));. Because, think about that line for a moment: How in the blazes is java allowing you to write x.length() there? j.l.Object doesn't have length(). Why does x? We never mentioned what type x is.

    That's because java resolves inside-out first: Okay, we have a lambda. It will represent some type but we don't yet know what. For now we know: It's a lambda, and, it takes 1 argument.

    So then javac scans Comparator.comparing and finds a few overloads. It can immediately eliminate some. Eventually only one remains: It takes a Function<T, U>, and returns a Comparator<T>. Okay, so I guess for now we go with T t -> t.length(). That isn't much help. Yet. We keep going: That is passed to .sort, which requires a Comparator<E>. That just kicks the can down the road; replacing one typevar with another, not useful. Let's keep going: That sort method is called on a on a List<String>. aha! Only a Comparator<String> is valid there, so I guess E is string! So let's apply that all the way back down - we go with String t -> t.length(). And that, finally, 'fits'. So java compiles this whole thing interpreting x -> x.length() as 'a Function<String, Comparable-to-self>. And then re-scans that lambda to ensure it makes sense in that context. Only now could e.g. a typo in length() be found as 'hey, that's not a method strings have'.

    The problem with your code snippet that doesn't compile is that the compiler is limited in how far it can take this inside-out-outside-in process. It has to go too far and gives up. (It's a lot more complicated than this, but, you need to fully grok large swathes of the JLS to go any further).

    Add some casts to 'help' the compiler and it works again:

    Comparator<Edge> myComparator = Comparator.comparing(e -> e.a.x()).thenComparing(Comparator.<Edge>comparing(e -> e.b.x()));
    

    Or add a (Comparator<Edge>) cast. The reason your other snippet does work is the local variable types serve as that 'type hint'.