Issue
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 thegetValue
methodadapt that
MethodHandle
, so that I could callinvokeExact
on it (otherwise I would need to callinvoke
, 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?
Solution
Well, maybe you could use good old reflection. Using reflection allows you to look up a method by name and parameter types but not return type. You can then inspect the return type to see if the caller provided the right one:
Method method = instance.getClass().getMethod(methodName);
Class<?> rtype = method.getReturnType();
if (rtype != propertyClass) {
throw new IllegalArgumentException("must use " + rtype + " instead of " + propertyClass);
}
MethodHandle handle = LOOKUP.unreflect(method);
You might need to adjust how you do the reflective lookup (getMethod or getDeclaredMethod) based on your needs. You might also need to check to ensure the matched method isn't abstract or static. (One would think it cannot be abstract because you're provided with an instance, but there might be an edge case I haven't thought of, such as separate compilation.) And you might also need to check that the method is declared on the same class you're reflecting on. Since you're concerned about performance, doing reflection might be too slow. But if all you're concerned about is diagnostics, you could try the happy path, and if you get a NSME, do a reflective lookup to get the right return type.
Answered By - Stuart Marks
Answer Checked By - Candace Johnson (JavaFixing Volunteer)