Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java generic method cannot call another generic method with looser constraint and return its value

Tags:

java

generics

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).

like image 269
Mark Avatar asked Sep 17 '19 12:09

Mark


People also ask

Which of the following statements are true about generic methods in Java?

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.

How do you declare a generic method How do you invoke a generic method?

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.

What is Java generics in what scenarios we can and Cannot use generics?

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.


2 Answers

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)?

like image 200
Jacob G. Avatar answered Sep 30 '22 05:09

Jacob G.


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.

like image 38
meriton Avatar answered Sep 30 '22 05:09

meriton