In a Java project I'm currently working on, I'm dynamically loading classes then using the reflection API to find and execute methods of those classes that have certain annotations.
The code that performs the actual execution works exclusively in terms of Java-8 functional interfaces (for compatibility reasons), so I need to have an intermediate stage where the Method
instances discovered using reflection are converted to appropriate functional interfaces. I achieve this using the MethodHandleProxies
class.
For compatibility reasons again, the functional interfaces in question are generic interfaces. This causes an "unchecked conversion" warning when using the MethodHandleProxies.asInterfaceInstance
method, since that method returns the "bare" interface.
The following is a brief example that reproduces the main steps involved:
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleProxies;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.Arrays;
public class TestClass {
private String prefix;
public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, SecurityException {
// Use reflection to find method.
Method method = Arrays.stream(TestClass.class.getDeclaredMethods()) // Stream over methods of ConsumerClass
.filter(m -> m.isAnnotationPresent(Marker.class)) // Retain only methods with @Marker annotation
.findFirst().get(); // Get first such method (there is only one in this case)
// Convert method to "MethodInterface" functional interface.
MethodHandle handle = MethodHandles.lookup().unreflect(method);
MethodInterface<TestClass, String> iface = MethodHandleProxies.asInterfaceInstance(MethodInterface.class, handle);
// Call "testMethod" via functional interface.
iface.call(new TestClass("A"), "B");
}
public TestClass(String prefix) {
this.prefix = prefix;
}
@Marker
public void testMethod(String arg) {
System.out.println(prefix + " " + arg);
}
@Retention(RUNTIME)
public @interface Marker { }
@FunctionalInterface
public interface MethodInterface<I,V> {
void call(I instance, V value);
}
}
This code compiles and runs, but has an unchecked conversion warning on the assignment to iface
.
Making MethodInterface
non-generic would solve this particular problem, but would mean that it would no longer work with method references for arbitrary types (which is desirable for other parts of the code).
For example, with the above definitions of TestClass
and MethodInterface
, the following line compiles:
MethodInterface<TestClass,String> iface = TestClass::testMethod;
However, changing to the following definition of MethodInterface
breaks this:
@FunctionalInterface
public interface MethodInterface {
void call(Object inst, Object value);
}
Assigning TestClass::testMethod
to an instance of this interface doesn't compile, as the parameters are of the wrong types.
As I see it, I have three options:
@SuppressWarnings
annotation to the assignment.I try to ensure there are no warnings generated by my code (to minimise the opportunities for bugs), so I'm not keen on option 1. Option 2 feels like it's simply "papering over the cracks", but is acceptable if absolutely necessary. So my preferred option is to come up with a different approach.
Is there a different approach that is inherently type-safe?
Assigning a reflectively generated instance to a parameterized generic interface is an unchecked operation, as there is no way to ensure that the generated class fulfills that parameterized interface. In fact, the implementation behind MethodHandleProxies
doesn’t care about this signature at all. So having a warning is correct, suppressing it when you are confident that you did everything right, limiting the suppression to the smallest scope possible, is the best (or unavoidable) solution.
You could create a reifiable sub-interface, e.g. interface Specific extends MethodInterface<TestClass,String> {}
, use this for the code generation, to have no unchecked operation from the compiler’s point of view, but it wouldn’t change the fact, that the proxy doesn’t really care about the correctness at all.
By the way, if your target interface is a functional interface, you can use LambdaMetafactory
instead of MethodHandleProxies
. The code generation is slightly more complex, but the resulting class potentially more efficient (even practically in today’s JREs) than the more general proxy.
// Use reflection to find method.
Method method = Arrays.stream(TestClass.class.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Marker.class))
.findFirst().get();
// Convert method to "MethodInterface" functional interface.
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.unreflect(method);
MethodInterface<TestClass, String> iface;
try {
iface = (MethodInterface<TestClass, String>)LambdaMetafactory.metafactory(lookup,
"call", MethodType.methodType(MethodInterface.class),
MethodType.methodType(void.class, Object.class, Object.class),
handle, handle.type())
.getTarget().invoke();
} catch(RuntimeException|Error|ReflectiveOperationException|LambdaConversionException ex) {
throw ex;
}
catch (Throwable ex) {
throw new AssertionError(ex);
}
// Call "testMethod" via functional interface.
iface.call(new TestClass("A"), "B");
It’s just a coincidence that this code doesn’t generate an unchecked warning. It actually bears an unchecked operation, but like the MethodHandleProxies
variant, it also has so many other things you can do wrong without the compiler telling you, that it actually doesn’t matter.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With