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).
Explanation: Generic methods are methods that introduce their own type parameters. This is similar to declaring a generic type, but the type parameter scope is limited to the method where it is declared. Static and non-static generic methods are allowed, as well as generic class constructors.
Generic MethodsAll generic method declarations have a type parameter section delimited by angle brackets (< and >) that precedes the method's return type ( < E > in the next example). Each type parameter section contains one or more type parameters separated by commas.
Generics means parameterized types. The idea is to allow type (Integer, String, … etc., and user-defined types) to be a parameter to methods, classes, and interfaces. Using Generics, it is possible to create classes that work with different data types.
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)?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With