反射

通过 Java 反射机制,可以在程序中访问已经被装载到 JVM 中的 Java 对象的描述,实现访问、检测和修改描述 Java 对象本身信息的功能。Java 反射机制的功能十分强大,在 java.lang.reflect 包中提供了对该功能的支持。

众所周知,所有 Java 类均继承了 Object 类,在 Object 类中定义了一个 getClass() 方法,该方法返回一个类型为 Class 的对象。例如下面的代码:

JTextField textField = new JTextField();  // 创建 JTextField 对象
Class textFieldC = textField.getClass();  // 获取 Class 对象

利用 Class 类的对象 textFieldC,可以访问用来返回该对象的 textField 对象的描述信息。可以访问的主要描述信息如表16.1所示。

在通过 getFields() 方法和 getMethods() 方法依次获得权限为 public 的成员变量和方法时,将包含从超类中继承的成员变量和方法;而通过 getDeclaredFields() 方法和 getDeclaredMethods() 方法只是获得在本类中定义的所有成员变量和方法。

image 2024 03 05 17 58 41 232
Figure 1. 表16.1 通过反射可访问的主要描述信息

访问构造方法

在通过下列一组方法访问构造方法时,将返回 Constructor 类型的对象或数组。每个 Constructor 对象代表一个构造方法,利用 Constructor 对象可以操纵相应的构造方法:

  • getConstructors()。

  • getConstructor(Class<?>…​parameterTypes)。

  • getDeclaredConstructors()。

  • getDeclaredConstructor(Class<?>…​parameterTypes)。

如果是访问指定的构造方法,则需要根据该构造方法的入口参数的类型来访问。例如,访问一个入口参数类型依次为 String 型和 int 型的构造方法,通过下面两种方式均可实现:

objectClass.getDeclaredConstructor(String.class, int.class);
objectClass.getDeclaredConstructor(new Class[] { String.class, int.class });

Constructor 类中提供的常用方法如表16.2所示。

image 2024 03 05 18 00 35 844
Figure 2. 表16.2 Constructor类的常用方法

通过 java.lang.reflect.Modifier 类可以解析出 getModifiers() 方法的返回值所表示的修饰符信息,在该类中提供了一系列用来解析的静态方法,既能查看是否被指定的修饰符修饰,又能以字符串的形式获得所有修饰符。Modifier 类常用静态方法如表16.3所示。

image 2024 03 05 18 01 12 841
Figure 3. 表16.3 Modifier类中的常用解析方法

例如,判断对象 constructor 代表的构造方法是否被 private 修饰,以及以字符串形式获得该构造方法的所有修饰符的典型代码如下:

int modifiers = constructor.getModifiers();
boolean isEmbellishByPrivate = Modifier.isPrivate(modifiers);
String embellishment = Modifier.toString(modifiers);

【例16.1】反射一个类的所有构造方法(实例位置:资源包\TM\sl\16\1)

在 com.mr 包下创建一个 Demo1 类,在该类中声明一个 String 型成员变量和 3 个 int 型成员变量,并提供 3 个构造方法。具体代码如下:

package com.mr;
public class Demo1 {
	String s;
	int i, i2, i3;
	private Demo1() {
	}
	protected Demo1(String s, int i) {
		this.s = s;
		this.i = i;
	}
	public Demo1(String... strings) throws NumberFormatException {
		if (0 < strings.length)
		   i = Integer.valueOf(strings[0]);
		if (1 < strings.length)
		   i2 = Integer.valueOf(strings[1]);
		if (2 < strings.length)
		   i3 = Integer.valueOf(strings[2]);
	}
	public void print() {
		System.out.println("s=" + s);
		System.out.println("i=" + i);
		System.out.println("i2=" + i2);
		System.out.println("i3=" + i3);
	}
}

然后编写测试类 ConstructorDemo1,在该类中通过反射访问 com.mr.Demo1 类中的所有构造方法,并将该构造方法是否允许带有可变数量的参数、入口参数类型和可能抛出的异常类型信息输出到控制台中。具体代码如下:

import java.lang.reflect.Constructor;

import com.mr.Demo;

public class ConstructorDemo {
	public static void main(String[] args) {
		Demo d1 = new Demo("10", "20", "30");
		Class<? extends Demo> demoClass = d1.getClass();
		// 获得所有构造方法
		Constructor[] declaredConstructors = demoClass.getDeclaredConstructors();
		for (int i = 0; i < declaredConstructors.length; i++) { // 遍历构造方法
			Constructor<?> constructor = declaredConstructors[i];
			System.out.println("查看是否允许带有可变数量的参数:" + constructor.isVarArgs());
			System.out.println("该构造方法的入口参数类型依次为:");
			Class[] parameterTypes = constructor.getParameterTypes(); // 获取所有参数类型
			for (int j = 0; j < parameterTypes.length; j++) {
				System.out.println(" " + parameterTypes[j]);
			}
			System.out.println("该构造方法可能抛出的异常类型为:");
			// 获得所有可能抛出的异常信息类型
			Class[] exceptionTypes = constructor.getExceptionTypes();
			for (int j = 0; j < exceptionTypes.length; j++) {
				System.out.println(" " + exceptionTypes[j]);
			}
			Demo d2 = null;
			try { // 如果该成员变量的访问权限为private,则抛出异常,即不允许访问
				if (i == 2) // 通过执行默认没有参数的构造方法创建对象
					d2 = (Demo) constructor.newInstance();
				else if (i == 1)
					// 通过执行具有两个参数的构造方法创建对象
					d2 = (Demo) constructor.newInstance("7", 5);
				else { // 通过执行具有可变数量参数的构造方法创建对象
					Object[] parameters = new Object[] { new String[] { "100", "200", "300" } };
					d2 = (Demo) constructor.newInstance(parameters);
				}
			} catch (Exception e) {
				System.out.println("在创建对象时抛出异常,下面执行setAccessible()方法");
				constructor.setAccessible(true); // 设置为允许访问
			}
			if (d2 != null) {
				d2.print();
				System.out.println();
			}
		}

	}
}

运行结果如下:

image 2024 03 05 18 07 08 087

当反射无参构造方法时将输出所有属性的默认值,当反射有参数的构造方法时将输出所有属性被赋予的相应值。

访问成员变量

在通过下列一组方法访问成员变量时,将返回 Field 类型的对象或数组。每个 Field 对象代表一个成员变量,利用 Field 对象可以操纵相应的成员变量:

  • getFields()。

  • getField(String name)。

  • getDeclaredFields()。

  • getDeclaredField(String name)。

如果是访问指定的成员变量,则可以通过该成员变量的名称来访问。例如,访问一个名称为 birthday 的成员变量,访问方法如下:

object. getDeclaredField("birthday");

Field 类中提供的常用方法如表16.4所示。

image 2024 03 05 18 08 46 703
Figure 4. 表16.4 Field类的常用方法

【例16.2】反射一个类的所有成员变量(实例位置:资源包\TM\sl\16\2)

在 com.mr 包下创建一个 Demo2 类,在该类中依次声明一个 int、float、boolean 和 String 型的成员变量,并将它们设置为不同的访问权限。具体代码如下:

package com.mr;

public class Demo2 {
	int i;
	public float f;
	protected boolean b;
	private String s;
}

然后通过反射访问 com.mr.Demo2 类中的所有成员变量,将成员变量的名称和类型信息输出到控制台中,并分别将各个成员变量在修改前后的值输出到控制台中。关键代码如下:

import java.lang.reflect.Field;
import com.mr.Demo2;

public class FieldDemo {
	public static void main(String[] args) {
		Demo2 example = new Demo2();
		Class exampleC = example.getClass();
		// 获得所有成员变量
		Field[] declaredFields = exampleC.getDeclaredFields();
		for (int i = 0; i < declaredFields.length; i++) { // 遍历成员变量
			Field field = declaredFields[i];
			System.out.println("名称为:" + field.getName()); // 获得成员变量名称
			Class fieldType = field.getType(); // 获得成员变量类型
			System.out.println("类型为:" + fieldType);
			boolean isTurn = true;
			while (isTurn) {
				// 如果该成员变量的访问权限为private,则抛出异常,即不允许访问
				try {
					isTurn = false;
					// 获得成员变量值
					System.out.println("修改前的值为:" + field.get(example));
					if (fieldType.equals(int.class)) { // 判断成员变量的类型是否为int型
						System.out.println("利用方法setInt()修改成员变量的值");
						field.setInt(example, 168); // 为int型成员变量赋值
					} else if (fieldType.equals(float.class)) { // 判断成员变量的类型是否为float型
						System.out.println("利用方法setFloat()修改成员变量的值");
						field.setFloat(example, 99.9F); // 为float型成员变量赋值
						// 判断成员变量的类型是否为boolean型
					} else if (fieldType.equals(boolean.class)) {
						System.out.println("利用方法setBoolean()修改成员变量的值");
						field.setBoolean(example, true); // 为boolean型成员变量赋值
					} else {
						System.out.println("利用方法set()修改成员变量的值");
						field.set(example, "MWQ"); // 可以为各种类型的成员变量赋值
					}
					// 获得成员变量值
					System.out.println("修改后的值为:" + field.get(example));
				} catch (Exception e) {
					System.out.println("在设置成员变量值时抛出异常," + "下面执行setAccessible()方法!");
					field.setAccessible(true); // 设置为允许访问
					isTurn = true;
				}
			}
			System.out.println();
		}
	}
}

运行结果如下:

image 2024 03 05 18 10 45 704

通过这个结果可以看出,在反射权限为 private 或 protected 的成员变量时,需要执行 setAccessible() 方法,并将入口参数设为 true,否则不允许访问。

访问成员方法

在通过下列一组方法访问成员方法时,将返回 Method 类型的对象或数组。每个 Method 对象代表一个方法,利用 Method 对象可以操纵相应的方法:

  • getMethods()。

  • getMethod(String name, Class<?>…​parameterTypes)。

  • getDeclaredMethods()。

  • getDeclaredMethod(String name, Class<?>…​parameterTypes)。

如果是访问指定的方法,需要根据该方法的名称和入口参数的类型来访问。例如,访问一个名称为 print、入口参数类型依次为 String 型和 int 型的方法,通过下面两种方式均可实现:

objectClass.getDeclaredMethod("print", String.class, int.class);
objectClass.getDeclaredMethod("print", new Class[] {String.class, int.class});

Method 类中提供的常用方法如表16.5所示。

image 2024 03 05 18 13 37 966
Figure 5. 表16.5 Method类的常用方法

【例16.3】反射一个类的所有成员方法(实例位置:资源包\TM\sl\16\3)

在 com.mr 包下创建一个 Demo3 类,并编写 4 个成员方法。具体代码如下:

package com.mr;

public class Demo3 {
	static void staticMethod() {
		System.out.println("执行staticMethod()方法");
	}

	public int publicMethod(int i) {
		System.out.println("执行publicMethod()方法");
		return i * 100;
	}

	protected int protectedMethod(String s, int i) throws NumberFormatException {
		System.out.println("执行protectedMethod()方法");
		return Integer.valueOf(s) + i;
	}

	private String privateMethod(String... strings) {
		System.out.println("执行privateMethod()方法");
		StringBuffer stringBuffer = new StringBuffer();
		for (int i = 0; i < strings.length; i++) {
			stringBuffer.append(strings[i]);
		}
		return stringBuffer.toString();
	}
}

然后通过反射访问 com.mr.Demo3 类中的所有方法,将各个方法的名称、入口参数类型、返回值类型等信息输出到控制台中,并执行部分方法。关键代码如下:

import java.lang.reflect.*;

import com.mr.Demo3;

public class MethondDemo {
	public static void main(String[] args) {
		Demo3 demo = new Demo3();
		Class demoClass = demo.getClass();
		// 获得所有方法
		Method[] declaredMethods = demoClass.getDeclaredMethods();
		for (int i = 0; i < declaredMethods.length; i++) {
			Method method = declaredMethods[i]; // 遍历方法
			System.out.println("名称为:" + method.getName()); // 获得方法名称
			System.out.println("是否允许带有可变数量的参数:" + method.isVarArgs());
			System.out.println("入口参数类型依次为:");
			// 获得所有参数类型
			Class[] parameterTypes = method.getParameterTypes();
			for (int j = 0; j < parameterTypes.length; j++) {
				System.out.println(" " + parameterTypes[j]);
			}
			// 获得方法返回值类型
			System.out.println("返回值类型为:" + method.getReturnType());
			System.out.println("可能抛出的异常类型有:");
			// 获得方法可能抛出的所有异常类型
			Class[] exceptionTypes = method.getExceptionTypes();
			for (int j = 0; j < exceptionTypes.length; j++) {
				System.out.println(" " + exceptionTypes[j]);
			}
			boolean isTurn = true;
			while (isTurn) {
				try {// 如果该方法的访问权限为private,则抛出异常,即不允许访问
					isTurn = false;
					if ("staticMethod".equals(method.getName()))
						method.invoke(demo); // 执行没有入口参数的方法
					else if ("publicMethod".equals(method.getName()))
						System.out.println("返回值为:" + method.invoke(demo, 168)); // 执行方法
					else if ("protectedMethod".equals(method.getName()))
						System.out.println("返回值为:" + method.invoke(demo, "7", 5)); // 执行方法
					else if ("privateMethod".equals(method.getName())) {
						Object[] parameters = new Object[] { new String[] { "M", "W", "Q" } }; // 定义二维数组
						System.out.println("返回值为:" + method.invoke(demo, parameters));
					}
				} catch (Exception e) {
					System.out.println("在执行方法时抛出异常," + "下面执行setAccessible()方法!");
					method.setAccessible(true); // 设置为允许访问
					isTurn = true;
				}
			}
			System.out.println();
		}
	}
}

运行结果如下,程序将依次访问 staticMethod()、publicMethod()、protectedMethod() 和 privateMethod() 方法:

image 2024 03 05 18 15 44 589

在反射中执行具有可变数量的参数的构造方法时,需要将入口参数定义成二维数组。

编程训练(答案位置:资源包\TM\sl\16\编程训练)

【训练1】反射方法 利用反射实现通用扩展数组长度的方法。

【训练2】反射私有变量 为了保证面向对象的封装特性,通常会将类的成员变量设置成私有的,然后提供对应的 get 和 set 方法。对于非内部类而言,只能使用 get 和 set 方法来操作该变量。然而利用反射机制,就可以在运行时修改类的私有变量。请通过简单的 Student 类来演示反射的这种用法,效果图如图16.1所示。

image 2024 03 05 18 16 48 067
Figure 6. 图16.1 实现效果图