How do you check using reflection whether a given object would be a valid parameter of a method (where the parameter and the object are generic types)?
To get a bit of background, this is what I'm trying to achieve:
While playing with reflective method calls I thought that it would be nice to call all methods that have a parameter of a specific type. This works well for raw types as you can call isAssignableFrom(Class<?> c)
on their class objects. However, when you start throwing in generics into the mix it suddenly isn't that easy because generics weren't part of reflection's original design and because of type erasure.
The problem is larger but it basically boils down to the following:
Ideal solution
Ideally the code
import java.lang.reflect.*;
import java.util.*;
public class ReflectionAbuse {
public static void callMeMaybe(List<Integer> number) {
System.out.println("You called me!");
}
public static void callMeAgain(List<? extends Number> number) {
System.out.println("You called me again!");
}
public static void callMeNot(List<Double> number) {
System.out.println("What's wrong with you?");
}
public static <T> void reflectiveCall(List<T> number){
for(Method method : ReflectionAbuse.class.getDeclaredMethods()) {
if(method.getName().startsWith("call")) {
if(canBeParameterOf(method, number)) {
try {
method.invoke(null, number);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public static <T> boolean canBeParameterOf(Method method, List<T> number) {
// FIXME some checks missing
return true;
}
public static void main(String[] args) {
reflectiveCall(new ArrayList<Integer>());
}
}
would print
You called me!
You called me again!
This should be possible regardless of how T
look like (i.e. it could be another generic Type, such as List<List<Integer>>
).
Obviously this can't work because the type T
is erased and unknown at runtime.
Attempt 1
The first thing I could get working is something like this:
import java.lang.reflect.*;
import java.util.*;
public class ReflectionAbuse {
public static void callMeMaybe(ArrayList<Integer> number) {
System.out.println("You called me!");
}
public static void callMeAgain(ArrayList<? extends Number> number) {
System.out.println("You called me again!");
}
public static void callMeNot(ArrayList<Double> number) {
System.out.println("What's wrong with you?");
}
public static <T> void reflectiveCall(List<T> number){
for(Method method : ReflectionAbuse.class.getDeclaredMethods()) {
if(method.getName().startsWith("call")) {
if(canBeParameterOf(method, number)) {
try {
method.invoke(null, number);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public static <T> boolean canBeParameterOf(Method method, List<T> number) {
return method.getGenericParameterTypes()[0].equals(number.getClass().getGenericSuperclass());
}
public static void main(String[] args) {
reflectiveCall(new ArrayList<Integer>(){});
}
}
which only prints
You called me!
However, this has some additional caveats:
- It only works for direct instances and inheritance hierarchy is not taken into account as the
Type
interface does not provide the needed methods. A cast here and there could definitely help to find this out (see also my second attempt) - The argument of
reflectiveCall
actually needs to be a subclass of the wanted parameter type (notice the{}
innew ArrayList<Integer>(){}
which create an anonymous inner class). That's obviously less than ideal: Creates unnecessary class objects and is error prone. This is the only way I could think of getting around type erasure.
Attempt 2
Thinking about the missing type in the ideal solution due to erasure, one could also pass the type as an argument which gets quite close to the ideal:
import java.lang.reflect.*;
import java.util.*;
public class ReflectionAbuse {
public static void callMeMaybe(List<Integer> number) {
System.out.println("You called me!");
}
public static void callMeAgain(List<? extends Number> number) {
System.out.println("You called me again!");
}
public static void callMeNot(List<Double> number) {
System.out.println("What's wrong with you?");
}
public static <T> void reflectiveCall(List<T> number, Class<T> clazz){
for(Method method : ReflectionAbuse.class.getDeclaredMethods()) {
if(method.getName().startsWith("call")) {
Type n = number.getClass().getGenericSuperclass();
if(canBeParameterOf(method, clazz)) {
try {
method.invoke(null, number);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public static <T> boolean canBeParameterOf(Method method, Class<T> clazz) {
Type type = ((ParameterizedType)method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
if (type instanceof WildcardType) {
return ((Class<?>)(((WildcardType) type).getUpperBounds()[0])).isAssignableFrom(clazz);
}
return ((Class<?>)type).isAssignableFrom(clazz);
}
public static void main(String[] args) {
reflectiveCall(new ArrayList<Integer>(), Integer.class);
}
}
which actually prints the correct solution. But this is also not without negative sides:
- The user of
reflectiveCall
needs to pass the type parameter which is unnecessary and tedious. At least the correct call is checked at compile time. - Inheritance between the type parameters is not completely taken into account and there are definitely many cases left which need to be implemented in
canBeParameterOf
(such as typed parameters). - And the biggest problem: The type parameter can't be generic in itself, so a
List
ofList
s ofIntegers
can't be used as an argument.
The question
Is there anything I could do differently to get as close as possible to my goal? Am I stuck with either using anonymous subclasses or passing the type parameter? For the moment, I'll settle with giving the parameter as this gives you compile time safety.
Is there anything I need to be aware of when recursively checking the parameter types?
Is there any possibility to allow generics a type parameter in solution 2?
Actually, for learning purposes I would like to roll my own solution instead of using a library although I wouldn't mind taking a look at the inner workings of some.
Just to make things clear, I'm for example aware of the following but try to keep the examples clean:
- This could potentially be solved without reflection (while redesigning some of the requirements and using for example interfaces and inner classes). This is for learning purposes. The problem I'd like to solve is actually quite a bit larger but this is what it boils down to.
- Instead of using a naming pattern I could use annotations. I actually do that, but I'd like to keep the examples as self-contained as possible.
- The array returned by
getGenericParameterTypes()
could be empty, but let's assume all methods have an argument and this is checked beforehand. - There could be non-static methods which fail when
null
is called. Assume there aren't. - The
catch
conditions could be more specific. - Some casts need to be more safe.