Java方法调用在传参的时候遵循就近兼容原则:如果找不到实际参数类型与声明类型完全匹配的方法,Java会自动寻找其他可以兼容实际参数类型的方法。所谓“兼容”指实际参数类型从声明类型继承而来,或者如声明类型是接口,实际参数类型实现该接口(注:本文仅针对引用类型,暂不考虑基本类型之间的兼容性)
最简单的例子如下(以构造函数为例):
public class Test { public Test(List<?> arg1) { System.out.println("Instance Newed as List: "+arg1.getClass()); } public static void main(String[] args){ new Test(new ArrayList<Object>()); } }
输出:
Instance Newed as List: class java.util.ArrayList
但是如果用利用反射机制调用方法,在获取参数的时候参数类型必须精确匹配:
public class Test { public Test(List<?> arg1) { System.out.println("Instance Newed as List: "+arg1.getClass()); } public static void main(String[] args){ Class<?> clazz = Test.class; Constructor<?> constructor; ArrayList<String> arg = new ArrayList<String>(); constructor = clazz.getDeclaredConstructor(arg.getClass()); constructor.newInstance(arg); } }
以上代码执行时会报“找不到方法”异常:
Exception in thread "main" java.lang.NoSuchMethodException: test.Test.<init>(java.util.ArrayList) at java.lang.Class.getConstructor0(Unknown Source) at java.lang.Class.getDeclaredConstructor(Unknown Source) at test.Test.main
原因是实际参数arg的真实类型是ArrayList,因此args.getClass()返回ArrayList.class,而不是List.class,因此反射机制找不到匹配的构造函数
为解决这个问题,可以修改getDeclaredConstructor的参数:
constructor = clazz.getDeclaredConstructor(List.class);
此时程序运行正常,输出如下:
Instance Newed as List: class java.util.ArrayList
但这样做的缺点是必须在代码中显示写明声明类型List.class,这样就无法实现带多态的方法。有没有办法像普通调用那样,根据实际参数类型自动匹配兼容的方法呢?
我们可以看一下Class类的源码,以下是Class类中与构造相关的几个关键方法:
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { checkMemberAccess(Member.DECLARED, ClassLoader.getCallerClassLoader()); return getConstructor0(parameterTypes, Member.DECLARED); } private Constructor<T> getConstructor0(Class[] parameterTypes, int which) throws NoSuchMethodException { Constructor[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC)); for (int i = 0; i < constructors.length; i++) { if (arrayContentsEq(parameterTypes, constructors[i].getParameterTypes())) { return getReflectionFactory().copyConstructor(constructors[i]); } } throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes)); } private static boolean arrayContentsEq(Object[] a1, Object[] a2) { if (a1 == null) { return a2 == null || a2.length == 0; } if (a2 == null) { return a1.length == 0; } if (a1.length != a2.length) { return false; } for (int i = 0; i < a1.length; i++) { if (a1[i] != a2[i]) { return false; } } return true; }
其中公有方法getDeclaredConstructor是反射调用的入口,其中的checkMemberAccess方法主要是校验方法和参数的可视性,与本文关系不大
继而调用的私有方法getConstructor0是功能核心,先通过私有方法privateGetDeclaredConstructors获取全部(公有的)构造方法列表。privateGetDeclaredConstructors这个方法的核心是一个通过底层实现的原生方法(native method),本文不作探讨其具体的实现机制。Class类还有一个公有入口方法getDeclaredConstructors,用来从外部获取全部构造方法列表,这个公有入口本质上也是调用privateGetDeclaredConstructors方法实现功能。
获得方法列表后,通过轮循列表寻找参数类型匹配的方法,匹配与否的判断通过静态方法arrayContentsEq来实现。这个方法就是逐个比较两个Object数组(实际是Class数组)的对应元素。反射机制无法实现类型兼容的源头正在此处,因为arrayContentsEq方法中是直接用等号比对两个Class对象,因此继承或实现接口等关系就被完全忽略了。
找到问题后修改就很容易了,Class本身就带有一个公有的原生方法isAssignableFrom(Class<?> cls),当且仅当B类继承或实现A类,A.class.isAssignableFrom(B.class)返回true,因此原则上只要将arrayContentsEq中if (a1[i] != a2[i])这句判断改成if (!a2[i].isAssignableFrom(a1[i]))就可以了。
当然我们不太可能重写Class类,但是可以仿照上面的三个方法,自己写一个方法来获取一个Class类的特定方法:
public static Constructor<?> getCompatibleDeclaredConstructor( Class<?> clazz, Class<?>... parameterTypes) throws NoSuchMethodException { Constructor<?>[] constructors = clazz.getDeclaredConstructors(); Constructor<?> compatibleConstructor = null; for (int i = 0; i < constructors.length; i++) if (parameterTypesCampatible(parameterTypes, constructors[i].getParameterTypes()) && ((compatibleConstructor == null) || parameterTypesCampatible( constructors[i].getParameterTypes(), compatibleConstructor.getParameterTypes()))) compatibleConstructor = constructors[i]; if (compatibleConstructor == null) throw new NoSuchMethodException(clazz.getName() + ".<init>" + argumentTypesToString(parameterTypes)); return compatibleConstructor; } private static boolean parameterTypesCampatible(Class<?>[] a1, Class<?>[] a2) { if (a1 == null) return a2 == null || a2.length == 0; if (a2 == null) return a1.length == 0; if (a1.length != a2.length) return false; for (int i = 0; i < a1.length; i++) if (!a2[i].isAssignableFrom(a1[i])) return false; return true; }
getCompatibleDeclaredConstructor中,把Class类作为参数传入,这样就避免重写Class类本身的方法。方法中沿用原先的思路,先通过Class.getDeclaredConstructors方法获取构造函数列表,再轮循比对参数兼容性,比对通过私有方法parameterTypesCampatible实现,其地位相当于原先的Class.arrayContentsEq方法,但是我们放宽了兼容性条件。
getCompatibleDeclaredConstructor中我们利用了一个compatibleConstructor对象来存储返回值,这个对象的在轮循中可能会多次赋值,因为在有多态的情况下可能有多个方法与实际参数类型兼容。轮循中if语句的判断条件恰好实现了就近兼容原则。
以上代码中还用到一个用来输出报错的静态方法argumentTypesToString,这个方法可以从Class类中复制过来,与核心功能关系不大,实现代码如下:
private static String argumentTypesToString(Class<?>[] argTypes) { StringBuilder buf = new StringBuilder("("); if (argTypes != null) { for (int i = 0; i < argTypes.length; i++) { buf.append(i > 0 ? ", " : ""); buf.append((argTypes[i] == null) ? "null" : argTypes[i] .getName()); } } return buf.append(")").toString(); }
以下是对上述方法的测试代码:
public class Test { public Test(List<?> arg1) { System.out.println("Instance Newed as List: " + arg1.getClass()); } public static Constructor<?> getCompatibleDeclaredConstructor(); private static boolean parameterTypesCampatible(); private static String argumentTypesToString(); public static void main(String[] args) throws Exception { // TODO Auto-generated method stub Class<?> clazz = Test.class; Constructor<?> constructor; ArrayList<String> arg = new ArrayList<String>(); System.out.println("直接构造:"); new Test(new ArrayList<Object>()); System.out.println("兼容式反射构造:"); constructor = getCompatibleDeclaredConstructor(clazz, arg.getClass()); constructor.newInstance(arg); System.out.println("直接反射构造:"); constructor = clazz.getDeclaredConstructor(arg.getClass()); constructor.newInstance(arg); } }
输出:
直接构造:
Instance Newed as List: class java.util.ArrayList
兼容式反射构造:
Instance Newed as List: class java.util.ArrayList
直接反射构造:
Exception in thread "main" java.lang.NoSuchMethodException: test.Test.<init>(java.util.ArrayList)
at java.lang.Class.getConstructor0(Unknown Source)
at java.lang.Class.getDeclaredConstructor(Unknown Source)
at test.Test.main(Test.java:100)
可以看到通过修改后的反射机制实现的功能与直接引用相同,而直接用原先的Class.getDeclaredConstructor方法则会报错
作者:编程趋势
出处:http://www.cnblogs.com/codetrend/
作者微博:http://weibo.com/liuxue9527
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。