反射:能够分析类能力的程序。
反射机制的作用:
•在运行时分析类的能力。
•在运行时查看对象, 例如, 编写一个 toString 方法供所有类使用。
•实现通用的数组操作代码。
•利用 Method 对象, 这个对象很像中的函数指针。
本文包含如下内容:
- class类
- 捕获异常
- 利用反射分析类的能力
- 在运行时使用反射分析对象
- 使用反射编写泛型数组代码
- 调用任意方法
1.class类
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。
可以通过专门的Java类访问这些信息,保存这些信息的类称为Class。Object类中的getClass()方法会返回一个Class类型的实例。
Employee e;
Class cl = e.getClassO;
最常用的Class方法是getName(),这个方法返回类的名字。
例如下面这条语句:
System.out.println(e.getClass().getName() + " " + e.getNameO);
如果 e 是一个雇员,则会打印输出:
Employee Harry Hacker
如果 e 是经理, 则会打印输出:
Manager Harry Hacker
如果类在一个包里,包的名字也作为类名的一部分:
Random generator = new Random0:
Class cl = generator.getClass() ;
String name = cl.getNameQ; // name is set to "java.util.Random"
还可以调用静态方法 forName 获得类名对应的 Class 对象。
String dassName = "java.util .Random";
Class cl = Cl ass.forName(dassName) ;
如果类名保存在字符串中, 并可在运行中改变, 就可以使用这个方法。当然, 这个方法
只有在 dassName 是类名或接口名时才能够执行。 否则,forName 方法将抛出一个 checked
exception ( 已检查异常)。无论何时使用这个方法, 都应该提供一个异常处理器(exception
handler) o 如何提供一个异常处理器,请参看下一节。
获得 Class 类对象的第三种方法非常简单。如果 T 是任意的 Java 类型(或 void 关键字,)
T.class 将代表匹配的类对象。例如:
Class dl = Random,class; // if you import java.util
Gass cl 2 = int.class;
Class cl 3 = Doublet] ,cl ass;
请注意, 一个 Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类。 例如,
int 不是类, 但 int.class 是一个 Class 类型的对象。
Class 类实际上是一个泛型类。例如, Employee.class 的类型是 Class<Employee。
没有说明这个问题的原因是: 它将已经抽象的概念更加复杂化了。 在大多数实际问题中, 可以忽略类型参数, 而使用原始的 Class 类。
注意:鉴于历史原因, getName 方法在应用于数组类型的时候会返回一个很奇怪的名字:
•Double[ ] class.getName( ) 返回“ [Ljava.lang.Double;’’
•int[ ].class.getName( ) 返回“ [I ” ,
虚拟机为每个类型管理一个 Class 对象。 因此, 可以利用= 运算符实现两个类对象比较的操作。 例如,
if (e.getClass()== Employee, class) . . .
还有一个很有用的方法 newlnstance( ), 可以用来动态地创建一个类的实例例如,
e.getClass0.newlnstance();
创建了一个与 e 具有相同类类型的实例。
newlnstance 方法调用默认的构造器 (没有参数的构造器)初始化新创建的对象。 如果这个类没有默认的构造器, 就会抛出一个异常。
将 forName 与 newlnstance 配合起来使用, 可以根据存储在字符串中的类名创建一个对象
String s = "java.util .Random";
Object m = Cl ass.forName(s) .newlnstance();
注意:
如果需要以这种方式向希望按名称创建的类的构造器提供参数, 就不要使用上面那条语句, 而必须使用 Constructor 类中的 newlnstance 方法。
C++ 注释:newlnstance 方法对应 C++ 中虚拟构造器的习惯用法。 然而,C++ 中的虚拟构造器不是一种语言特性, 需要由专门的库支持。 Class 类与 C++ 中的 typejnfo 类相似,getClass 方法与 C++ 中的 typeid 运算符等价。 但 Java 中的 Class 比 C++ 中的 type_info的功能强。C++ 中的 typejnfo 只能以字符串的形式显示一个类型的名字, 而不能创建那个类型的对象。
2. 捕获异常
捕获异常的处理器可以对异常情况进行处理
如果没有提供处理器, 程序就会终止,并在控制台上打印出一条信息, 其中给出了异常的类型。
异常有两种类型: 未检查异常和已检查异常。
- 对于已检查异常, 编译器将会检查是否提供了处理器。
- 然而, 有很多常见的异常, 例如, 访问 null 引用, 都属于未检查异常。 编译器不会査看是否为这些错误提供了处理器。毕竟,应该精心地编写代码来避免这些错误的发生, 而不要将精力花在编写异常处理器上。
并不是所有的错误都是可以避免的。 如果竭尽全力还是发生了异常, 编译器就要求提供一个处理器。
Class.forName 方法就是一个抛出已检查异常的例子。
接下来介绍如何实现最简单的处理器:
将可能抛出已检査异常的一个或多个方法调用代码放在 try 块中,然后在 catch 子句中提供处理器代码
try
{
statements that might throw exceptions
}
catch (Exception e)
{
handler action
}
下面是一个示例:
try
{
String name = . . .; // get class name
Class cl = Class.forName(name) ; // might throw exception
do something with cl
}
catch (Exception e)
{
e.printStackTraceO ;
}
能抛出已检査异常的一个或多个方法调用代码放在 try 块中,然后在 catch 子句中提供处理器代码。
如果类名不存在, 则将跳过 try 块中的剩余代码, 程序直接进人 catch 子句(这里,利用Throwable 类的 printStackTrace 方法打印出栈的轨迹。Throwable 是 Exception 类的超类)。 如果 try 块中没有抛出任何异常, 那么会跳过 catch 子句的处理器代码。
对于已检查异常, 只需要提供一个异常处理器。 可以很容易地发现会抛出已检査异常的方法。如果调用了一个抛出已检查异常的方法, 而又没有提供处理器, 编译器就会给出错误报告.
java.lang.Class 1.0
• static Class forName(String className)
返回描述类名为 className 的 Class 对象。
• Object newlnstance()
返回这个类的一个新实例。
java.Iang.reflect.Constructor 1.1
• Object newlnstance(Object[] args)
构造一个这个构造器所属类的新实例。
参数:args 这是提供给构造器的参数。
java.Iang.Throwable 1.0
• void printStackTrace()
将 Throwable 对象和栈的轨迹输出到标准错误流。
3.利用反射分析类的能力
下面简要地介绍一下反射机制最重要的内容—检查类的结构
在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的域、 方法和构造器。 这三个类都有一个叫做 getName 的方法, 用来返回项目的名称。
Field 类有一个 getType 方法, 用来返回描述域所属类型的 Class 对象。
Method 和 Constructor 类有能够报告参数类型的方法,
Method 类还有一个可以报告返回类型的方法。
这 三个类还有一个叫做 getModifiers 的方法, 它将返回一个整型数值, 用不同的位开关描述 public 和static 这样的修饰符使用状况。另外, 还可以利用java.lang.reflect 包中的 Modifier类的静态方法分析getModifiers 返回的整型数值。 例如, 可以使用 Modifier 类中的 isPublic、 isPrivate 或 isFinal判断方法或构造器是否是 public、 private 或 final。
我们需要做的全部工作就是调用 Modifier类的相应方法, 并对返回的整型数值进行分析, 另外,还可以利用 Modifier.toString方法将修饰符打印出来。
Class类中的 getFields、 getMethods 和 getConstructors 方 法 将 分 别 返 回 类 提 供 的public 域、 方法和构造器数组, 其中包括超类的公有成员。
Class 类的 getDeclareFields、getDeclareMethods 和getDeclaredConstructors 方法将分别返回类中声明的全部域、 方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。
下面程序显示了如何打印一个类的全部信息的方法。这个程序将提醒用户输入类名,然后输出类中所有的方法和构造器的签名, 以及全部域名。
package reflection;
import java.util
import java.lang.reflect.*;
11 public class ReflectionTest
12 {
13 public static void main(String[] args)
14 {
is // read class name from command line args or user input
16 String name;
17 if (args.length > 0) name = args[0];
18 else
19 {
20 Scanner in = new Scanner(System.in);
21 System.out.println("Enter class name (e.g. java.util.Date): ");
22 name = in.nextO;
23 }
24
2s try
26 {
27 // print class name and superclass name (if != Object)
28 Class cl = Class.fo「Name(name);
29 Class supercl = cl.getSuperclassO;
30 String modifiers = Modifier.toString(cl.getModifiers());
31 if (modi fiers.lengthO > 0) System,out.print(modifiers + " ");
32 System.out.print("class"+ name);
33 if (s叩ercl != null && supercl != Object,cl ass) System.out.print(" extends "
34 + supercl.getNameO);
35
36 System.out.print("\n{\n");
37 printConstructors(d);
38 System.out.printlnO;
39 printMethods(cl);
System.out.printlnO;
printFields(cl);
System.out.println("}");
}
catch (ClassNotFoundException e)
{
e.printStackTraceO;
}
System.exit(O) ;
public static void printConstructors(Class cl)
{
Constructor ]] constructors = cl.getDedaredConstructors():
for (Constructor c : constructors)
{
String name = c.getNameO ;
System.out.print(" ") ;
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.lengthO > 0) System,out.print(modifiers + " ");
System,out.print(name + "(");
// print parameter types
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++)
{
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
public static void printMethods(Class cl)
{
Method□ methods - cl.getDedaredMethodsO ;
for (Method m : methods)
{
Class retType = m.getReturnType();
String name = m. getName();
System.out.print(" ••);
//print modifiers, return type and method name
String modifiers = Modi fier.toString(m.getModi fiers());
if (modifiers.lengthO > 0) System.out.print(modifiers + n ");
System. out.print(retType. getName() +"" + name +"(");
//print parameter types
Class[] paramTypes = m.getParamete「Types();
for (int j = 0; j < paramTypes.length; j++)
{
if (j > 0) System.out.print(" f ");
System.out.print(paramTypes[j].getName());
}
System.out.println(") ;") ;
public static void printFields(Cl ass cl)
{
Field[] fields = cl.getDedaredFieldsO;
for (Field f : fields)
{
Class type = f.getTypeO;
String name = f.getName();
System.out.print(" ");
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.lengthO > 0) System,out.print(modifiers + " ";)
System.out.println(type.getName() + " " + name +";");
}
这个程序可以分析 Java 解释器能够加载的任何类, 而不仅仅是编译程序时可以使用的类。
java.lang.Class 1.0
• Field[] getFields() 1.1
• Filed[] getDeclaredFie1ds() 1.1
getFields 方法将返回一个包含 Field 对象的数组, 这些对象记录了这个类或其超类的公有域。getDeclaredField 方法也将返回包含 Field 对象的数组, 这些对象记录了这个类的全部域。 如果类中没有域, 或者 Class 对象描述的是基本类型或数组类型, 这些方法将返回一个长度为 0 的数组。
• Method[] getMethods() 1.1
• Method[] getDeclareMethods() 1.1
返回包含 Method 对象的数组:getMethods 将返回所有的公有方法, 包括从超类继承来的公有方法;getDeclaredMethods 返回这个类或接口的全部方法, 但不包括由超类继承了的方法。
• Constructor[] getConstructors() 1.1
• Constructor[] getDeclaredConstructors() 1.1
返回包含 Constructor 对象的数组, 其中包含了 Class 对象所描述的类的所有公有构造(getConstructors) 或所有构造器(getDeclaredConstructors)。
java.lang.reflect.Field 1.1
java.lang.reflect.Method 1.1
java.lang.reflect.Constructor 1.1
• Class getDeclaringClass( )
返冋一个用于描述类中定义的构造器、 方法或域的 Class 对象。
• Class[ ] getExceptionTypes ( ) ( 在 Constructor 和 Method 类 中)
返回一个用于描述方法抛出的异常类型的 Class 对象数组。
• int getModifiers( )
返回一个用于描述构造器、 方法或域的修饰符的整型数值。使用 Modifier 类中的这个方法可以分析这个返回值。
• String getName( )
返冋一个用于描述构造器、 方法或域名的字符串。
• Class[ ] getParameterTypes ( ) ( 在 Constructor 和 Method 类 中)
返回一个用于描述参数类型的 Class 对象数组。
• Class getReturnType( ) ( 在 Method 类 中)
返回一个用于描述返回类型的 Class 对象。
java.lang.reflect.Modifier 1.1
4.在运行时使用反射分析对象
从前面一节中, 已经知道如何查看任意对象的数据域名称和类型:
•获得对应的 Class 对象。
•通过 Class 对象调用 getDeclaredFields。
本节将进一步查看数据域的实际内容。当然, 在编写程序时, 如果知道想要査看的域名和类型, 查看指定的域是一件很容易的事情。而利用反射机制可以查看在编译时还不清楚的对象域。
查看对象域的关键方法是 Field类中的 get 方法。 如果 f 是一个 Field 类型的对象(例如,通过 getDeclaredFields 得到的对象,) obj 是某个包含 f 域的类的对象,f.get(obj) 将返回一个对象,其值为 obj 域的当前值。
Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Class cl = harry.getClass0;
// the class object representing Employee
Field f = cl .getDeclaredFieldC'name"):
// the name field of the Employee class
Object v = f.get(harry);
// the value of the name field of the harry object, i .e., the String object "Harry Hacker"
反射机制的默认行为受限于 Java 的访问控制。然而, 如果一个 Java 程序没有受到安全管理器的控制, 就可以覆盖访问控制。 为了达到这个目的, 需要调用 Field、 Method 或Constructor 对象的 setAccessible 方法。例如:
f.setAtcessible(true); // now OK to call f.get(harry) ;
setAccessible 方法是 AccessibleObject 类中的一个方法, 它是 Field、 Method 和 Constructor类的公共超类。这个特性是为调试、 持久存储和相似机制提供的。
get 方法还有一个需要解决的问题。
name 域是一个 String, 因此把它作为 Object 返回没有什么问题。但是, 假定我们想要查看 salary 域。它属于 double 类型, 而 Java 中数值类型不是对象。
要想解决这个问题, 可以使用 Field 类中的 getDouble方法,也可以调用 get方法, 此时, 反射机制将会自动地将这个域值打包到相应的对象包装器中, 这里将打包成Double。
当然,可以获得就可以设置。 调用 f.set(obj ,value)
可以将 obj 对象的 f 域设置成新值。
程序清单 5-14 显示了如何编写一个可供任意类使用的通用 toString方法。 其中使用getDeclaredFileds 获得所有的数据域, 然后使用 setAccessible 将所有的域设置为可访问的。 对于每个域,获得了名字和值。程序清单 5-14 递归调用 toString方法, 将每个值转换成字符串。
泛型 toString方法需要解释几个复杂的问题。循环引用将有可能导致无限递归。因此,ObjectAnalyzer 将记录已经被访问过的对象。 另外, 为了能够査看数组内部, 需要采用一种不同的方式。
程序清单 5-14 objectAnalyzer/ObjectAnalyzerTest.java
package objectAnalyzer;
import java.util.ArrayList;
public class ObjectAnal yzerTest
{
public static void main(String[] args)
{
ArrayList<Integer> squares = new ArrayLi st<>() ;
for (inti = 1;i <= 5;i++)
squares.add(i * i);
System,out.println(new ObjectAnalyzer().toString(squares));
}
}
java.Iang.reflect.AccessibleObject 1.2
• void setAccessible(boolean flag)
为反射对象设置可访问标志。 flag 为 true 表明屏蔽 Java 语言的访问检查,使得对象的私有属性也可以被査询和设置。
• boolean isAccessible( )
返回反射对象的可访问标志的值。
• static void setAccessible(AccessibleObject[ ] array,boolean flag)
是一种设置对象数组可访问标志的快捷方法。
java.lang.Class 1.1
•Field getField( String name )
•Field[] getField()
返回指定名称的公有域, 或包含所有域的数组
• Field getDeclaredField( String name )
•Field[] getDeclaredFields()
返回类中声明的给定名称的域, 或者包含声明的全部域的数组。
java.Iang.reflect.Field 1.1
•Object get( Object obj )
返回 obj 对象中用 Field 对象表示的域值。
• void set(Object obj ,Object newValue)
用一个新值设置 Obj 对象中Field 对象表示的域。
5.使用反射编写泛型数组代码
java.lang.reflect 包中的 Array 类允许动态地创建数组。 例如, 将这个特性应用到 Array类中的 copyOf 方法实现中, 应该记得这个方法可以用于扩展已经填满的数组。
Employee[] a = new Employee[100]:
// array is full
a = Arrays.copyOf(a, 2 * a.length);
上面代码使得本来长为 100 的数组变为长 200。
下面展示将 Employee[ ] 数组转换为 Object[ ] 数组:
public static Object口 badCopyOf(Object[] a, int newLength) // not useful
{
Object[] newArray = new Object[newlength]:
System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
return newArray;
}
一个对象数组不能转换成雇员数组(Employee[ ])。 如果这样做, 则在运行时 Java 将会产生 ClassCastException 异常。
将一个 Employee[ ]临时地转换成 Object[ ] 数组, 然后再把它转换回来是可以的,但一 从开始就是 Object[ ] 的数组却永远不能转换成 Employe [ ] 数组。
为了编写这类通用的数组代码, 需要能够创建与原数组类型相同的新数组。为此, 需要 java.lang.reflect 包中 Array 类的一些方法。其中最关键的是 Array类中的静态方法 newlnstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的长度。
Object newArray = Array.newlnstance(componentType, newLength) ;
可以通过调用 Array.getLength(a) 获得数组的长度, 也可以通过 Array类的静态 getLength方法的返回值得到任意数组的长度。而要获得新数组元素类型,就需要进行以下工作:
1 ) 首先获得 a 数组的类对象。
2 ) 确认它是一个数组。
3 ) 使用 Class 类(只能定义表示数组的类对象)的 getComponentType 方法确定数组对应的类型。
为什么 getLength 是 Array 的方法,而 getComponentType 是 Class 的方法呢? 我们也不清楚。反射方法的分类有时确实显得有点古怪。下面是这段代码:
public static Object goodCopyOf(Object a, int newLength)
{
Class cl = a.getClass();
if (!cl.isArray()) return null ;
Class componentType = cl .getComponentType0;
int length = Array.getLength(a);
Object newArray = Array.newlnstance(componentType, newLength):
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength)) ;
return newArray;
}
请注意,这个 CopyOf 方法可以用来扩展任意类型的数组, 而不仅是对象数组。
int[] a = { 1,2, 3, 4, 5 };
a = (int[]) goodCopyOf(a, 10) ;
为了能够实现上述操作,应该将 goodCopyOf 的参数声明为 Object 类型,.而不要声明为对象型数组(Object[])。整型数组类型 int[] 可以被转换成 Object ,但不能转换成对象数组。
6.调用任意方法
为了能够看到方法指针的工作过程, 先回忆一下利用 Field 类的 get 方法查看对象域的过程。与之类似, 在 Method 类中有一个 invoke 方法, 它允许调用包装在当前 Method 对象中的方法。invoke 方法的签名是:
Object invoke(Object obj, Object... args)
第一个参数是隐式参数, 其余的对象提供了显式参数。
对于静态方法,第一个参数可以被忽略, 即可以将它设置为 null。
例如, 假设用 ml 代表 Employee 类的 getName 方法,下面这条语句显示了如何调用这个方法:
String n = (String) ml.invoke(harry)
如果返回类型是基本类型, invoke 方法会返回其包装器类型。 例如, 假设 m2 表示Employee 类的 getSalary 方法, 那么返回的对象实际上是一个 Double, 必须相应地完成类型转换。可以使用自动拆箱将它转换为一个 double:
double s = (Double) m2,invoke(harry);
如何得到 Method 对象呢? 当然, 可以通过调用 getDeclareMethods 方法, 然后对返回的 Method 对象数组进行查找, 直到发现想要的方法为止。 也可以通过调用 Class 类中的getMethod方法得到想要的方法。它与 getField 方法类似。getField 方法根据表示域名的字符串,返回一个 Field 对象。然而, 有可能存在若干个相同名字的方法, 因此要格外小心,以确保能够准确地得到想要的那个方法。有鉴于此, 还必须提供想要的方法的参数类型。getMethod 的签名是:
Method getMethod(String name, Class... parameterTypes)
例如, 下面说明了如何获得 Employee 类的 getName 方法和 raiseSalary 方法的方法指针。
Method ml = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);
java.Iang.reflect.Method 1.1
•public Object invokeCObject implicitParameter,Object[] explicitParamenters)
调用这个对象所描述的方法, 传递给定参数,并返回方法的返回值。对于静态方法,把 null 作为隐式参数传递。 在使用包装器传递基本类型的值时, 基本类型的返回值必须是未包装的。