Andoni Michael
Andoni Michael

Reputation: 33

Java Reflection Casting Method ReturnType

To give some background, I am creating a small dependency injector and running into problems casting method invocations back into their return types. A minimal example is:

public class MinimalExample {
    public static <T> void invokeMethod(Class<T> aClass) throws ReflectiveOperationException {
        Optional<Method> myOptMethod = resolveMethod(aClass);
        if (myOptMethod.isPresent()) {
            Method myMethod = myOptMethod.get();
            Object myInstance = myMethod.invoke(myMethod);
            doSomething(myMethod.getReturnType(), myMethod.getReturnType().cast(myInstance));
        }
    }

    private static <T> Optional<Method> resolveMethod(Class<T> aClass) {
        return Stream.of(aClass.getMethods())
                .filter(aMethod -> Modifier.isStatic(aMethod.getModifiers()))
                .filter(aMethod -> aMethod.getParameterCount() == 0)
                .findAny();
    }

    private static <U> void doSomething(Class<U> aClass, U anInstance) {
        // E.g. Map aClass to anInstance.
    }
}

The problem here, is that doSomething needs to be called with Class<U>, U, but it is currently being called with Class<capture of ?>, capture of ? due to the invoke method's wildcard return type.

I could change doSomething to doSomething(Class<?> aClass, Object anInstance) but then I lose the type safety and this is not necessarily the only place that method is called.

My question is: Why can the compiler not infer they have the same underlying type, U, given the explicit cast?


Edit (09 Mar 2021):

I went through the liberty of decompiling the byte code in order to see why rzwitserloot's helper method does indeed resolve the type problem. They seem to be identical calls due to type erasure. I guess that the compiler is just not smart enough to infer they are the same capture type after the cast and needs the type-binding to help.

I've added the following functions

private static <U> void doSomethingWithTypeBinding(Class<U> aClass, Object anObject) {
    doSomething(aClass, aClass.cast(anObject));
}

private static void doSomethingUnsafe(Class<?> aClass, Object anInstance) {}

which I call from lines 15 and 16 respectively now

doSomethingWithTypeBinding(myMethod.getReturnType(), myInstance);
doSomethingUnsafe(myMethod.getReturnType(), myMethod.getReturnType().cast(myInstance));

Resulting in the following bytecode:

L5
    LINENUMBER 15 L5
    ALOAD 2
    INVOKEVIRTUAL java/lang/reflect/Method.getReturnType ()Ljava/lang/Class;
    ALOAD 3
    INVOKESTATIC depinjection/handspun/services/MinimalExample.doSomethingWithTypeBinding (Ljava/lang/Class;Ljava/lang/Object;)V
L6
    LINENUMBER 16 L6
    ALOAD 2
    INVOKEVIRTUAL java/lang/reflect/Method.getReturnType ()Ljava/lang/Class;
    ALOAD 2
    INVOKEVIRTUAL java/lang/reflect/Method.getReturnType ()Ljava/lang/Class;
    ALOAD 3
    INVOKEVIRTUAL java/lang/Class.cast (Ljava/lang/Object;)Ljava/lang/Object;
    INVOKESTATIC depinjection/handspun/services/MinimalExample.doSomethingUnsafe (Ljava/lang/Class;Ljava/lang/Object;)V

// access flags 0xA
// signature <U:Ljava/lang/Object;>(Ljava/lang/Class<TU;>;TU;)V
// declaration: void doSomething<U>(java.lang.Class<U>, U)
private static doSomething(Ljava/lang/Class;Ljava/lang/Object;)V
    L0
        LINENUMBER 30 L0
        RETURN
    L1
        LOCALVARIABLE aClass Ljava/lang/Class; L0 L1 0
        // signature Ljava/lang/Class<TU;>;
        // declaration: aClass extends java.lang.Class<U>
        LOCALVARIABLE anInstance Ljava/lang/Object; L0 L1 1
        // signature TU;
        // declaration: anInstance extends U
        MAXSTACK = 0
        MAXLOCALS = 2

// access flags 0xA
// signature <U:Ljava/lang/Object;>(Ljava/lang/Class<TU;>;Ljava/lang/Object;)V
// declaration: void doSomethingWithTypeBinding<U>(java.lang.Class<U>, java.lang.Object)
private static doSomethingWithTypeBinding(Ljava/lang/Class;Ljava/lang/Object;)V
    L0
        LINENUMBER 33 L0
        ALOAD 0
        ALOAD 0
        ALOAD 1
        INVOKEVIRTUAL java/lang/Class.cast (Ljava/lang/Object;)Ljava/lang/Object;
        INVOKESTATIC depinjection/handspun/services/MinimalExample.doSomething (Ljava/lang/Class;Ljava/lang/Object;)V
    L1
        LINENUMBER 34 L1
        RETURN
    L2
        LOCALVARIABLE aClass Ljava/lang/Class; L0 L2 0
        // signature Ljava/lang/Class<TU;>;
        // declaration: aClass extends java.lang.Class<U>
        LOCALVARIABLE anObject Ljava/lang/Object; L0 L2 1
        MAXSTACK = 3
        MAXLOCALS = 2

// access flags 0xA
// signature (Ljava/lang/Class<*>;Ljava/lang/Object;)V
// declaration: void doSomethingUnsafe(java.lang.Class<?>, java.lang.Object)
private static doSomethingUnsafe(Ljava/lang/Class;Ljava/lang/Object;)V
    L0
        LINENUMBER 37 L0
        RETURN
    L1
        LOCALVARIABLE aClass Ljava/lang/Class; L0 L1 0
        // signature Ljava/lang/Class<*>;
        // declaration: aClass extends java.lang.Class<?>
        LOCALVARIABLE anInstance Ljava/lang/Object; L0 L1 1
        MAXSTACK = 0
        MAXLOCALS = 2

We can see the INVOKEVIRTUAL cast right into the INVOKESTATIC look identical due to their runtime type erasure.


Edit (12 Mar 2021):

@Holger pointed out in the comments, Method#getReturnType returns a Class<?>. Because it is wildcarded, the method, from the compiler's point-of-view, has no guarantee of subsequent method calls returning a Class with the same capture type.

Upvotes: 3

Views: 768

Answers (2)

rzwitserloot
rzwitserloot

Reputation: 102832

Type variables are a figment of the compiler's imagination: They don't survive compilation (erasure*). It is best to think about them as linking things. A type variable that is used in only one place, ever, is completely useless; once they show up in two places, now that's useful: It lets you link multiple usages of types together, to say that occurences are the same. For example, you can tie the parameter type of .add(Obj thingToAdd), and the return type of .get(int idx) together for java.util.List.

Here, you want to link the Class<X> of myMethod.getReturnType together with the myInstance variable. As you realized, this is impossible, as the compiler doesn't know that they're going to end up being the same type. However, by invoking the cast() method of Class<X>, we work around that part.

But you still need some type variable to serve as vehicle to tie things together, and you don't have one. ? is analogous to a one-use-and-done type variable; Class<?> cls and myMethod.getReturnType().cast(myInstance)` are 'different' ?s: Yes, your eyeballs can tell it's going to be the same type, but java can't. You need a type variable. You can introduce one, of course:

private static <X> helper(Class<X> x, Object myInstance) {
    doSomething(x, x.cast(myInstance));
}

Add this method to your code and invoke this one, instead of invoking doSomething. The <X> that we created here serves to tie the results together.

*) They remain in public signatures, of course, but everywhere else, at runtime - they are erased.

An alternative option is available to you here: The doSomething method is private, so you are in full control of it. Therefore, you can either just move the cast inside it, which solves all problems, or, you write it like so:

/** precondition: o must be an instance of c */
private static void doSomething(Class<?> c, Object o) {
}

As it is a private method, preconditions are fine to introduce. You are in complete control of all code that invokes this method. If you really want to, you can add a runtime check (if (!c.isInstanceof(o)) throw new IllegalArgumentException("o not instance of c"); at the top), but whether it is worth doing is an open debate in java ecosystems for private methods. Usually the verdict is not to do that, or to use an assert keyword for it.

NB: This has some atrocious null/optional handling. In case the method to be resolved isn't found you.. just silently do nothing? This is why NPEs are better: At least then careless coding results in an exception instead of a wild goose chase.

Upvotes: 2

Eugene
Eugene

Reputation: 120848

First of all the invocation is :

Object myInstance = myMethod.invoke(null);

myMethod is static (as you already found in resolveMethod), so you need to pass a null, otherwise you would need an instance for that, which you do not have.

And then fixing your example, is fairly trivial:

Method myMethod = myOptMethod.get();
Object myInstance = myMethod.invoke(null);

Class<?> cls = myMethod.getReturnType();
Object obj = myMethod.getReturnType().cast(myInstance);

doSomething(cls, obj);

where that method changes the definition to:

 private static <U> void doSomething(Class<? extends U> aClass, U anInstance) {....}

Upvotes: 0

Related Questions