java 核心技术-12版 卷Ⅰ- 5.9.7 调用任意方法和构造器
原文
5.9.7 调用任意方法和构造器
在C和C++ 中,可以通过一个函数指针执行任意函数。从表面上看,Java 没有提供方法指针,也就是说,Java 没有提供途径将一个方法的存储地址传给另外一个方法,以便第二个方法以后调用。事实上,Java 的设计者曾说过: 方法指针很危险,而且很容易出错。他们认为Java 的接口 (interface) 和 lambda 表达式(将在下一章讨论) 是一种更好的解方案。不过,反射机制允许你调用任意的方法。
回想一下,可以用 Field类的 get 方法查看一个对象的字段。与之类似,Method 类有一个invoke 方法,允许你调用包装在当前 Method 对象中的方法。invoke 方法的签名为Object invoke(0bject obj, Object... args)
第一个参数是隐式参数,其余的对象提供了显式参数。
对于静态方法,第一个参数会忽略,即可以将它设置为 null。例如,假设用 m1 表示 Employee 类的 getName 方法,下面这条语句显示了如何调用这个方法:
String n = (String) m1.invoke(harry);
如果返回类型是基本类型,则 invoke 方法会返回其包装器类型。例如,假设 m2 表示Employee 类的 getSalary 方法,那么返回的对象实际上是一个 Double,必须相应地完成强制类型转换。可以使用自动拆箱将它转换为一个 double:
double s = (Double) m2.invoke(harry);
如何得到 Method 对象呢?当然,可以调用 getDeclaredMethods 方法,然后搜索返回的 Method对象数组,直到发现想要的方法为止。也可以调用 Class 类的 getMethod 方法。这与 getField 方去类似。getField 方法接受一个表示字段名的字符申,返回一个 Field 对象。不过,有可能存在若干个同名的方法,因此要准确地得到想要的那个方法必须格外小心。有鉴于此,还必须是供想要的方法的参数类型。getMethod 的签名为
Method getMethod(String name, Class... parameterTypes)
例如,下面展示了如何获得 Employee 类的 getName 方法和 raiseSalary 方法的方法指针:
Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);
可以使用类似的方法调用任意的构造器。将构造器的参数类型提供给 Class.getConstructor方法,并为 Constructor.newInstance 方法提供参数值;
// or any other class with a constructor that
// accepts a long parameter
Class cl = Random.class;
Constructor cons = cl.getConstructor(long.class);
Object obj = cons.newInstance(42L);
注释: Method和 Constructor 类扩展了 Executable 类。在Java 17中,Executable 类是密封类,只允许Method和 Constructor 作为子类。
到此为止,我们已经了解了使用 Method 对象的规则。下面来看如何具体使用。程序清单 5-19的程序会打印一个数学函数 (如 Math.sqrt 或 Math.sin) 的取值表。打印的结果如下所示:
public static native double java.lang.Math.sqrt(double)
1.0000 | 1.0000
2.0000 | 1.4142
3.0000 | 1.7321
4.0000 | 2.0000
5.0000 | 2.2361
6.0000 | 2.4495
7.0000 | 2.6458
8.0000 | 2.8284
9.0000 | 3.0000
10.0000 | 3.1623
当然,打印表格的代码与表格中计算的数学函数无关
double dx = (to - from ) / (n -1);
for( double x = from;x <= to; x+= dx){
double y = (Double) f.invoke(null, x);
System.out.println("%10.4f | %10.4f%n",x ,x);
}
在这里,f 是一个 Method 类型的对象。由于我们调用的方法是一个静态方法,所以 invoke的第一个参数是 null。
要打印 Math.sqrt 函数的取值表,可以如下设置 f:
Math.class.getMethod("sqrt",double.class)
这是 Math类的一个方法,名为 sqrt,有一个 double 类型的参数。程序清单 5-19 给出了这个通用取值表程序和两个测试的完整代码
程序清单5-19
methods/MethodTableTest.java
package methods;
import java.lang.reflect.*;
/**
* This program shows how to invoke methods through reflection.
* @version 1.2 2012-05-04
* @author Cay Horstmann
*/
public class MethodTableTest
{
public static void main(String[] args)
throws ReflectiveOperationException
{
// get method pointers to the square and sqrt methods
Method square = MethodTableTest.class.getMethod("square", double.class);
Method sqrt = Math.class.getMethod("sqrt", double.class);
// print tables of x- and y-values
printTable(1, 10, 10, square);
printTable(1, 10, 10, sqrt);
}
/**
* Returns the square of a number
* @param x a number
* @return x squared
*/
public static double square(double x)
{
return x * x;
}
/**
* Prints a table with x- and y-values for a method
* @param from the lower bound for the x-values
* @param to the upper bound for the x-values
* @param n the number of rows in the table
* @param f a method with a double parameter and double return value
*/
public static void printTable(double from, double to, int n, Method f)
throws ReflectiveOperationException
{
// print out the method as table header
System.out.println(f);
double dx = (to - from) / (n - 1);
for (double x = from; x <= to; x += dx)
{
double y = (Double) f.invoke(null, x);
System.out.printf("%10.4f | %10.4f%n", x, y);
}
}
}
这个例子清楚地表明,利用 Method 对象可以实现 C 语言中函数指针(或C# 中的委托)所能完成的所有操作。同C 中一样,这种编程风格不是很方便,而且总是很容易出错。如果在调用方法的时候提供了错误的参数会发生什么? invoke 方法将会抛出一个异常。
另外,invoke 的参数和返回值必须是 Object 类型。这就意味着必须来回进行多次强制类型转换。这样一来,编译器会丧失检查代码的机会,以至于等到测试阶段才会发现错误,而这个时候查找和修正错误会麻烦得多。不仅如此,使用反射获得方法指针的代码要比直接调用方法的代码慢得多。
有鉴于此,建议仅在绝对必要的时候才在你自己的程序中使用 Method 对象。通常,更好的做法是使用接口以及Java 8引人的 lambda 表达式(第6章中介绍)。特别要强调:我们建议Java 开发人员不要使用回调函数的 Method 对象。可以使用回调的接口,这样不仅代码的执行速度更快,也更易于维护。
java.lang.reflect.Method 1.1
- public Object invoke(Object implicitParameter, Object[] explicitParameters) 调用这个对象描述的方法,传人给定参数,并返回那个方法的返回值。对于静态方法,传人 null作为隐式参数。使用包装器传递基本类型值。基本类型的返回值必须拆包。