How to safely cast generic wildcard "?" to known type parameter in Java?

Viewed 342

How to safely cast Class<?> (returned by Class.forName()) to Class<Annotation> without issuing "Unchecked cast" warning?

    private static Class<? extends Annotation> getAnnotation() throws ClassNotFoundException {
        final Class<?> loadedClass = Class.forName("java.lang.annotation.Retention");
        if (!Annotation.class.isAssignableFrom(loadedClass)) {
            throw new IllegalStateException("@Retention is expected to be an annotation.");
        }
        @SuppressWarnings("unchecked")
        final Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) loadedClass;
        return annotationClass;
    }
2 Answers

Multiple misconceptions need to be explained before delving into the answer.

You're using the wrong variance

final Class<Annotation> annotationClass = (Class<Annotation>) loadedClass;

This is actually illegal in any case. Try it:

Class<Number> n = Integer.class;

That won't compile.

Generics are invariant. It means that within the <>, you can't use a supertype as a standin for a subtype or vice versa.

Normal java (when <> are not involved) is covariant. Any subtype is a stand-in for one of its supertypes. This:

Number n = Integer.valueOf(5);

is perfectly legal java. But in generics world it isn't. If you want it to be, then, you have to opt into it: X extends Y is how you opt into covariance, and X super Y is how you opt into contravariance (contravariance is as if Integer i = new Number(); was legal - a SUPERtype can stand in for a subtype).

This is all because that's just how the universe ends up working out. If generics were naturally covariant, this would compile:

List<Integer> listOfInts = new ArrayList<>();
List<Number> listOfNums = listOfInts;
listOfNums.add(Double.valueOf(1.0));
int i = listOfInts.get(0);

but, follow along with your own eyes and you realize that code is a walking type violation. It shoves a non-integer into a list of integers. That's why opting into covariance or contravariances closes doors. If you opt into covariance, the add method is disabled *1:

List<? extends Number> list = new ArrayList<Integer>(); //legal
list.add(Integer.valueOf(5)); // will not compile

similarly, if you opt into contravariance, add works great, but get is disabled. 'disabled' in the type system sense: You can call it. But the expression list.get(i) would be of type Object:

List<? super Integer> list = new ArrayList<Number>(); // legal
list.add(Integer.valueOf(5)); // legal
Integer i = list.get(0); // won't compile
Object o = list.get(0); // this will.

With classes, where 'write' is not exactly clear, it's harder to see why Class<Annotation> c = SomeSpecificAnno.class; should fail to compile, but it does, so, that's important realization one.

Why are you using reflection here?

You can make class literals in java. This works great:

Class<? extends Number> c = Integer.class;

That's real java: You can stick .class at the end of any type and that will be an expression of type java.lang.Class, in fact, it's of type Class<TheExactThing>. So:

private static Class<? extends Annotation> getAnnotationType() {
    return Retention.class;
}

works and compiles fantastically. I had to update the return type because as I explained above, returning the instance of j.l.Class that represents the Retention annotation for a method that is specced to return Class<Annotation> is as broken as returning an integer from a method that is specced to return a string.

The answer

If your code example is using java.lang.annotation.Retention as a stand-in, but your actual string here is a dynamic value that you do not know at compile time, the return Retention.class; option is off the table, then:

private static Class<? extends Annotation> getAnnotationType(String fqn) throws ClassNotFoundException {
    return Class.forName(fqn).asSubclass(Annotation.class);
}

Again, do not use reflection unless there is no other way, and if you have the class in a string constant, generally you do not need reflection.

*1 ) You can call add, but only with a null literal; list.add(null); compiles, because null is trivially a valid value for any type. However, that's not particularly useful, of course.

Due to type erasure, generic type information is not accessible anymore at runtime. This means: Class<?> and Class<? extends Annotation> are indistinguishable at runtime - so you cannot do a runtime check to make the unchecked cast a checked one.

This means: you have to live with the warning (mind: a warning is not an error, it just means "this is problemattic, make sure you know what you're doing!).

Related