3

I'd like to resolve a dubious point that has been affecting some parts of my code for years — it is fixed with a trivial cast, but now I'd like to understand the rationale behind it and possibly a reference in official specifications.

Let me introduce this piece of code:

import java.util.function.Supplier;

class BaseClass<T, R> {
  R getSomething() { return null; }
  static <U> U method1 (final U arg1) { return arg1; }
  static <U> U method2 (final U arg1, final Class<? extends U> arg2) { return arg1; }
  static <U> U method3 (final U arg1, final Supplier<? extends U> arg2) { return arg1; }
}

Now let me have a subclass with a partial generics binding (T is still unbound):

class DerivedClass<T> extends BaseClass<T, String> {
  private String s;

  void test (final DerivedClass<T> arg) {
    final var m1 = method1(arg);
    s = m1.getSomething();

    final DerivedClass<T> m3 = method1(arg);
    s = m3.getSomething();

    final var m2 = method2(arg, DerivedClass.class);
    // 1. ERROR: requires String, found Object
    s = m2.getSomething();

    // 2. WARNING: unchecked assignment DerivedClass to DerivedClass<T>
    final DerivedClass<T> m4 = method2(arg, DerivedClass.class);
    s = m4.getSomething();

    final var m5 = method3(arg, () -> new DerivedClass<>());
    s = m5.getSomething();
    } 
}

With method1 everything is fine and the returned object carries the binding R -> String. In other words, the U type of arg1 is correctly propagated to the result.

With method2 the binding is lost (and "downgraded" to Object): this ends up in a warning if I force things with an explicit type declaration of m4 (of course an explicit cast would work too) and an error if I use var.

With method3, in spite of arg2 being declared in a similar way as in method2, everything is fine again.

So it seems that the culprit is the presence of an argument of Class<> type. Why? In general, why doesn't the compiler use arg1 for a complete match even when there is a Class<> parameter?

As far as I can remember this happens since Java 5 (of course the portions with var refer to Java 17).

pppery
  • 3,731
  • 22
  • 33
  • 46
Fabrizio Giudici
  • 532
  • 3
  • 15

2 Answers2

4

Because both BaseClass and DerivedClass are generic classes, but DerivedClass.class is a raw type (namely Class<DerivedClass> and not Class<DerivedClass<T>>).

var is inferred to be the raw type and your statement is identical to:

final DerivedClass m2 = method2(arg, DerivedClass.class);

Once you use raw types, all generic information is "lost"/erased and you have to deal with plain Objects.

this.<DerivedClass<T>>method2(arg, DerivedClass.class) should make m2 generic again (but you probably still have the unchecked cast warning, just for the argument).

And it's exactly this "unchecked assignment" that's causing the issues (unchecked from the raw type to the generic closed type).

knittl
  • 246,190
  • 53
  • 318
  • 364
2

The case of method3 is not similar to the case of method2 at all. Namely, there are no raw types involved when calling method3. DerivedClass in DerivedClass.class is a raw type.

It would be similar if you return a raw DerivedClass in the supplier:

final var m5 = method3(arg, () -> new DerivedClass());

// this doesn't compile, for the same reason as in the method2 case
s = m5.getSomething();

The type argument for method2 cannot be inferred as DerivedClass<T>, because then the second parameter (arg2) would be of type Class<? extends DerivedClass<T>>, but you are passing a Class<DerivedClass> to it.

It's like assigning a Class<DerivedClass> to Class<? extends DerivedClass<T>> variable:

// this does not compile either
Class<? extends DerivedClass<T>> x = DerivedClass.class;

This doesn't compile because the raw DerivedClass is not a subtype of DerivedClass<T>. In fact, if you look at section 4.10.2 of the JLS, DerivedClass is a direct supertype of DerivedClass<T>.

Given a generic class or interface C with type parameters F1,...,Fn (n > 0), the direct supertypes of the parameterized type C<T1,...,Tn>, where each of Ti (1 ≤ i ≤ n) is a type, are all of the following:

  • [...]

  • The raw type C.

Therefore, the type parameter of method2 is inferred as the raw DerivedClass. This works for arg1 too, as the generic DerivedClass<T> is a subtype of the raw DerivedClass.

That said, it is not possible to get a Class<DerivedClass<T>> unless you do unchecked casts.

Now you might be wondering how this assignment works if method2 returns a raw DerivedClass, but you are assigning it to DerivedClass<T>:

final DerivedClass<T> m4 = BaseClass.method2(arg, DerivedClass.class);

This works because in assignment contexts just are specified to work like this. The important sentence is:

If, after the conversions listed above have been applied, the resulting type is a raw type (§4.8), an unchecked conversion (§5.1.9) may then be applied.

There is an implicit unchecked conversion here converting DerivedClass to DerivedClass<T>. This is also why you get the warning.

Sweeper
  • 213,210
  • 22
  • 193
  • 313