如何使用反射检查给定对象是否将是方法的有效参数(其中参数和对象是泛型类型)?
为了得到一点背景,这是我想要达到的:
在使用反射方法调用时,我认为调用具有特定类型参数的所有方法会很好。这对于原始类型非常有效,因为您可以在它们的类对象上调用isAssignableFrom(Class<?> c)
。然而,当你开始加入泛型时,这就不那么容易了,因为泛型并不是反射最初设计的一部分,而且还有类型擦除。
问题更大,但它基本上归结为以下几点:
理想溶液
理想情况下代码
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>());
}
}
将打印
You called me!
You called me again!
无论T
看起来如何,这都应该是可能的(即它可能是另一个泛型类型,如List<List<Integer>>
)。
显然这不能工作,因为类型T
在运行时被擦除并且未知。
尝试1
我能做的第一件事是这样的:
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>(){});
}
}
只打印
You called me!
但是,这有一些额外的注意事项:
- 它只适用于直接实例,由于
Type
接口不提供所需的方法,因此不考虑继承层次结构。这里和那里的演员绝对可以帮助找到这个(也见我的第二次尝试) reflectiveCall
的参数实际上需要是所需参数类型的子类(注意new ArrayList<Integer>(){}
中的{}
创建了一个匿名内部类)。这显然不太理想:创建不必要的类对象并且容易出错。这是我能想到的绕过类型擦除的唯一方法。
尝试2
考虑到理想解中由于擦除而丢失的类型,也可以将该类型作为参数传递,这与理想解非常接近:
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);
}
}
实际打印正确的解决方案。但这也不是没有消极的一面:
reflectiveCall
的用户需要传递类型参数,这是不必要且繁琐的。至少在编译时检查了正确的调用。- 类型参数之间的继承没有被完全考虑,并且肯定有许多情况需要在
canBeParameterOf
中实现(例如类型化参数)。 - 最大的问题是:类型参数本身不能是泛型的,所以
List
的List
的Integers
不能用作参数。
的问题我能做什么不同的事情来尽可能接近我的目标吗?我是否坚持使用匿名子类或传递类型参数?目前,我将满足于给出参数,因为这样可以保证编译时的安全性。
递归检查参数类型时,我需要注意什么吗?
是否有可能在解决方案2中允许泛型作为类型参数?
实际上,出于学习的目的,我想推出我自己的解决方案,而不是使用库,尽管我不介意看看一些库的内部工作原理。
为了让事情更清楚,我知道下面的例子,但尽量保持例子的干净:
- 这可以在没有反射的情况下解决(同时重新设计一些需求并使用例如接口和内部类)。这是为了学习。我想要解决的问题实际上要大得多,但归结起来就是这样。
- 我可以使用注释代替命名模式。我实际上是这样做的,但我想让示例尽可能地独立。
getGenericParameterTypes()
返回的数组可以是空的,但是我们假设所有的方法都有一个参数,并且这是事先检查的。- 调用
null
时可能存在非静态方法失败。 catch
条件可以更具体。
我是否坚持使用匿名子类或传递类型参数?
或多或少,但最好使用超类型标记模式创建子类,而不是创建值类型的子类。毕竟,您的值类型可能不允许子类。使用类型标记模式允许您接受泛型类型,而接受Class
作为类型参数只允许原始类型(这就是为什么您必须接受List
的组件类型,而不是类型本身)。
public static <T> void reflectiveCall(TypeToken<T> type, T value)
Guava对创建和使用TypeToken
s有很好的支持。到目前为止,最简单的创建方法是创建一个匿名子类(注意:如果要重用它,请将其设置为常量):
reflectiveCall(new TypeToken<List<Integer>>() {}, new ArrayList<Integer>());
一旦你有了这个,canBeParameterOf
就变得更容易实现了。
public static boolean canBeParameterOf(Method method, TypeToken<?> givenType) {
Type[] argTypes = method.getGenericParameterTypes();
return argTypes.length != 0 &&
TypeToken.of(argTypes[0]).isAssignableFrom(givenType);
}