文档

Java™教程
隐藏目录
故障排除
路径: 反射API
课程: 成员
部分: 方法

故障排除

这个部分包含了使用反射来定位、调用或获取方法信息时开发者可能遇到的问题的示例。

由于类型擦除引发的NoSuchMethodException

这个示例MethodTrouble展示了当搜索一个类中的特定方法时,如果没有考虑到类型擦除会发生什么。

import java.lang.reflect.Method;

public class MethodTrouble<T>  {
    public void lookup(T t) {}
    public void find(Integer i) {}

    public static void main(String... args) {
	try {
	    String mName = args[0];
	    Class cArg = Class.forName(args[1]);
	    Class<?> c = (new MethodTrouble<Integer>()).getClass();
	    Method m = c.getMethod(mName, cArg);
	    System.out.format("Found:%n  %s%n", m.toGenericString());

        // production code should handle these exceptions more gracefully
	} catch (NoSuchMethodException x) {
	    x.printStackTrace();
	} catch (ClassNotFoundException x) {
	    x.printStackTrace();
	}
    }
}
$ java MethodTrouble lookup java.lang.Integer
java.lang.NoSuchMethodException: MethodTrouble.lookup(java.lang.Integer)
        at java.lang.Class.getMethod(Class.java:1605)
        at MethodTrouble.main(MethodTrouble.java:12)
$ java MethodTrouble lookup java.lang.Object
Found:
  public void MethodTrouble.lookup(T)

当一个方法声明了一个泛型参数类型时,编译器会用其上界替换泛型类型,也就是说,在这种情况下,T的上界是Object。因此,当代码搜索lookup(Integer)时,尽管MethodTrouble的实例是这样创建的:

Class<?> c = (new MethodTrouble<Integer>()).getClass();

但是并没有找到任何方法。

$ java MethodTrouble find java.lang.Integer
Found:
  public void MethodTrouble.find(java.lang.Integer)
$ java MethodTrouble find java.lang.Object
java.lang.NoSuchMethodException: MethodTrouble.find(java.lang.Object)
        at java.lang.Class.getMethod(Class.java:1605)
        at MethodTrouble.main(MethodTrouble.java:12)

在这种情况下,find()没有泛型参数,因此由getMethod()搜索的参数类型必须完全匹配。


提示: 在搜索方法时,总是传递参数化类型的上界。

调用方法时的IllegalAccessException

如果尝试调用一个private或其他不可访问的方法,则会抛出IllegalAccessException

示例MethodTroubleAgain展示了尝试在另一个类中调用私有方法时产生的典型堆栈跟踪。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

class AnotherClass {
    private void m() {}
}

public class MethodTroubleAgain {
    public static void main(String... args) {
	AnotherClass ac = new AnotherClass();
	try {
	    Class<?> c = ac.getClass();
 	    Method m = c.getDeclaredMethod("m");
//  	    m.setAccessible(true);      // 解决方法
 	    Object o = m.invoke(ac);    // IllegalAccessException

        // 产品代码应该更优雅地处理这些异常
	} catch (NoSuchMethodException x) {
	    x.printStackTrace();
	} catch (InvocationTargetException x) {
	    x.printStackTrace();
	} catch (IllegalAccessException x) {
	    x.printStackTrace();
	}
    }
}

所抛出的异常的堆栈跟踪如下。

$ java MethodTroubleAgain
java.lang.IllegalAccessException: Class MethodTroubleAgain can not access a
  member of class AnotherClass with modifiers "private"
        at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
        at java.lang.reflect.Method.invoke(Method.java:588)
        at MethodTroubleAgain.main(MethodTroubleAgain.java:15)

提示: 存在访问限制,阻止通过反射调用通常无法通过直接调用访问的方法。(这包括但不限于在另一个类中的private方法和在一个私有类中的公共方法。)然而,Method被声明为扩展AccessibleObject,它提供了通过AccessibleObject.setAccessible()方法来抑制此检查的能力。如果成功,那么此方法对象的后续调用将不会因为这个问题而失败。

Method.invoke()引发的IllegalArgumentException

Method.invoke()已被改造为可变参数方法。这是非常方便的,但它可能导致意想不到的行为。在MethodTroubleToo示例中展示了Method.invoke()可能产生混乱结果的各种方式。

import java.lang.reflect.Method;

public class MethodTroubleToo {
    public void ping() { System.out.format("PONG!%n"); }

    public static void main(String... args) {
	try {
	    MethodTroubleToo mtt = new MethodTroubleToo();
	    Method m = MethodTroubleToo.class.getMethod("ping");

 	    switch(Integer.parseInt(args[0])) {
	    case 0:
  		m.invoke(mtt);                 // 正常工作
		break;
	    case 1:
 		m.invoke(mtt, null);           // 正常工作(预计编译器警告)
		break;
	    case 2:
		Object arg2 = null;
		m.invoke(mtt, arg2);           // IllegalArgumentException
		break;
	    case 3:
		m.invoke(mtt, new Object[0]);  // 正常工作
		break;
	    case 4:
		Object arg4 = new Object[0];
		m.invoke(mtt, arg4);           // IllegalArgumentException
		break;
	    default:
		System.out.format("找不到测试%n");
	    }

        // 生成代码应该更优雅地处理这些异常
	} catch (Exception x) {
	    x.printStackTrace();
	}
    }
}
$ java MethodTroubleToo 0
PONG!

由于Method.invoke()的所有参数都是可选的,除了第一个参数外,当要调用的方法没有参数时,可以省略这些参数。

$ java MethodTroubleToo 1
PONG!

在这种情况下,代码会生成此编译器警告,因为null是模棱两可的。

$ javac MethodTroubleToo.java
MethodTroubleToo.java:16: 警告: 使用不精确的参数类型对可变参数方法的非变长调用;
 		m.invoke(mtt, null);           // 正常工作(预计编译器警告)
 		              ^
  为可变参数调用进行对象转型
  为非变长调用进行对象数组转型以消除此警告
1 警告

无法确定null是表示空参数数组还是表示第一个参数为null

$ java MethodTroubleToo 2
java.lang.IllegalArgumentException: 参数数量错误
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke
          (NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke
          (DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at MethodTroubleToo.main(MethodTroubleToo.java:21)

尽管参数是null,但这个示例失败了,因为类型是一个Object,而ping()不希望有任何参数。

$ java MethodTroubleToo 3
PONG!

这个示例能够运行是因为new Object[0]创建了一个空数组,并且对于可变参数方法来说,这等同于不传递任何可选参数。

$ java MethodTroubleToo 4
java.lang.IllegalArgumentException: wrong number of arguments
        at sun.reflect.NativeMethodAccessorImpl.invoke0
          (Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke
          (NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke
          (DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at MethodTroubleToo.main(MethodTroubleToo.java:28)

与前一个示例不同,如果空数组存储在一个Object中,则将其视为一个Object。这个示例失败的原因与案例2相同,ping()不期望有参数。


提示: 当声明一个方法foo(Object... o)时,编译器将把所有传递给foo()的参数放入一个类型为Object的数组中。实现foo()的方式与声明foo(Object[] o)相同。理解这一点可能有助于避免上述示例中的问题。

当调用方法失败时的InvocationTargetException

InvocationTargetException会包装调用方法对象时产生的所有异常(已检查和未检查异常)。MethodTroubleReturns示例展示了如何检索由被调用方法抛出的原始异常。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MethodTroubleReturns {
    private void drinkMe(int liters) {
	if (liters < 0)
	    throw new IllegalArgumentException("I can't drink a negative amount of liquid");
    }

    public static void main(String... args) {
	try {
	    MethodTroubleReturns mtr  = new MethodTroubleReturns();
 	    Class<?> c = mtr.getClass();
   	    Method m = c.getDeclaredMethod("drinkMe", int.class);
	    m.invoke(mtr, -1);

        // production code should handle these exceptions more gracefully
	} catch (InvocationTargetException x) {
	    Throwable cause = x.getCause();
	    System.err.format("drinkMe() failed: %s%n", cause.getMessage());
	} catch (Exception x) {
	    x.printStackTrace();
	}
    }
}
$ java MethodTroubleReturns
drinkMe() 失败: 我不能喝负数的液体

提示: 如果抛出了InvocationTargetException,则该方法已被调用。对问题的诊断与直接调用该方法并抛出由getCause()检索到的异常相同。此异常不表示反射包或其使用存在问题。

上一页: 调用方法
下一页: 构造函数