8

I would like to do this:

<T extends java.util.Date> T a(@Nonnull T... dates) {
    return b(dates);  // compile error
}

<T extends Comparable<T>> T b(T... comparables) {
   return comparables[0];
}

But it fails to compile unless I insert a cast within a:

<T extends java.util.Date> T a(@Nonnull T... dates) {
    return (T) b(dates);  // warning about unsafe cast in IntelliJ
}

<T extends Comparable<T>> T b(T... comparables) {
   return comparables[0];
}

Interestingly, if I remove the generic from a it works:

java.util.Date a(java.util.Date... dates) {
    return b(dates);
}

<T extends Comparable<T>> T b(T... comparables) {
   return comparables[0];
}

And if I port the original code to Kotlin, it also works (this makes me think it's a limitation of Java, and not something that is fundamentally unknowable):

fun <T: java.util.Date> a(dates: Array<T>): T {
    return b(dates);
}

fun <T: Comparable<T>> b(comparables: Array<T>): T {
    return comparables[0];
}

My question is: what is special about Java's type system that prevents this from compiling? It feels to me like the Java compiler could just insert the cast behind the scenes (it is my understanding that that is how generics are implemented in other cases).

Mark
  • 18,730
  • 7
  • 107
  • 130
  • Since both are compiled to bytecode then yes, the Java compiler could do exactly what the Kotlin compiler is doing. But there are things that Java compiler won't do because of the JLS and other things that specify what is allowed and what not. Removing the generic type from `a` is not very interesting, because switching to raw types allows you to do all sorts of things that cause confusion (and since you're not supposed to use raw types, it's not very useful to go through those oddities). – Kayaman Sep 17 '19 at 13:11
  • @Kayaman, `java.util.Date` is not a raw type, because it is not a generic type at all (nitpicking). – jaco0646 Sep 17 '19 at 13:28
  • @jaco0646 well that's true, and it does change the scenario. Everyone ignore the latter part of my comment. – Kayaman Sep 17 '19 at 13:32

2 Answers2

6

A cast isn't required to get this to compile. Instead, you can use a bounded wildcard when specifying that T must extend Comparable<T>:

<T extends java.util.Date> T a(T... dates) {
    return b(dates);  // compiles fine
}

<T extends Comparable<? super T>> T b(T... comparables) {
    return comparables[0];
}

Note the Comparable<? super T> instead of Comparable<T>.

As Johannes Kuhn pointed out in his comment, a subclass of Date will implicitly implement Comparable<Date> instead of Comparable<DateSubclass>, hence the need for Comparable<? super T>.

For more information, see: What is PECS (Producer Extends Consumer Super)?

Jacob G.
  • 28,856
  • 5
  • 62
  • 116
5

The problem is that the first method could be called with the following class:

class MyDate extends Date {}

Then, T in the first method is inferred as MyDate, but T in the second method can not be MyDate, because MyDate does not extend Comparable<MyDate> - it only extends Comparable<Date> ...

The root cause of the compilation error therefore is that Java generics are invariant. And that's why Kotlin, whose generics support declaration site variance, accepts the code without issue.

To fix this in Java, you can use a wildcard type, as shown in Jacoc G.'s answer.

meriton
  • 68,356
  • 14
  • 108
  • 175