Java反射

3/22/2022 反射

# 反射基础

  1. 反射(Reflection)是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。

  2. 通过反射机制,可以在运行时访问 Java 对象的属性,方法,构造方法等。

# 反射的应用场景

  1. 开发通用框架 - 反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。

  2. 动态代理 - 在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。

  3. 注解 - 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。如果没有反射机制,注解并不比注释更有用。

  4. 可扩展性功能 - 应用程序可以通过使用完全限定名称创建可扩展性对象实例来使用外部的用户定义类。

# 反射的缺点

  1. 性能开销 - 由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用程序中频繁调用的代码段中避免。

  2. 破坏封装性 - 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

  3. 内部曝光 - 由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。

# 反射机制

# 类加载过程

类加载的完整过程如下:

  1. 在编译时,Java 编译器编译好 .java 文件之后,在磁盘中产生 .class 文件。.class 文件是二进制文件,内容是只有 JVM 能够识别的机器码。

  2. JVM 中的类加载器读取字节码文件,取出二进制数据,加载到内存中,解析.class 文件内的信息。类加载器会根据类的全限定名来获取此类的二进制字节流;然后,将字节流所代表的静态存储结构转化为方法区的运行时数据结构;接着,在内存中生成代表这个类的 java.lang.Class 对象

  3. 加载结束后,JVM 开始进行连接阶段(包含验证、准备、初始化)。经过这一系列操作,类的变量会被初始化。

img

# Class对象

  1. 要想使用反射,首先需要获得待操作的类所对应的 Class 对象。Java 中,无论生成某个类的多少个对象,这些对象都会对应于同一个 Class 对象。这个 Class 对象是由 JVM 生成的,通过它能够获悉整个类的结构。所以,java.lang.Class 可以视为所有反射 API 的入口点。

  2. 反射的本质就是:在运行时,把 Java 类中的各种成分映射成一个个的 Java 对象。

  3. Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载

  4. Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要(关于反射稍后分析)

  5. 步骤说明:

    • JVM 加载方法的时候,遇到 new User(),JVM 会根据 User 的全限定名去加载 User.class 。

    • JVM 会去本地磁盘查找 User.class 文件并加载 JVM 内存中。

    • JVM 通过调用类加载器自动创建这个类对应的 Class 对象,并且存储在 JVM 的方法区。注意:一个类有且只有一个 Class 对象

# 反射的使用

# Class类对象的获取

  1. 在类加载的时候,jvm会创建一个class对象

  2. class对象是可以说是反射中最常用的,获取class对象的方式的主要有三种

    • 根据类名:类名.class

    • 根据对象:对象.getClass()

    • 根据全限定类名:Class.forName(全限定类名)

    @Test
    public void classTest() throws Exception {
        // 获取Class对象的三种方式
        logger.info("根据类名:  \t" + User.class);
        logger.info("根据对象:  \t" + new User().getClass());
        logger.info("根据全限定类名:\t" + Class.forName("com.test.User"));
        // 常用的方法
        logger.info("获取全限定类名:\t" + userClass.getName());
        logger.info("获取类名:\t" + userClass.getSimpleName());
        logger.info("实例化:\t" + userClass.newInstance());
    }

    // ...
    package com.test;

    public class User {
        private String name = "init";
        private int age;
        public User() {}
        public User(String name, int age) {
            super();
            this.name = name;
            this.age = age;
        }
        private String getName() {
            return name;
        }
        private void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        @Override
        public String toString() {
            return "User [name=" + name + ", age=" + age + "]";
        }
    }

输出:

根据类名:  	class com.test.User
根据对象:  	class com.test.User
根据全限定类名:	class com.test.User
获取全限定类名:	com.test.User
获取类名:	User
实例化:	User [name=init, age=0]

# Class类的方法

方法名 说明
forName() (1)获取Class对象的一个引用,但引用的类还没有加载(该类的第一个对象没有生成)就加载了这个类。
(2)为了产生Class引用,forName()立即就进行了初始化。
Object-getClass() 获取Class对象的一个引用,返回表示该对象的实际类型的Class引用。
getName() 取全限定的类名(包括包名),即类的完整名字。
getSimpleName() 获取类名(不包括包名)
getCanonicalName() 获取全限定的类名(包括包名)
isInterface() 判断Class对象是否是表示一个接口
getInterfaces() 返回Class对象数组,表示Class对象所引用的类所实现的所有接口。
getSupercalss() 返回Class对象,表示Class对象所引用的类所继承的直接基类。应用该方法可在运行时发现一个对象完整的继承结构。
newInstance() 返回一个Oject对象,是实现“虚拟构造器”的一种途径。使用该方法创建的类,必须带有无参的构造器。
getFields() 获得某个类的所有的公共(public)的字段,包括继承自父类的所有公共字段。 类似的还有getMethods和getConstructors。
getDeclaredFields 获得某个类的自己声明的字段,即包括public、private和proteced,默认但是不包括父类声明的任何字段。类似的还有getDeclaredMethods和getDeclaredConstructors。

# Constructor

  1. 获取Constructor对象是通过Class类中的方法获取的,Class类与Constructor相关的主要方法如下:
方法返回值 方法名称 方法说明
static Class<?> forName(String className) 返回与带有给定字符串名的类或接口相关联的 Class 对象。
Constructor getConstructor(Class<?>... parameterTypes) 返回指定参数类型、具有public访问权限的构造函数对象
Constructor<?>[] getConstructors() 返回所有具有public访问权限的构造函数的Constructor对象数组
Constructor getDeclaredConstructor(Class<?>... parameterTypes) 返回指定参数类型、所有声明的(包括private)构造函数对象
Constructor<?>[] getDeclaredConstructor() 返回所有声明的(包括private)构造函数对象
T newInstance() 调用无参构造器创建此 Class 对象所表示的类的一个新实例。
public class ConstructionTest implements Serializable {
    public static void main(String[] args) throws Exception {

        Class<?> clazz = null;

        //获取Class对象的引用
        clazz = Class.forName("com.example.javabase.User");

        //第一种方法,实例化默认构造方法,User必须无参构造函数,否则将抛异常
        User user = (User) clazz.newInstance();
        user.setAge(20);
        user.setName("Jack");
        System.out.println(user);

        System.out.println("--------------------------------------------");

        //获取带String参数的public构造函数
        Constructor cs1 =clazz.getConstructor(String.class);
        //创建User
        User user1= (User) cs1.newInstance("hiway");
        user1.setAge(22);
        System.out.println("user1:"+user1.toString());

        System.out.println("--------------------------------------------");

        //取得指定带int和String参数构造函数,该方法是私有构造private
        Constructor cs2=clazz.getDeclaredConstructor(int.class,String.class);
        //由于是private必须设置可访问
        cs2.setAccessible(true);
        //创建user对象
        User user2= (User) cs2.newInstance(25,"hiway2");
        System.out.println("user2:"+user2.toString());

        System.out.println("--------------------------------------------");

        //获取所有构造包含private
        Constructor\<?> cons[] = clazz.getDeclaredConstructors();
        // 查看每个构造方法需要的参数
        for (int i = 0; i \< cons.length; i++) {
            //获取构造函数参数类型
            Class\<?> clazzs[] = cons[i].getParameterTypes();
            System.out.println("构造函数["+i+"]:"+cons[i].toString() );
            System.out.print("参数类型["+i+"]:(");
            for (int j = 0; j \< clazzs.length; j++) {
                if (j == clazzs.length - 1)
                    System.out.print(clazzs[j].getName());
                else
                    System.out.print(clazzs[j].getName() + ",");
            }
            System.out.println(")");
        }
    }
}
  1. 关于Constructor类本身一些常用方法如下(仅部分,其他可查API)
方法返回值 方法名称 方法说明
Class getDeclaringClass() 返回 Class 对象,该对象表示声明由此 Constructor 对象表示的构造方法的类,其实就是返回真实类型(不包含参数)
Type[] getGenericParameterTypes() 按照声明顺序返回一组 Type 对象,返回的就是 Constructor对象构造函数的形参类型。
String getName() 以字符串形式返回此构造方法的名称。
Class<?>[] getParameterTypes() 按照声明顺序返回一组 Class 对象,即返回Constructor 对象所表示构造方法的形参类型
T newInstance(Object... initargs) 使用此 Constructor对象表示的构造函数来创建新实例
String toGenericString() 返回描述此 Constructor 的字符串,其中包括类型参数。
Constructor cs3 = clazz.getDeclaredConstructor(int.class,String.class);
System.out.println("-----getDeclaringClass-----");
Class uclazz=cs3.getDeclaringClass();
//Constructor对象表示的构造方法的类
System.out.println("构造方法的类:"+uclazz.getName());

System.out.println("-----getGenericParameterTypes-----");
//对象表示此 Constructor 对象所表示的方法的形参类型
Type[] tps=cs3.getGenericParameterTypes();
for (Type tp:tps) {
    System.out.println("参数名称tp:"+tp);
}
System.out.println("-----getParameterTypes-----");
//获取构造函数参数类型
Class\<?> clazzs[] = cs3.getParameterTypes();
for (Class claz:clazzs) {
    System.out.println("参数名称:"+claz.getName());
}
System.out.println("-----getName-----");
//以字符串形式返回此构造方法的名称
System.out.println("getName:"+cs3.getName());

System.out.println("-----getoGenericString-----");
//返回描述此 Constructor 的字符串,其中包括类型参数。
System.out.println("getoGenericString():"+cs3.toGenericString());

# Field

  1. Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。

  2. 同样的道理,我们可以通过Class类的提供的方法来获取代表字段信息的Field对象,Class类与Field对象相关方法如下:

方法返回值 方法名称 方法说明
Field getDeclaredField(String name) 获取指定name名称的(包含private修饰的)字段,不包括继承的字段
Field[] getDeclaredField() 获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段
Field getField(String name) 获取指定name名称、具有public修饰的字段,包含继承字段
Field[] getField() 获取修饰符为public的字段,包含继承字段
public class ReflectField {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class\<?> clazz = Class.forName("reflect.Student");
        //获取指定字段名称的Field类,注意字段修饰符必须为public而且存在该字段,
        // 否则抛NoSuchFieldException
        Field field = clazz.getField("age");
        System.out.println("field:"+field);

        //获取所有修饰符为public的字段,包含父类字段,注意修饰符为public才会获取
        Field fields[] = clazz.getFields();
        for (Field f:fields) {
            System.out.println("f:"+f.getDeclaringClass());
        }

        System.out.println("================getDeclaredFields====================");
        //获取当前类所字段(包含private字段),注意不包含父类的字段
        Field fields2[] = clazz.getDeclaredFields();
        for (Field f:fields2) {
            System.out.println("f2:"+f.getDeclaringClass());
        }
        //获取指定字段名称的Field类,可以是任意修饰符的自动,注意不包含父类的字段
        Field field2 = clazz.getDeclaredField("desc");
        System.out.println("field2:"+field2);
    }
    /**
      输出结果: 
     field:public int reflect.Person.age
     f:public java.lang.String reflect.Student.desc
     f:public int reflect.Person.age
     f:public java.lang.String reflect.Person.name

     ================getDeclaredFields====================
     f2:public java.lang.String reflect.Student.desc
     f2:private int reflect.Student.score
     field2:public java.lang.String reflect.Student.desc
     */
}
  1. 关于Field类还有其他常用的方法如下:
方法返回值 方法名称 方法说明
void set(Object obj, Object value) 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
Object get(Object obj) 返回指定对象上此 Field 表示的字段的值
Class<?> getType() 返回一个 Class 对象,它标识了此Field 对象所表示字段的声明类型。
boolean isEnumConstant() 如果此字段表示枚举类型的元素则返回 true;否则返回 false
String toGenericString() 返回一个描述此 Field(包括其一般类型)的字符串
String getName() 返回此 Field 对象表示的字段的名称
Class<?> getDeclaringClass() 返回表示类或接口的 Class 对象,该类或接口声明由此 Field 对象表示的字段
void setAccessible(boolean flag) 将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性
  1. 上述方法可能是较为常用的,事实上在设置值的方法上,Field类还提供了专门针对基本数据类型的方法,如setInt()/getInt()、setBoolean()/getBoolean、setChar()/getChar()等等方法,这里就不全部列出了,需要时查API文档即可。需要特别注意的是被final关键字修饰的Field字段是安全的,在运行时可以接收任何修改,但最终其实际值是不会发生改变的。

# Method

  1. Method 提供关于类或接口上单独某个方法(以及如何访问该方法)的信息,所反映的方法可能是类方法或实例方法(包括抽象方法)。

  2. 下面是Class类获取Method对象相关的方法:

方法返回值 方法名称 方法说明
Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回一个指定参数的Method对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
Method[] getDeclaredMethod() 返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
Method getMethod(String name, Class<?>... parameterTypes) 返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。
Method[] getMethods() 返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。
import java.lang.reflect.Method;
public class ReflectMethod  {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {

        Class clazz = Class.forName("reflect.Circle");

        //根据参数获取public的Method,包含继承自父类的方法
        Method method = clazz.getMethod("draw",int.class,String.class);

        System.out.println("method:"+method);

        //获取所有public的方法:
        Method[] methods =clazz.getMethods();
        for (Method m:methods){
            System.out.println("m::"+m);
        }

        System.out.println("=========================================");

        //获取当前类的方法包含private,该方法无法获取继承自父类的method
        Method method1 = clazz.getDeclaredMethod("drawCircle");
        System.out.println("method1::"+method1);
        //获取当前类的所有方法包含private,该方法无法获取继承自父类的method
        Method[] methods1=clazz.getDeclaredMethods();
        for (Method m:methods1){
            System.out.println("m1::"+m);
        }
    }
}
  1. Method3本身还有一些常用的方法如下:
方法返回值 方法名称 方法说明
Object invoke(Object obj, Object... args) 对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
Class<?> getReturnType() 返回一个 Class 对象,该对象描述了此 Method 对象所表示的方法的正式返回类型,即方法的返回类型
Type getGenericReturnType() 返回表示由此 Method 对象所表示方法的正式返回类型的 Type 对象,也是方法的返回类型。
Class<?>[] getParameterTypes() 按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型。即返回方法的参数类型组成的数组
Type[] getGenericParameterTypes() 按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型的,也是返回方法的参数类型
String getName() 以 String 形式返回此 Method 对象表示的方法名称,即返回方法的名称
boolean isVarArgs() 判断方法是否带可变参数,如果将此方法声明为带有可变数量的参数,则返回 true;否则,返回 false。
String toGenericString() 返回描述此 Method 的字符串,包括类型参数。

# 方法的反射调用

# 原理

  1. 方法的反射调用,也就是 Method.invoke 方法。Method.invoke 方法源码:
public final class Method extends Executable {
  ...
  public Object invoke(Object obj, Object... args) throws ... {
    ... // 权限检查
    MethodAccessor ma = methodAccessor;
    if (ma == null) {
      ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
  }
}

# 开销

  1. 方法的反射调用会带来不少性能开销,原因主要有三个:

    • 变长参数方法导致的 Object 数组

    • 基本类型的自动装箱、拆箱

    • 还有最重要的方法内联

  2. 下面只关注反射调用本身的性能开销。

    • 第一,由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组(感兴趣的同学私下可以用 javap 查看)。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中。
    • 第二,由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱。
  3. 这两个操作除了带来性能开销外,还可能占用堆内存,使得 GC 更加频繁。

# 参考

Java 基础 - 反射机制详解 | Java 全栈知识体系 (opens new window)

深入理解 Java 反射和动态代理 | JAVACORE (opens new window)

Last Updated: 3/28/2022, 9:29:49 PM