Java 反射机制

作者: JamFF | 来源:发表于2019-05-28 12:30 被阅读5次

    不论是 Java 开发 还是 Android 开发,反射、泛型、注解 都是架构设计中很重要的一个知识点。

    为了更好的理解反射,需要先简单了解一些类加载器相关的知识。

    类加载器

    一、类的初始化

    当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化。

    1. 加载
      就是指将 class 文件读入内存,并为之创建一个 Class 对象,任何类被使用时系统都会建立一个 Class 对象。
    2. 连接
    • 验证:是否有正确的内部结构,并和其他类协调一致。
    • 准备:负责为类的静态成员分配内存,并设置默认初始化值,静态随着类的加载而加载。
    • 解析:将类的二进制数据中的符号引用替换为直接引用。
    1. 初始化
      为堆栈开辟内存,默认初始化,构造初始化,等等。

    二、类初始化时机

    • 创建类的实例。
    • 访问类的静态变量,或者为静态变量赋值。
    • 调用类的静态方法。
    • 使用反射方式来强制创建某个类或接口对应的 java.lang.Class 对象。
    • 初始化某个类的子类。
    • 直接使用 java.exe 命令来运行某个主类。

    三、类加载器

    负责将 .class 文件加载到内在中,并为之生成对应的 Class 对象。

    • Bootstrap ClassLoader
      根类加载器,也被称为引导类加载器,负责 Java 核心类的加载,比如 System、String 等。在 JDK 中 JRE 的 lib 目录下 rt.jar 文件中。

    • Extension ClassLoader
      扩展类加载器,负责 JRE 的扩展目录中 jar 包的加载,在 JDK 中 JRE 的 lib 目录下 ext 目录。

    • System ClassLoader
      系统类加载器,负责在 JVM 启动时加载来自 java 命令的 class 文件,以及 classpath 环境变量所指定的 jar 包和类路径。也就是说,平时我们写的 java 文件,编译后生成的 class 文件,都是通过该加载器进行加载的。

    通过这些描述我们就可以知道我们常用的东西的加载都是由谁来完成的。

    那么,我们如何使用这些class文件中的内容呢?这就是反射要研究的内容。

    反射

    Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能成为 Java 语言的反射机制。

    一、获取 Class 类对象

    要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是 Class 类中的方法。所以先要获取到每一个字节码文件对应的 Class 类型的对象。

    有三种方式获取,下面用这个 Book.java 举例:

    public class Book {
    
        private String name;
        public int price;
    
        public Book() {
        }
    
        Book(String name) {
            this.name = name;
        }
    
        public Book(String name, int price) {
            this.name = name;
            this.price = price;
        }
    
        public void show() {
            System.out.println("show");
        }
    
        public void function(String s) {
            System.out.println("function: " + s);
        }
    
        public String returnValue(String name, int price) {
            return name + " - " + price;
        }
    
        private void hello() {
            System.out.println("hello");
        }
    
        @Override
        public String toString() {
            return "Book{" +
                    "name='" + name + '\'' +
                    ", price=" + price +
                    '}';
        }
    }
    
    1. Object 类的 getClass() 方法
      在可以获取到该实例对象的情况下,采用该方法

      // 方式一
      Book book = new Book();
      Class c1 = book.getClass();
      
    2. 数据类型的静态 class 属性
      在可以导入该类的情况下,采用该方法

      // 方式二
      Class c2 = Book.class;
      
    3. 通过Class类的静态方法 forName(String className)
      在得知完整类名的情况下,采用该方法

      // 方式三
      try {
          Class c3 = Class.forName("com.ff.reflect.Book");
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
      

    开发中经常会使用方式三,首先,方式三可以结合配置文件使用,从配置文件中获取完整类名;其次,很多情况下不能获取实例对象和导入类。

    二、获取构造方法

    前提条件就是先要获取到 Class 文件对象

    Class clazz = null;
    try {
        clazz = Class.forName("com.ff.reflect.Book");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    if (clazz == null) {
        return;
    }
    
    获取全部构造方法
    1. 获取所有公共构造方法
    Constructor[] constructors = clazz.getConstructors();
    for (Constructor constructor : constructors) {
        System.out.println(constructor);
    }
    

    打印结果:

    public com.ff.reflect.Book(java.lang.String,int)
    public com.ff.reflect.Book()
    

    使用 getConstructors() 可以获取到 public 修饰的构造方法。

    1. 获取所有构造方法
    Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
    for (Constructor declaredConstructor : declaredConstructors) {
        System.out.println(declaredConstructor);
    }
    

    打印结果:

    private com.ff.reflect.Book(int)
    com.ff.reflect.Book(java.lang.String)
    public com.ff.reflect.Book(java.lang.String,int)
    public com.ff.reflect.Book()
    

    使用 getDeclaredConstructors() 可以获取到全部构造方法,包括 public、protected、private 以及默认修饰的构造方法。

    获取单个构造方法

    开发中我们一般需要使用一种构造方法,所以下面方式更为常用。

    1. 获取单个公共构造方法
    • 无参构造
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();// 无参构造
        System.out.println(object);
    } catch (NoSuchMethodException | IllegalAccessException
            | InstantiationException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
    • 带参构造
    try {
        Constructor constructor = clazz.getConstructor(String.class, int.class);
        Object object = constructor.newInstance("Java", 18);// 两个参数的构造
        System.out.println(object);
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
    1. 获取单个非公共构造方法
      和上面获取公共构造是类似的,只不过将 getConstructor() 替换为 getDeclaredConstructor() 就可以获取非 public 的构造方法了。
    try {
        Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class);
        Object object = declaredConstructor.newInstance("Java");
        System.out.println(object);
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

    需要注意的是,如果反射得到的是私有构造方法,那么直接调用会报: IllegalAccessException 非法访问异常,需要使用暴力访问,即设置 setAccessible(true)

    try {
        Constructor constructor = clazz.getDeclaredConstructor(int.class);
        constructor.setAccessible(true);// 暴力访问
        Object object = constructor.newInstance(18);// 私有构造
        System.out.println(object);
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

    三、获取成员变量

    与上面获取构造方法大同小异

    获取全部成员变量
    1. 获取所有公共成员变量
    Field[] fields = clazz.getFields();
    for (Field field : fields) {
        System.out.println(field);
    }
    
    1. 获取所有成员变量
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        System.out.println(declaredField);
    }
    
    获取单个成员变量
    1. 获取单个公共成员变量
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Field price = clazz.getField("price");// 获取 price 成员变量
        price.set(object, 18);// 修改成员变量的值
        System.out.println(object);
    } catch (NoSuchFieldException | NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
    1. 获取单个非公共成员变量
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Field name = clazz.getDeclaredField("name");// 获取 name 成员变量
        name.set(object, "Java");// 修改成员变量的值
        System.out.println(object);
    } catch (NoSuchFieldException | NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

    需要注意的是,如果反射得到的是私有成员变量,那么直接调用会报: IllegalAccessException 非法访问异常,需要使用暴力访问,即设置 setAccessible(true)

    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Field name = clazz.getDeclaredField("name");// 获取 name 成员变量
        name.setAccessible(true);// 暴力访问,可访问私有成员变量
        name.set(object, "Java");// 修改成员变量的值
        System.out.println(object);
    } catch (NoSuchFieldException | NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

    四、获取成员方法

    获取全部成员方法
    1. 获取所有公共成员方法,包括父类
    Method[] methods = clazz.getMethods();
    for (Method method : methods) {
        System.out.println(method);
    }
    
    1. 获取所有成员方法,不包含父类
    Method[] declaredMethods = clazz.getDeclaredMethods();
    for (Method declaredMethod : declaredMethods) {
        System.out.println(declaredMethod);
    }
    
    获取单个成员方法
    1. 获取单个公共成员方法
    • 无参数、无返回值
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Method show = clazz.getMethod("show");// show 方法
        show.invoke(object);// 调用 show 方法
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
    • 带参数、无返回值
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Method function = clazz.getMethod("function", String.class);// function 方法
        function.invoke(object, "hello");// 调用 function 方法,传参 hello
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
    • 带多个参数,有返回值
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Method returnValue = clazz.getMethod("returnValue", String.class, int.class);// returnValue 方法
        Object string = returnValue.invoke(object, "Java", 18);// 调用 returnValue 方法,传参,得到方法返回值
        System.out.println(string);// 打印 returnValue 方法返回值
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
    1. 获取单个非公共成员方法
      需要注意的是,如果反射得到的是私有成员方法,那么直接调用会报: IllegalAccessException 非法访问异常,需要使用暴力访问,即设置 setAccessible(true)
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Method hello = clazz.getDeclaredMethod("hello");// 私有成员方法
        hello.setAccessible(true);// 暴力访问
        hello.invoke(object);// 调用 hello 方法
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

    反射的应用

    一、跳过泛型检查

    向 ArrayList<Integer> 中添加字符串数据。
    由于泛型只在编译期间生效,而反射是在运行期间调用,所以可以利用这两点进行实现:

    /**
     * 向 ArrayList<Integer> 中添加字符串数据
     */
    private static void test() {
        ArrayList<Integer> array = new ArrayList<>();
    
        Class<? extends ArrayList> aClass = array.getClass();
        try {
            Method add = aClass.getMethod("add", Object.class);
            add.invoke(array, "hello");
            add.invoke(array, "world");
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
        System.out.println(array);
    }
    

    二、通用工具类

    设置某个对象的某个属性为指定值:
    public void setProperty(Object obj, String propertyName, Object value){},
    此方法可将obj对象中名为propertyName的属性的值设置为value。

    public class Utils {
    
        /**
         * 设置某个对象的某个属性为指定值
         *
         * @param obj          对象
         * @param propertyName 属性
         * @param value        值
         */
        public static void setProperty(Object obj, String propertyName, Object value) {
            Class<?> aClass = obj.getClass();
            try {
                Field declaredField = aClass.getDeclaredField(propertyName);
                declaredField.setAccessible(true);
                declaredField.set(obj, value);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    

    使用工具类:

    private static void test() {
        Book book = new Book();
        Utils.setProperty(book, "name", "Java");
        Utils.setProperty(book, "price", 18);
        System.out.println(book);
    }
    

    在架构设计中的应用也很常见,比如动态代理等等,就不在这里展开了。

    至此,基本的 Java 反射机制都已经介绍完了。

    相关文章

      网友评论

        本文标题:Java 反射机制

        本文链接:https://www.haomeiwen.com/subject/xoictctx.html