vendredi 12 juin 2020

Is there a way to tell if the runtime type was erased

This is going to be a bit complicated to explain, but I will try.

Suppose you have a generic class:

static class Box<T extends Number> {

    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

}

And a method that allows to call the getValue in a reflective way:

 // it's just an example, the real world scenario is slightly more involved

 private static final Lookup LOOKUP = MethodHandles.lookup();     

 public static <T, R> T result(String methodName, Class<T> propertyClass, R instance) {
    try {

        /* line1 */ 
        MethodHandle handle = LOOKUP.findVirtual(
              instance.getClass(), 
              methodName, 
              MethodType.methodType(propertyClass)
        );

        /* line2 */
        handle = handle.asType(handle.type()
                       .changeReturnType(Object.class)
                       .changeParameterType(0, Object.class));

        /* line3 */
        Object obj = handle.invokeExact(instance);
        return propertyClass.cast(obj);

    } catch (Throwable t) {
        throw new RuntimeException(t);
    }
}

What this does is

  • create a MethodHandle to the getValue method

  • adapt that MethodHandle, so that I could call invokeExact on it (otherwise I would need to call invoke, which is slower). But this step is entirely optional.

  • once I build the MethodHandle, invoke it.

And now let's try to call this:

public static void main(String[] args) throws Throwable {
    Box<Long> box = new Box<>();
    box.setValue(42L);
    result("getValue", Long.class, box);
}

This should work, right? Well, no. This will fail with:

 Caused by: java.lang.NoSuchMethodException: no such method: GenericTest$Box.getValue()Long/invokeVirtual

I understand why, because the erased type of T extends Number is Number, so the invocation is really supposed to be:

result("getValue", Number.class, box); // not Long.class

This is obvious to me, but not to the callers of the library at my work-place and I can't blame them. Please note that this is a simplified example...


When they build Box<Long> box = new Box<>(); with a Long type, it is sort of natural to provide Long.class further, instead of Number.class. The solution is obviously trivial, but, I was thinking that if I could (at runtime) "see" that the return type of getValue was a generic type, I could throw a proper error message. For example:

"you provided Long.class, but the generic type was erased to ..."

In other words if I could tell at runtime that the return type is Number.class from getValue and that it is the result of some erasure, I could be a bit smarter in the later decisions.

Is that possible?





Aucun commentaire:

Enregistrer un commentaire