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方法则会报错

posted on 2012-08-27 09:58  编程趋势  阅读(2763)  评论(0编辑  收藏  举报