Java基础丨反射

前言

反射(Reflection),它是一种专门为静态语言提供的技术(反射并不是 Java 独有的,许多编程语言都提供了反射功能,例如:Go),用于在程序运行时动态地改变程序的状态和行为。
在程序运行状态中,给定任意一个对象都可以调用这个对象的属性、方法、构造方法,这种动态的获取类的信息和调用对象的方法的功能称之为 Java 的反射机制。

了解反射

主要功能

Java 反射机制主要提供了以下功能,这些功能都位于 java.lang.reflect 包。

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

    应用场景

  • 很多框架(例如:Spring、JDBC)都是配置化的(例如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。
  • 注解作为一种标记,常常用于代替冗余复杂的配置文件(XML、properties),而反射是所有注解实现的原理。
  • 可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。

    使用反射

    众所周知,所有 Java 类均继承了 Object 类,在 Object 类中定义了一个 getClass() 方法,该方法返回一个 Class 类型的对象,而反射机制的实现主要通过操作 java.lang.Class 类。

java.lang.reflect 包的核心接口和类如下:

  • Array 类:该类提供动态地生成和访问 Java 数组的方法。
  • Constructor 类:提供一个类的构造函数的信息以及访问类的构造函数的接口。
  • Field 类:提供一个类的域的信息以及访问类的域的接口。
  • Member 接口:反映关于单个成员(字段或方法)或构造函数的标识信息。
  • Method 类:提供一个类的方法的信息以及访问类的方法的接口。
  • Modifier 类:提供了 static 方法和常量,对类和成员访问修饰符进行解码。
  • Proxy 类:提供动态地生成代理类和类实例的静态方法。

在使用 Java 反射机制时,主要步骤包括:

  1. 获取目标类型的 Class 对象;
  2. 通过 Class 对象分别获取 Constructor 类对象、Method 类对象、Field 类对象;
  3. 通过 Constructor 类对象、Method 类对象、Field 类对象分别获取类的构造函数、方法、属性的具体信息,再进行后续操作。

获取 Class 对象

获取 Class 对象有三种方法。以下演示:

public class ReflectClassTest {
    public static void main(String[] args) throws ClassNotFoundException {
        // 1.通过字符串获取 Class 对象,这个字符串必须带上完整的路径名
        Class clazz1 = Class.forName("myHomeWork.Exp5.Hero3");
        System.out.println(clazz1.getCanonicalName());

        // 2.通过类的 class 属性
        Class clazz2 = Hero3.class;
        System.out.println(clazz2.getCanonicalName());

        // 3.通过对象的 getClass() 函数
        Hero3 hero3Object = new Hero3();
        Class clazz3 = hero3Object.getClass();
        System.out.println(clazz3.getCanonicalName());

        // 判断获取的 Class 对象是否相等
        System.out.println(clazz1 == clazz2);
        System.out.println(clazz2 == clazz3);
    }
}

第一种方法应该不陌生,例如 JDBC 加载数据库驱动 Class.forName("oracle.jdbc.driver.OracleDriver"),就使用类的完全限定名来反射对象的类。这种方法也最常用。
第二种方法需要导入类的包,依赖性太强,不导包就会抛出编译错误。
第三种方法都已经创建了一个对象,还需要反射吗?
最后的判断结果都是 true,说明在运行期间,一个类只有一个 Class 对象产生。

获取构造方法

Class 对象提供以下方法获取对象的构造方法(Constructor):

  • getConstructor:返回类的特定 public 构造方法。参数为方法参数对应 Class 的对象。
  • getDeclaredConstructor:返回类的特定构造方法。参数为方法参数对应 Class 的对象。
  • getConstructors:返回类的所有 public 构造方法。
  • getDeclaredConstructors:返回类的所有构造方法。

获取一个 Constructor 对象后,可以用 newInstance 方法来创建类实例。
先新建一个 Worker 类,其中设置了 6 个构造方法。

public class Worker {
    // 默认的构造方法
    Worker(String str) {
        System.out.println("默认的构造方法 s = " + str);
    }

    // 无参构造方法
    public Worker() {
        System.out.println("调用了公有、无参构造方法执行了");
    }

    // 有一个参数的构造方法
    public Worker(char gender) {
        System.out.println("性别:" + gender);
    }

    // 有多个参数的构造方法
    public Worker(String name, int age) {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }

    // 受保护的构造方法
    protected Worker(boolean n) {
        System.out.println("受保护的构造方法,n = " + n);
    }

    // 私有的构造方法
    private Worker(int age) {
        System.out.println("私有的构造方法,年龄:" + age);
    }
}

以下演示:

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author yyt
 */
public class ReflectConstructorTest {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 1.加载Class对象
        Class<?> clazz = Class.forName("myHomeWork.Exp5.Worker");

        // 2.获取所有公有的构造方法
        System.out.println("-----所有公有构造方法-----");
        Constructor<?>[] conArray = clazz.getConstructors();
        for(Constructor c : conArray){
            System.out.println(c);
        }

        System.out.println("-----所有的构造方法-----");
        conArray = clazz.getDeclaredConstructors();
        for(Constructor c : conArray){
            System.out.println(c);
        }

        System.out.println("-----获取公有、无参的构造方法-----");
        Constructor con = clazz.getConstructor(null);
        System.out.println("con = " + con);
        // 调用构造方法创建实例
        Object obj = con.newInstance();

        System.out.println("-----获取私有构造方法,并调用-----");
        con = clazz.getDeclaredConstructor(int.class);
        System.out.println(con);
        // 暴力访问
        con.setAccessible(true);
        // 创建实例
        obj = con.newInstance( 22);
    }
}

以上设置了 con.setAccessible(true) 这一句,这是使用 Field 类、Method 类、Constructor 类对象的 setAccessible(flag) 来绕开访问限制,当 flagtrue 时 ,表示已屏蔽 Java 语言的访问检查,使得反射可以访问、修改对象的私有属性、方法。

获取成员变量

Class 对象提供以下方法获取对象的成员(Field):

  • getFiled:根据名称获取公有的(public)类成员。
  • getFields:获取所有公有的(public)类成员。
  • getDeclaredField:根据名称获取已声明的类成员。但不能得到其父类的类成员。
  • getDeclaredFields:获取所有已声明的类成员。

新建一个 Person 类,其中设置不同访问权限的属性:公有、受保护、默认、私有。

public class Person {
    // 公有属性
    public String name;

    // 私有属性
    protected int age;
    char sex;
    private String phoneNumber;
}

以下演示:

import java.lang.reflect.Field;

/**
 * @author lenovo
 * 反射获取属性、方法
 */
public class ReflectFieldsTest {
    public static void main(String[] args) throws ClassNotFoundException {
        // 获取反射对象
        Class<?> person = Class.forName("myHomeWork.Exp5.Person");
        // 获取公有的属性
        Field[] fs1 = person.getFields();
        if (fs1 != null){
            System.out.println("获取的公有属性");
            for (Field f : fs1){
                System.out.println(f);
            }
        }
        System.out.println("----------");
        // 获取所有的属性
        Field[] fs2 = person.getDeclaredFields();
        System.out.println("获取所有的属性");
        for (Field f : fs2){
            System.out.println(f);
        }
    }
}

获取成员方法

Class 对象提供以下方法获取对象的方法(Method):

  • getMethod:返回类或接口的特定方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
  • getMethods:返回类或接口的所有 public 方法,包括其父类的 public 方法。
  • getDeclaredMethod:返回类或接口的特定声明方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
  • getDeclaredMethods:返回类或接口声明的所有方法,包括 public、protected、默认(包)访问和 private 方法,但不包括继承的方法。

获取一个 Method 对象后,可以用 invoke 方法来调用这个方法。
新建一个 Programmer 类,其中声明了不同访问权限的成员方法。

public class Programmer {
    // 构造函数
    public Programmer(){
        System.out.println("乾坤未定,你我皆是牛马");
    }
    // 成员方法
    public void doSomething(){
        System.out.println("调用了公有的、无参方法:doSomething()");
    }

    protected void show(){
        System.out.println("调用了受保护的、无参的show()");
    }

    void writeBugs(int count){
        System.out.println("调用了默认的、有参方法:writeBugs(int count):" + count);
    }

    private String sayHello(String msg){
        System.out.println("调用了私有的、有返回值的,String 参数的 sayHello():msg = " + msg);
        return "hello";
    }
}

以下演示:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author yyt
 * 反射获取成员方法并使用
 */
public class ReflectMethodTest {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException,
            InvocationTargetException, IllegalAccessException, InstantiationException {
        // 1.获取 Class 对象
        Class<?> clazzP = Class.forName("myHomeWork.Exp5.Programmer");
        // 2.获取所有方法
        System.out.println("-----获取所有的方法-----");
        Method[] methods1 = clazzP.getDeclaredMethods();
        for(Method m : methods1){
            System.out.println(m);
        }
        // 3.获取所有 public 的方法
        System.out.println("-----获取所有 public 的方法-----");
        Method[] methods2 = clazzP.getMethods();
        for(Method m : methods2){
            System.out.println(m);
        }
        // 4.获取公有的特定方法
        System.out.println("-----获取公有的特定方法-----");
        // 获取声明的函数,传入所需参数的类名,如果有多个参数,用','连接即可
        Method m = clazzP.getMethod("doSomething");
        System.out.println(m);
        // 实例化一个对象
        Object obj = clazzP.getConstructor().newInstance();
        // 利用 Method 的 invoke 方法调用 doSomething(),无返回值,输出为 null
        System.out.println(m.invoke(obj));
        // 5.获取私有的特定方法
        System.out.println("-----获取私有的特定方法-----");
        Method m2 = clazzP.getDeclaredMethod("sayHello", String.class);
        System.out.println(m2);
        // 因为是私有的方法,需要下面这一行代码使其可使用,公有的则不需要
        m2.setAccessible(true);
        // 需要两个参数,一个是要调用的对象(获取有反射),一个是实参
        Object result = m2.invoke(obj, "This is a message.");
        System.out.println("方法返回值:" + result);
    }
}

使用 invoke(Object obj, Object... args) 方法调用,传入对象以及函数所需的参数,如果有多个参数,用英文逗号 ',' 连接即可。函数会返回一个 Object 对象,使用强制类型转换转成实际类型即可。

存在的问题

  1. 性能问题:由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差。
  2. 安全问题:众所周知,单例设计模式中会强调将构造器设计为私有,因为这样可以防止从外部构造对象。但是反射可以获取类中的域、方法、构造器,修改访问权限,破坏了封装性,导致 “private” 的意义何在?
  3. 检查问题:丧失了编译时类型检查的好处(通过反射可以越过泛型检查),包括异常检查。如果程序企图用反射去调用不存在或者不可访问的方法,在运行时将会失败。

对于“Java 的反射是否破坏了其封装性”这个问题,有两种不同的见解:

  1. Java 的 private 修饰符并不是为了安全性设计的,private 并不是解决“安全”问题的。private 想表达的不是“安全性”的意思,而是面向对象编程的封装概念,用户常规使用 Java 的一种约束。
  2. 如果一个类中的私有成员是为了服务那些对外提供的 public 成员,那么即使通过反射机制获取了类中私有成员的存取权限,恶意修改,也纯属于自己恶心自己。

参考

知乎专栏 - 力扣
Github - JavaCore
CSDN - 杨晨光
CSDN - 徐大侠0610
打赏
评论区
头像
文章目录