文档

Java™ 教程-Java Tutorials 中文版
故障排除
Trail: The Reflection API
Lesson: Members
Section: Methods

故障排除

本节包含开发人员在使用反射来查找,调用或获取有关方法的信息时可能遇到的问题的示例。

由于类型擦除的 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();

搜索 lookup(Object) 会取得期望的成功。

$ 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);      // solution
 	    Object o = m.invoke(ac);    // IllegalAccessException

        // production code should handle these exceptions more gracefully
	} 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);                 // works
		break;
	    case 1:
 		m.invoke(mtt, null);           // works (expect compiler warning)
		break;
	    case 2:
		Object arg2 = null;
		m.invoke(mtt, arg2);           // IllegalArgumentException
		break;
	    case 3:
		m.invoke(mtt, new Object[0]);  // works
		break;
	    case 4:
		Object arg4 = new Object[0];
		m.invoke(mtt, arg4);           // IllegalArgumentException
		break;
	    default:
		System.out.format("Test not found%n");
	    }

        // production code should handle these exceptions more gracefully
	} catch (Exception x) {
	    x.printStackTrace();
	}
    }
}
$ java MethodTroubleToo 0
PONG!

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

$ java MethodTroubleToo 1
PONG!

这种情况下的代码会生成此编译器警告,因为 null 不明确。

$ javac MethodTroubleToo.java
MethodTroubleToo.java:16: warning: non-varargs call of varargs method with
  inexact argument type for last parameter;
 		m.invoke(mtt, null);           // works (expect compiler warning)
 		              ^
  cast to Object for a varargs call
  cast to Object[] for a non-varargs call and to suppress this warning
1 warning

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

$ java MethodTroubleToo 2
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: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() failed: I can't drink a negative amount of liquid

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

Previous page: Invoking Methods
Next page: Constructors