反射


反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。

在Java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法?

Java 反射机制主要提供了以下功能

  • 在运行时(动态编译)判断任意一个对象所属的类。
  • 在运行时构造任意一个类的对象。
  • 在运行时判断任意一个类所具有的成员变量和方法。
  • 在运行时调用任意一个对象的方法和属性。

这种动态获取信息以及动态调用对象的方法的功能称为 Java 语言的反射机制

1. Class 类

在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。 这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。

可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class,这个名字很容易让人混淆。

常用方法

Object.getClass()

Object 类中的 getClass( ) 方法将会返回一个 Class 类型的实例

Employee e;
Class c1 = e.getClass();

如同用一个 Employee 对象表示一个特定的雇员属性一样, 一个 Class 对象将表示一个特定类的属性。最常用的 Class 方法是 getName, 这个方法将返回类的名字。例如,下面这条语句:

System.out.println(e.getClass().getName()); // Employee

如果类在一个包里,包的名字也作为类名的一部分

Random generator = new Random();
Class c1 = generator.getClass();
String name = c1.getName(); // name is set to "java.util .Random"

Class.forName()

还可以调用静态方法 forName 获得类名对应的 Class 对象

String className = "java.util.Random";
Class cl = Class.forName(className);

如果类名保存在字符串中, 并可在运行中改变, 就可以使用这个方法。当然, 这个方法只有在 className 是类名或接口名时才能够执行。否则,forName 方法将抛出一个 checked exception ( 已检查异常)。无论何时使用这个方法, 都应该提供一个异常处理器( exception handler )

在启动时, 包含 main 方法的类被加载。它会加载所有需要的类。这些被加载的类又要加载它们需要的类, 以此类推。对于一个大型的应用程序来说, 这将会消耗很多时 间, 用户会因此感到不耐烦。可以使用下面这个技巧给用户一种启动速度比较快的幻觉。 不过,要确保包含 main 方法的类没有显式地引用其他的类。

  • 首先,显示一个启动画面;
  • 然后,通过调用 Class.forName 手工地加载其他的类。

T.class()

获得 Class类对象的第三种方法非常简单。如果 T 是任意的 Java 类型(或 void 关键字,) T.class 将代表匹配的类对象。例如:

Class cl1 = Random.class; // if you import java.util 
Class cl2 = int.class; 
Class cl3 = Double[].class; 

请注意,一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如, int 不是类, 但 int.class 是一个 Class 类型的对象。

虚拟机为每个类型管理一个 Class 对象。 因此,可以利用 == 运算符实现两个类对象比较的操作。 例如,

if(e.getClass() == Employee.getClass())

还有一个很有用的方法 newInstance(), 可以用来动态地创建一个类的实例。例如,

e.getClass().newInstance(); 

创建了一个与 e 具有相同类类型的实例。 newlnstance方法调用默认的构造函数(无参构造函数)初始化新创建的对象。如果这个类没有默认的构造函数, 就会抛出一个异常

forNamenewlnstance 配合起来使用, 可以根据存储在字符串中的类名创建一个对象 :

String s = "java.util.Random"; 
Object m = Class.forName(s).newInstance();

2. 利用反射分析类的能力

下面简要地介绍一下反射机制最重要的内容 — 检查类的结构

java.lang.reflect 包中有三个类 FieldMethodConstructor 分别用于描述类的域、 方法和构造器。

这三个类都有一个叫做 getName 的方法, 用来返回项目的名称。

Field 类有一 个 getType 方法, 用来返回描述域所属类型的 Class 对象。

Method Constructor 类有能够报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。

这 3 个类还有一个叫 做 getModifiers 的方法, 它将返回一个整型数值,用不同的位开关描述 public 和 static 这样 的修饰符使用状况。

另外, 还可以利用 java.lang.reflect 包中的 Modifier类的静态方法分析 getModifiers 返回的整型数值。例如, 可以使用 Modifier 类中的 isPublicisPrivate isFinal 判断方法或构造器是否是 public、 private 或 final。 可以利用 Modifier.toString 方法将修饰符打印出来。

Class 类的 getFields、 ``getMethodsgetConstructors` 方法将分别返回类提供的 public 域、 方法和构造器数组, 其中包括超类的公有成员。

Class 类的 getDeclareFieldsgetDeclareMethodsgetDeclaredConstructors 方法将分别返回类中声明的全部域、 方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。

API 如下:

image-1662340456659
image-1662340467514

3. 在运行时使用反射分析对象

从前面一节中, 已经知道如何查看任意对象的数据域名称和类型:

  • 获得对应的 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.getClass(); 
Field f = cl.getDeclaredField("name")
Object v = f.get(harry);

实际上,这段代码存在一个问题。由于 name 是一个私有域, 所以 get 方法将会抛出一个 IllegalAccessException。只有利用 get 方法才能得到可访问域的值。除非拥有访问权限,否则 Java 安全机制只允许査看任意对象有哪些域, 而不允许读取它们的值。

为了达到这个目的, 需要调用 Field、 Method 或 Constructor 对象的 setAccessible 方法。例如,

f.setAtcessible(true); // now OK to call f.get(harry);

4. 调用任意方法

在 C 和 C++ 中, 可以从函数指针执行任意函数。从表面上看, Java 没有提供方法指针, 即将一个方法的存储地址传给另外一个方法, 以便第二个方法能够随后调用它。事实上, Java 的设计者曾说过:方法指针是很危险的,并且常常会带来隐患。他们认为 Java 提供的 接口(interface ) (将在下一章讨论)是一种更好的解决方案。然而, 反射机制允许你调用任意方法

在 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方法得到想要的方法。然而, 有可能存在若干个相同名字的方法,因此要格外小心, 以确保能够准确地得到想要的那个方法。有鉴于此,还必须提供想要的方法的参数类型。 getMethod 的签名是:

Method getMethod(String name, Class... parameterTypes)

例如, 下面说明了如何获得 Employee 类的 getName 方法和 raiseSalary 方法的方法指针。

Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary",double.class);

5. 代码实例

创建一个我们要使用反射操作的类 TargetObject

package com.smallbeef;

public class TargetObject {
    private String value;

    public TargetObject() {
        value = "Java";
    }

    public void publicMethod(String s) {
        System.out.println("I love " + s);
    }

    private void privateMethod() {
        System.out.println("value is " + value);
    }
}

使用反射操作这个类的方法以及参数

package com.smallbeef;

import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javax.print.attribute.standard.Fidelity;

public class Demo {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
            InstantiationException, InvocationTargetException, NoSuchFieldException {

        // 获取 TargetObject 类的 Class 对象并创建 TargetObject 类实例
        Class targetClass = Class.forName("com.smallbeef.TargetObject");
        TargetObject targetObject = (TargetObject)targetClass.newInstance();
                
        // 获取所有类中所有定义的方法
        Method[] methods = targetClass.getDeclaredMethods();
        for(Method m: methods){
            System.out.println(m.getName());
        }

        // 调用 publicMethod 方法 (有参)
        Method publicMethod = targetClass.getDeclaredMethod("publicMethod", String.class); // 获取指定方法
        publicMethod.invoke(targetObject, "Computer"); // 传入参数并调用

       // 获取指定参数并对参数进行修改
        Field field = targetClass.getDeclaredField("value");
        // 为了对类中的参数进行修改我们取消安全检查
        field.setAccessible(true);
        field.set(targetObject, "C++");

        // 调用 private 方法 (无参)
        Method privateMethod = targetClass.getDeclaredMethod("privateMethod");
        // 为了调用 private 方法我们取消安全检查
        privateMethod.setAccessible(true);
        privateMethod.invoke(targetObject);
    }
}

输出结果:

publicMethod

privateMethod

I love Computer

value is C++

6. 反射机制优缺点

优点: 动态编译。运行期类型的判断,动态加载类,提高代码灵活度。

缺点

  • 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。
  • 安全问题:让我们可以动态操作改变类的属性同时也增加了类的安全隐患。

7. 反射的应用场景

反射是框架设计的灵魂。

在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。

举例:

  • 我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;
  • Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;
  • 动态配置实例的属性(application.xml/yaml/properties等属性的获取)