什么是反射?

138 阅读12分钟

一、了解反射

动态语言: 在运行时可以改变其结构的语言:如函数、对象、代码可以引进,已有的函数可以被删除或是其他结构上的变化,就是运行代码可以根据某些条件改变自身结构。主要语言:C#、JS、php、py 等

静态语言: 运行时结构不可变,主要语言:Java、C、C++

Java 不是动态语言,但是 Java 可以称为“准动态语言”。即 Java 有一定的动态性,可以利用反射机制获得类似动态语言的特性。Java 的动态性让编程的时更加的灵活。

反射机制:

允许程序在执行期间借助于 Reflection API 获取任何类的内部信息(比如成员变量,构造器,成员方法等),并能操作对象的属性以及方法。反射在设计模式和框架底层都会用到。加载完类之后,在堆中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,形象的称之为反射。

反射机制可以完成什么:

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

二、反射相关的主要类

  1. java.lang.Class:代表一个类,Class 对象表示的是某个类加载后在堆中的对象
  2. java.lang.reflect.Method:代表类的方法,Method对象表示某个类的方法
  3. java.lang.reflect.Field:代表类的成员变量,Field 对象表示某个类的成员变量
  4. java.lang.reflect.Constructor:代表类的构造方法,Constructor 对象表示构造器
  • 创建一个 Member 类
public class Member {
​
    public String name = "张三 ";
​
    public Member() {
    }
​
    public Member(String name) {
        this.name = name;
    }
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
}
  • 创建一个测试类,测试使用通过反射获得Member 的属性信息和构造器信息
public class MemberTest {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchFieldException, ClassNotFoundException, NoSuchMethodException {
​
        // 1. 通过反射获得 Member 类
        // 1. 加载类,返回 Class类型的对象 cls
        Class<?> cls = Class.forName("com.lss.reflect.demo01.Member");
        // 2. 通过 cls 得到要加载的类  Member 的实例
        Object o = cls.newInstance();
​
        // java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量
        // 得到 name 字段
        // getField 不能得到私有的属性
        Field nameField = cls.getField("name");
        System.out.println("获得成员变量的信息:" + nameField.get(o));
​
        // 获得构造器
        /**
         * 1. 如果括号里面不写参数就是得到的无参构造器
         * 2. 假如对应的参数类型可以得到有参构造器
         * 3. 通过构造器也能创建类的对象
         */
        Constructor<?> constructor = cls.getConstructor();
        System.out.println("获得无参构造器:" + constructor);
        // 传入 String.class 就是得到 String 类的 Class 对象
        Constructor<?> constructor1 = cls.getConstructor(String.class);
        System.out.println("获得有参构造器:" + constructor1);
    }
}

三、反射优缺点

  1. 优点:可以动态的创建和使用对象(也是框架底层的核心),使用灵活,没有反射机制,框架技术就会失去底层支持。
  2. 缺点:使用反射基本是解释执行,对执行速度有影响
/**
 * @author lishisen
 * @description 测试通过反射调用方法执行和普通调用的执行效率
 * @date 2022/5/6 21:05
 **/
public class Demo01 {
​
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
​
        normalMethod();
        reflectMethod();
    }
    // 普通方法
    public static void normalMethod() {
​
        Member member = new Member();
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 900000000L; i++) {
            member.show();
        }
        long end = System.currentTimeMillis();
        System.out.println("普通方法:" + (end - begin));
    }
    // 通过反射
    public static void reflectMethod() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
​
​
        Class<?> clzz = Class.forName("com.lss.reflect.demo01.Member");
        Object o = clzz.newInstance();
        Method[] methods = clzz.getMethods();
        Method show = clzz.getMethod("show");
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 900000000L; i++) {
            show.invoke(o);
        }
        long end = System.currentTimeMillis();
        System.out.println("反射方法:" +(end - begin));
    }
​
}
​
// 通过对比普通方式创建对象调用方法和通过反射方式创建对象实例调用方法
// 结果相差还是比较大的
普通方法:795
反射方法:2886

可以通过关闭访问检查来进行有效的效率优化

  1. Method 和 Field、Constructor 对象都有 setAccessible() 方法
  2. setAccessible 作用是启动和禁用访问安全检查的开关
  3. 参数值为 true 表示反射的对象在使用时取消访问检查,提高反射的效率。参数值为 false 则表示反射的对象执行访问检查。
// 反射调用优化
public static void reflectMethod2() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
    Class<?> clzz = Class.forName("com.lss.reflect.demo01.Member");
    Object o = clzz.newInstance();
    Method[] methods = clzz.getMethods();
    Method show = clzz.getMethod("show");
    show.setAccessible(true); // 在反射调用的时候取消访问检查
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 900000000L; i++) {
        show.invoke(o);
    }
    long end = System.currentTimeMillis();
    System.out.println("反射方法:" +(end - begin));
}
​
// 结果
普通方法:671
反射方法:2032
反射方法优化:1572

四、Class 类

1. 类简介和常用方法

  1. Class 也是类,因此也继承 Object 类

  1. Class 类对象不是 new 出来的,而是系统创建的
// new 类名的方式创建对象,首先会进入到 ClassLoader 中的加载类方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
// 反射方式仍然也是使用的 ClassLoader 的 loadClass 方法
@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
  1. 对于某个类的 Class 类对象,在内存中只有一份,因为类只加载一次
Class<?> c1 = Class.forName("com.lss.reflect.demo02.Demo01");
Class<?> c2 = Class.forName("com.lss.reflect.demo02.Demo01");
System.out.println(c1.hashCode() == c2.hashCode());
// 结果为 true
  1. 每个类的实例都会记得自己是由那个 Class 实例所生成
  2. 通过 Class 对象可以完整地得到一个类的完整结构,通过一系列 API
方法名功能说明
static Class forName(String name)返回指定类名 name 的 Class 对象
Object newInstance()调用缺省构造函数,返回该 Class 对象的一个实例
getName()返回此 Class 对象所表示的实体(类、接口、数组类、基本类型等)名称
Class getSuperClass()返回当前 Class 对象的父类的 Class 对象
Class [] getInterFaces()获取当前 Class 对象的接口
ClassLoader getClassLoader()返回该类的类加载器
Class getSuperclass()返回表示 Class 所表示的实体的超类的 Class
Constructor[] getConstructors()返回一个包含某些 Constructor 对象的数组
Field[] getDeclaredFields()返回 Field 对象的一个数组
Method getMethod(String name,Class ... paramTypes)返回 Method 对象,此对象的形参类型为 paramType
  1. Class 对象是存放在堆中的
  2. 类的字节码二进制数据,是放在方法区的,有的地方称为元数据(包括方法代码、变量名、方法名、访问权限等 )
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchFieldException {
    // 1. 获取到 Member 类对应的 Class 对象
    Class<?> cls = Class.forName("com.lss.reflect.demo03.Member");
    // 输出 cls 对象,是那个类的 Class 对象 com.lss.reflect.demo03.Member
    System.out.println(cls);
    // 输出 cls 对象类型 java.lang.Class
    System.out.println(cls.getClass());
    // 2. 得到包名
    System.out.println("包名:" + cls.getPackage().getName());
    // 3. 得到全路径名称
    System.out.println("全路径名称:" + cls.getName());
    // 4. 通过 cls 创建对象实例
    Member member = (Member) cls.newInstance();
    System.out.println(member);
    // 5. 通过反射获得属性 name
    Field name = cls.getField("name");
    System.out.println("name属性的值:" + name.get(member));
    // 6. 通过反射给属性赋值
    name.set(member,"李四");
    System.out.println("反射修改属性值:" + name.get(member));
    System.out.println("--得到所有属性的名称--");
    Field[] fields = cls.getFields();
    for (Field f: fields) {
        System.out.println(f.getName());
    }
}
// 结果
class com.lss.reflect.demo03.Member
class java.lang.Class
包名:com.lss.reflect.demo03
全路径名称:com.lss.reflect.demo03.Member
com.lss.reflect.demo03.Member@6d6f6e28
name属性的值:张三
反射修改属性值:李四
--得到所有属性的名称--
name
age

2. 获取 Class 对象方式

  1. 通过 Class 类的静态方法 forName() 获取

    前提:已经知道一个类的全类名,且该类在类路径下

    应用场景:多用于配置文件读取类的全类名加载类

String path = "com.lss.reflect.demo04.Member";
Class<?> aClass = Class.forName(path);
  1. 通过类的 class 获取

    前提:已知具体的类,该方式安全可靠,程序性能高

    应用场景:多用于参数传递,比如通过反射得到对应构造器对象

Class<Member> memberClass = Member.class;
System.out.println(memberClass);
// 结果
class com.lss.reflect.demo04.Member
  1. 通过 getClass 方式获取

    前提:已知某个类的实例

    应用场景:通过创建好的对象,获取 Class 对象

Member member = new Member();
Class<? extends Member> aClass1 = member.getClass();
System.out.println(aClass1);
  1. 其他方式
// 通过类加载器得到类的对象
// 1. 得到类加载器
String path = "com.lss.reflect.demo04.Member";
Member member1 = new Member();
ClassLoader classLoader = member1.getClass().getClassLoader();
// 2. 通过类加载器得到 Class 对象
Class<?> aClass2 = classLoader.loadClass(path);
System.out.println(aClass2);

以上四种方法获得都是通过类加载器,因为一个类只会加载一次

通过输出他们的 hashCode 可以看到,都是相同的。

  1. 8中基本数据类也可以通过 .class 得到 Class 类
Class<Integer> integerClass = int.class;
Class<Character> characterClass = char.class;
System.out.println(integerClass);
System.out.println(characterClass);
  1. 基本数据类型对应的包装类可以通过.TYPE得到 Class 类对象
Class<Integer> type = Integer.TYPE;
System.out.println(type);
Class<Boolean> type1 = Boolean.TYPE;
System.out.println(type1);

3. 那些类型有 Class 对象

  1. 外部类,成员内部类,静态内部类,局部内部类,匿名内部类
  2. interface:接口
  3. 数组
  4. enum:枚举
  5. annotation:注解
  6. 基本数据类型
  7. void
public static void main(String[] args) {
    // 外部类
    Class<String> stringClass = String.class;
    // 接口
    Class<Serializable> serializableClass = Serializable.class;
    // 数组
    Class<Integer[]> aClass = Integer[].class;
    // 枚举
    Class<Thread.State> stateClass = Thread.State.class;
    // 注解
    Class<Deprecated> deprecatedClass = Deprecated.class;
    // 基本数据类型
    Class<Integer> integerClass = int.class;
    // void
    Class<Void> voidClass = void.class;
    // Class
    Class<Class> classClass = Class.class;
    
    System.out.println(c1);
    System.out.println(c2);
    System.out.println(c3);
    System.out.println(c4);
    System.out.println(c5);
    System.out.println(c6);
    System.out.println(c7);
    System.out.println(c8);
}
// 执行结果
class java.lang.String
interface java.io.Serializable
class [Ljava.lang.Integer;
class java.lang.Thread$State
interface java.lang.Deprecated
int
void
class java.lang.Class

五、类加载

1. 动/静加载

反射机制是 Java 实现动态语言的关键,也就是说通过反射实现类的动态加载。

  1. 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强
  2. 动态加载:运行时加载需要的类,如果运行时不用该类,即使该类不存在也不会报错,降低了依赖性
  • 类加载时机

    1. 当创建对象时(new)// 静态加载
    2. 当子类被加载时,父类也会加载// 静态加载
    3. 调用类中的静态成员时// 静态加载
    4. 通过反射// 动态加载

对静态加载和动态加载方式的模拟

代码:

import java.util.Scanner;

/**
 * @author lishisen
 * @description TODO
 * @date 2022/5/6 22:55
 **/
public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException {

        Scanner scanner = new Scanner(System.in);
        System.out.println("input:");
        String next = scanner.next();
        switch (next) {
            case "1":
                Student dog = new Student();
                break;
            case "2":
                Class<?> person = Class.forName("Person");
                break;
        }

    }
    
}

当我们使用 new Student() 创建一个类的对象,这个 Student 类是不存在的,这种方式是静态加载的方式,所以使用 cmd 创建执行 javac命令是编译不通过的

我们创建一个 Student() 类

class Student{
	
}

在使用 javac 命令进行编译,我们会发现这次是编译通过的,并且生成了字节码文件

但是我们看上面的代码是有一句 Class.forName("Person") 加载 Person 类的,这个类也是不存在,由此可以通过反射加载类不是发生在编译阶段的,我们编辑通过后通过 java 命令执行,在 switch 中是判断输入的为 2 则会加载 Person 类,启动之后可以看到代码是正常执行的,只有当我们执行到加载 Person类的时候才会报出异常,即动态加载

因为 new Student() 是静态加载,因此必须编写 Student类

Person 类是动态加载的,所以,没有编写 Person 类也是不会报错的,只有执行到加载类的时候才会抛出异常。

2. 类加载流程

上图中类加载的三个阶段

  • 加载:将类的 Class 文件读入内存,并为之创建一个 java.lang.Class 对象。此过程由类加载器完成

  • 连接:将类的二进制数据合并到 JRE 中

    • 验证:对文件的安全性进行校验
    • 准备:对静态变量做初始化分配默认空间
    • 解析:符号引用转成直接引用
  • 初始化:JVM负责对类进行初始化,这里主要指静态成员(静态资源加载跟类加载是有关联的)

2.1 加载阶段

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象

2.2 连接阶段-验证
  1. 目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

  1. 包括:文件格式验证(是否以魔数 oxcafebabe 开头)、元数据验证、字节码验证和符号引用验证
  2. 可以考虑 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
2.3 连接阶段-准备

JVM 会在该阶段对静态变量,分配内存并默认初始化(对应数据类型的默认初始值,如0、0L、null、false 等)。这些变量所使用的内存都将在方法区中进行分配

/** 类加载:连接阶段-准备 **/
class A {
    // 1. 实例属性,不是变量,所以在类加载的准备阶段不会分配内存
    public int a = 10;
    // 2. 静态变量,分配内存 b,但是分配的内存中的值为默认值 0,只有到类加载第三个阶段初始化阶段才会为该内存赋值20
    public static int b = 20;
    // 3. 常量,与静态变量不一样,因为一旦赋值就不会再改变。
    public static final int c = 30;

}
2.4 连接阶段-解析

虚拟机将常量池内的符号引用替换为直接引用的过程

假设有两个类 A 和 B类,在编译的时候其实是符号引用,还没有真正的加载到内存

类加载到内存中后,两个类分配了内存地址,通过内存地址来进行引用即直接引用

3. 初始化

  1. 初始化阶段才真正开始执行类中定义的 Java 程序代码,此阶段是执行 <clinit>() 方法的过程
  2. <clinit>()` 方法是由编译器按语句在源文件中出现的顺序,因此自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并
  3. 虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法,其他线程都需要阻塞等待,直到活动线程执行 <clinit>() 方法完毕,这个机制保证某个类在内存中只有一份 Class 对象