Generics - compiler inconsistency [jdk 1.8.0_162]

Viewed 91

I came across a problem with generics which keeps me puzzled of how the compiler actually deals with generic types. Consider the following:

// simple interface to make it a MCVE
static interface A<F, S> {
    public F getF();    
    public S getS();
}

static <V, S> Comparator<A<V, S>> wrap(Comparator<S> c) {
    return (L, R) -> c.compare(L.getS(), R.getS());
}

The following will not compile because both generic types are reduced to Object when calling thenComparing:

Comparator<A<String, Integer>> c = wrap((L, R) -> Integer.compare(L, R))
    .thenComparing(wrap((L, R) -> Integer.compare(L, R)));

But if I break them up like the following example, everything compiles (and runs) correctly:

Comparator<A<String, Integer>> c = wrap((L, R) -> Integer.compare(L, R));
c = c.thenComparing(wrap((L, R) -> Integer.compare(L, R)));

So the question is: what happens here? I suspect this is due to some weird behavior of the compiler rather than intended language specification? Or am I missing something obvious here?

2 Answers

The second attempt compiles correctly, because you specified the type of a variable yourself, telling the compiler what it is, because compiler does not have enough information to figure it out.

Look at this simplified example, it's from vavr (great by the way). There is a Try<T> class that represents a result of some operation. Generic parameter T is the type of that result. There is a static factory to create a failure immediately, meaning we have no result here, but the generic parameter is still there:

static <T> Try<T> failure(Throwable exception) {
    return new Try.Failure(exception);
}

Where does the T come from here? The usage looks like this:

public Try<WeakHashMap> method() {
  return Try.failure(new IllegalArgumentException("Some message"));
}

The Try<WeakHashMap> here is my choice, not the compilers, you can actually put anything you want in there because you are choosing the type.

The same thing in your example, the Comparator has generic parameter String only, because you specified it and compiler agreed to it (like with Try<WeakHashMap>). When you added a chained call you forced the compiler to infer the type itself, and it was Object, because what another type it could have been?

What else you can do (notice the Testing.<String, Integer>wrap):

public class Testing {
  static interface A<F, S> {
    public F getF();
    public S getS();
  }

  static <V, S> Comparator<A<V, S>> wrap(Comparator<S> c) {
    return (L, R) -> c.compare(L.getS(), R.getS());
  }

  public static void main(String[] args) {
    Comparator<A<String, Integer>> comp = Testing.<String, Integer>wrap((L, R) -> Integer.compare(L, R))
      .thenComparing(wrap((L, R) -> Integer.compare(L, R)));
  }
}

Just to make one central piece of information more visible, "chained call":

In anything of the form T t = m1().m2() everything left of the last dot will have limited type inference with no target type. The target type T can only help to infer m2(), not m1().

This is a deliberate choice in the language design, in order to keep the complexity at bay. In this approach inferring the type of m1() must complete before starting to infer the type for m2() (otherwise, where would you search for method m2?).

Related