使用Java反射
由格伦麦克拉斯基
1998年1月
反射是Java编程语言的一个特性。它允许执行的Java程序检查或“内省”自身,并操纵程序的内部属性。例如,Java类可以获取其所有成员的名称并显示它们。
从内部检查和操作Java类的能力听起来可能不是很多,但在其他编程语言中,此功能根本不存在。例如,Pascal,C或C ++程序中无法获取有关该程序中定义的函数的信息。
在JavaBeans中可以有效地使用反射,其中可以通过构建器工具直观地操作软件组件。该工具使用反射来获取动态加载的Java组件(类)的属性。
一个简单的例子
要了解反射是如何工作的,请考虑以下简单示例:
import java.lang.reflect.*; public class DumpMethods { public static void main(String args[]) { try { Class c = Class.forName(args[0]); Method m[] = c.getDeclaredMethods(); for (int i = 0; i < m.length; i++) System.out.println(m[i].toString()); } catch (Throwable e) { System.err.println(e); } } } |
对于调用:
java DumpMethods java.util.Stack
输出是:
public java.lang.Object java.util.Stack.push( java.lang.Object)
public synchronized java.lang.Object java.util.Stack.pop()
public synchronized java.lang.Object java.util.Stack.peek()
public boolean java.util.Stack.empty()
public synchronized int java.util.Stack.search(java.lang.Object) |
也就是说,java.util.Stack
列出了类的方法名称,以及它们的完全限定参数和返回类型。
此程序使用class.forName
,加载指定的类,然后调用getDeclaredMethods
以检索类中定义的方法列表。java.lang.reflect.Method
是一个表示单个类方法的类。
设置使用反射
反射类,例如Method
,在java.lang.reflect中找到。使用这些类必须遵循三个步骤。第一步是获取java.lang.Class
要操作的类的对象。java.lang.Class
用于表示正在运行的Java程序中的类和接口。
获取Class对象的一种方法是:
Class c = Class.forName("java.lang.String");
获取Class对象String
。另一种方法是使用:
class c = int.class;
要么
class c = Integer.TYPE;
获取基本类型的类信息。后一种方法访问基础类型TYPE
的包装器的预定义字段(例如Integer
)。
第二步是调用一个方法,例如getDeclaredMethods
,获取该类声明的所有方法的列表。
一旦掌握了这些信息,第三步就是使用反射API来操纵信息。例如,序列:
Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m[0].toString());
将显示声明的第一个方法的文本表示String
。
在下面的示例中,这三个步骤相结合,以展示如何使用反射处理特定应用程序的自包含插图。
模拟instanceof
运算符
一旦掌握了类信息,通常下一步就是询问有关Class对象的基本问题。例如,该Class.isInstance
方法可用于模拟instanceof
运算符:
class A {} public class instance1 { public static void main(String args[]) { try { Class cls = Class.forName("A"); boolean b1 = cls.isInstance(new Integer(37)); System.out.println(b1); boolean b2 = cls.isInstance(new A()); System.out.println(b2); } catch (Throwable e) { System.err.println(e); } } } |
在此示例中,A
创建了一个Class对象,然后检查类实例对象以查看它们是否是实例A
。Integer(37)
不是,但是new A()
。
找出关于类的方法
反射最有价值和最基本的用途之一是找出在类中定义的方法。为此,可以使用以下代码:
import java.lang.reflect.*; public class method1 { private int f1( Object p, int x) throws NullPointerException { if (p == null) throw new NullPointerException(); return x; } public static void main(String args[]) { try { Class cls = Class.forName("method1"); Method methlist[] = cls.getDeclaredMethods(); for (int i = 0; i < methlist.length; i++) { Method m = methlist[i]; System.out.println("name = " + m.getName()); System.out.println("decl class = " + m.getDeclaringClass()); Class pvec[] = m.getParameterTypes(); for (int j = 0; j < pvec.length; j++) System.out.println(" param #" + j + " " + pvec[j]); Class evec[] = m.getExceptionTypes(); for (int j = 0; j < evec.length; j++) System.out.println("exc #" + j + " " + evec[j]); System.out.println("return type = " + m.getReturnType()); System.out.println("-----"); } } catch (Throwable e) { System.err.println(e); } } } |
程序首先获取method1的类描述,然后调用getDeclaredMethods
以检索Method
对象列表,对应于类中定义的每个方法。这些包括public,protected,package和private方法。如果您getMethods
在程序中使用而不是getDeclaredMethods
,则还可以获取继承方法的信息。
Method
获得对象列表后,只需显示有关每种方法的参数类型,异常类型和返回类型的信息。这些类型中的每一种,无论它们是基本类型还是类类型,都由类描述符表示。
该计划的输出是:
name = f1 decl class = class method1 param #0 class java.lang.Object param #1 int exc #0 class java.lang.NullPointerException return type = int ----- name = main decl class = class method1 param #0 class [Ljava.lang.String; return type = void ----- |
获取有关构造函数的信息
类似的方法用于了解类的构造函数。例如:
import java.lang.reflect.*; public class constructor1 { public constructor1() { } protected constructor1(int i, double d) { } public static void main(String args[]) { try { Class cls = Class.forName("constructor1"); Constructor ctorlist[] = cls.getDeclaredConstructors(); for (int i = 0; i < ctorlist.length; i++) { Constructor ct = ctorlist[i]; System.out.println("name = " + ct.getName()); System.out.println("decl class = " + ct.getDeclaringClass()); Class pvec[] = ct.getParameterTypes(); for (int j = 0; j < pvec.length; j++) System.out.println("param #" + j + " " + pvec[j]); Class evec[] = ct.getExceptionTypes(); for (int j = 0; j < evec.length; j++) System.out.println( "exc #" + j + " " + evec[j]); System.out.println("-----"); } } catch (Throwable e) { System.err.println(e); } } } |
在此示例中没有检索到返回类型信息,因为构造函数实际上没有真正的返回类型。
运行此程序时,输出为:
name = constructor1
decl class = class constructor1
-----
name = constructor1
decl class = class constructor1
param#0 int
param#1 double
----- |
了解类字段
还可以找出在类中定义的数据字段。为此,可以使用以下代码:
import java.lang.reflect.*; public class field1 { private double d; public static final int i = 37; String s = "testing"; public static void main(String args[]) { try { Class cls = Class.forName("field1"); Field fieldlist[] = cls.getDeclaredFields(); for (int i = 0; i < fieldlist.length; i++) { Field fld = fieldlist[i]; System.out.println("name = " + fld.getName()); System.out.println("decl class = " + fld.getDeclaringClass()); System.out.println("type = " + fld.getType()); int mod = fld.getModifiers(); System.out.println("modifiers = " + Modifier.toString(mod)); System.out.println("-----"); } } catch (Throwable e) { System.err.println(e); } } } |
此示例与之前的示例类似。一个新功能是使用Modifier
。这是一个反射类,表示在字段成员上找到的修饰符,例如“ private int
”。修饰符本身由整数表示,Modifier.toString
用于以“官方”声明顺序(例如“ static
”之前的“ final
”)返回字符串表示。该计划的输出是:
name = d decl class = class field1 type = double modifiers = private ----- name = i decl class = class field1 type = int modifiers = public static final ----- name = s decl class = class field1 type = class java.lang.String modifiers = ----- |
与方法一样,可以获取有关在class(getDeclaredFields
)中声明的字段的信息,或者获取有关在超类(getFields
)中定义的字段的信息。
按名称调用方法
到目前为止,已经呈现的示例都与获取类信息有关。但是也可以以其他方式使用反射,例如调用指定名称的方法。
要了解其工作原理,请考虑以下示例:
import java.lang.reflect.*; public class method2 { public int add(int a, int b) { return a + b; } public static void main(String args[]) { try { Class cls = Class.forName("method2"); Class partypes[] = new Class[2]; partypes[0] = Integer.TYPE; partypes[1] = Integer.TYPE; Method meth = cls.getMethod( "add", partypes); method2 methobj = new method2(); Object arglist[] = new Object[2]; arglist[0] = new Integer(37); arglist[1] = new Integer(47); Object retobj = meth.invoke(methobj, arglist); Integer retval = (Integer)retobj; System.out.println(retval.intValue()); } catch (Throwable e) { System.err.println(e); } } } |
假设一个程序想要调用该add
方法,但直到执行时才知道这个。也就是说,在执行期间指定方法的名称(例如,这可能由JavaBeans开发环境完成)。上述程序显示了这样做的一种方法。
getMethod
用于在类中查找具有两个整数参数类型且具有适当名称的方法。一旦找到此方法并将其捕获到Method
对象中,就会在适当类型的对象实例上调用它。要调用方法,必须构造参数列表,基本整数值37和47包含在Integer
对象中。返回值(84)也包装在一个Integer
对象中。
创建新对象
没有与构造函数的方法调用等效,因为调用构造函数等同于创建新对象(最精确的是,创建新对象涉及内存分配和对象构造)。所以与上一个例子最接近的等价是:
import java.lang.reflect.*; public class constructor2 { public constructor2() { } public constructor2(int a, int b) { System.out.println( "a = " + a + " b = " + b); } public static void main(String args[]) { try { Class cls = Class.forName("constructor2"); Class partypes[] = new Class[2]; partypes[0] = Integer.TYPE; partypes[1] = Integer.TYPE; Constructor ct = cls.getConstructor(partypes); Object arglist[] = new Object[2]; arglist[0] = new Integer(37); arglist[1] = new Integer(47); Object retobj = ct.newInstance(arglist); } catch (Throwable e) { System.err.println(e); } } } |
它找到一个处理指定参数类型并调用它的构造函数,以创建该对象的新实例。这种方法的价值在于它纯粹是动态的,在执行时使用构造函数查找和调用,而不是在编译时。
改变字段的值
反射的另一个用途是更改对象中数据字段的值。它的值再次来自反射的动态特性,其中可以在执行程序中按名称查找字段,然后更改其值。以下示例说明了这一点:
import java.lang.reflect.*; public class field2 { public double d; public static void main(String args[]) { try { Class cls = Class.forName("field2"); Field fld = cls.getField("d"); field2 f2obj = new field2(); System.out.println("d = " + f2obj.d); fld.setDouble(f2obj, 12.34); System.out.println("d = " + f2obj.d); } catch (Throwable e) { System.err.println(e); } } } |
在此示例中,d字段的值设置为12.34。
使用数组
反射的最后一个用途是创建和操作数组。Java语言中的数组是一种特殊类型的类,可以将数组引用赋给Object
引用。
要查看数组的工作方式,请考虑以下示例:
import java.lang.reflect.*; public class array1 { public static void main(String args[]) { try { Class cls = Class.forName( "java.lang.String"); Object arr = Array.newInstance(cls, 10); Array.set(arr, 5, "this is a test"); String s = (String)Array.get(arr, 5); System.out.println(s); } catch (Throwable e) { System.err.println(e); } } } |
此示例创建一个10长的字符串数组,然后将数组中的位置5设置为字符串值。检索并显示该值。
以下代码说明了更复杂的数组操作:
|
此示例创建一个5 x 10 x 15的整数数组,然后继续将数组中的位置[3] [5] [10]设置为值37.请注意,多维数组实际上是一个数组数组因此,例如,在第一个Array.get之后,arrobj中的结果是10 x 15阵列。再次将其剥离以获得15个长的阵列,并使用该阵列中的第10个插槽进行设置Array.setInt
。
请注意,创建的数组类型是动态的,不必在编译时知道。
摘要
Java反射非常有用,因为它支持按名称动态检索有关类和数据结构的信息,并允许在执行的Java程序中对它们进行操作。此功能非常强大,并且在其他传统语言(如C,C ++,Fortran或Pascal)中没有等效功能。
Glen McCluskey自1988年以来一直专注于编程语言。他参与Java和C ++性能,测试和技术文档领域的咨询。