Java - 反射 - 有这一篇就够了

75 阅读14分钟

Java - 反射 - reflection

前言:大家好!接下来我将和大家一起来学习和认识反射

提出问题:

如果有一个需求是这样子的

  1. 需要你根据配置文件 properties中的指定的类以及方法来创建一个对象,并调用里面的方法用现在的技术能做到吗?

    # properties
    classfullpath=com.xxx.model.Cat
    method=hi
    
    • 显然是做不到的
  2. 这样子的需求在学习框架的时候特别多,即通过外部文件配置,在不修改源码情况下,来控制程序,也符合设计模式的ocp原则(开闭原则)

  3. 使用反射来解决这个问题(快速使用)

    • 定义一个类

      // 在com.xxx.model.Cat路径中定义这个类
      public class Cat {
          public void hi(){
              System.out.println("你好");
          }
      }
      
    • 获取当前文件

      // 获取当前文件
      Properties properties = new Properties();
      properties.load(new FileInputStream("src\\re.properties"));
      String clssfullname = properties.get("clssfullname").toString();
      String methodName = properties.get("method").toString();
      
    • 加载类,返回Class类型的对象

      Class cls = Class.forName(clssfullname);
      
    • 获取对象实例 得到你clssfullname中写的对象

      Object o = aClass.newInstance();
      
    • 通过cls 得到你加载的类使用配置文件中获取的方法名获取对象里面的方法

      Method method = cls.getMethod(methodName);
      
    • 通过当前的方法对象来实现方法的调用

      method.invoke(o);
      // 控制台打印
      // 你好
      
    • 快速上手总结:

      • 在反射的定义中,方法也是一个对象,Java中万物皆对象的概念在这里体现的淋漓尽致
      • 调用对象的方法和之前不一样,是相反的,方法.invoke(传入当前的对象)
      • 反射机制非常的强,奠定了框架的基础,如果Java中没有反射机制,那么Java的商业价值就会掉很多

一、什么是反射

  • 是一种允许程序在运行时动态获取类的信息、操作类或对象的属性和方法的机制。它打破了传统代码的“静态”特性,使得程序可以在运行时动态分析、修改甚至创建类和对象。

  • 反射的核心是在运行时(而非编译时)动态获取类的信息。通过反射,你可以:

    • 在运行时获取类的名称、方法、字段、构造函数、注解等信息。
    • 在运行时动态创建对象、调用方法、访问或修改字段的值。
    • 操作泛型、注解等元数据。
    • 绕过访问权限的限制(如访问私有成员)。
  • 加载完类之后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以形象的称之为:反射

二、反射机制原理图

Java程序在计算机中的三个阶段

三、反射机制能做些什么(通俗理解)

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

四、了解反射主要的类

接下来的演示都将以下面的类进行演示
package com.hgz.javaDemo.reflection.model;

import java.util.StringJoiner;

public class Cat {
    private Integer id;
    private String name;
    private String color;
    public String age;
    public Cat() {
    }

    public Cat(Integer id, String name, String color, String age) {
        this.id = id;
        this.name = name;
        this.color = color;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public void hi(){
        System.out.println("你好");
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", Cat.class.getSimpleName() + "[", "]")
                .add("id=" + id)
                .add("name='" + name + "'")
                .add("color='" + color + "'")
                .toString();
    }
}

1、java.lang.Class :
  • 代表一个类,Class对象表示某个类加载后在堆中的对象

  • // 1、加载类 多种获取方式
    Class cls = Class.forName(clssfullname);
    // Class cls = Cat.class;
    // 2、实例化类
    Object o = cls.newInstance();
    
2、java.lang.reflect.Method :
  • 代表类的方法

  • // 获取对象中的方法
    Method method = cls.getMethod(methodName);
    // 使用方法对象来调用这个方法
    method.invoke(o);
    
3、java.lang.reflect.Field :
  • 代表类的成员变量

  • // getFiled 不能得到私有的属性
    // Field name = cls.getField("name"); //抛出异常
    Field age = cls.getField("age");//成功获取
    System.out.println(age.get(o)); // 获取当前对象的字段值
    
4、java.lang.reflect.Constructor :
  • 代表类的构建

  • // 获取无参的构造函数
    Constructor constructor = cls.getConstructor();
    System.out.println(constructor);
    //获取有参的构造函数,里面需要传入参数的类型
    Constructor constructor1 = cls.getConstructor(String.class);
    System.out.println(constructor1);
    

五、反射的优缺点和性能优化

通过上面的案例,相信大家都会简单的使用反射来创建对象和调用方法了,虽然反射非常的强大,但是我们也需要认识到反射的优缺点

1、反射的优缺点
  • 优点:可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑
  • 缺点:使用反射基本就是解释执行,对执行速度有影响.
  • 测试性能问题:
package com.hgz.javaDemo.reflection;
import com.hgz.javaDemo.reflection.model.Cat;
import java.lang.reflect.Method;

@SuppressWarnings({"all"})
public class Main {
    public static void main(String[] args) throws Exception {
        m1();
        m2();
    }
    //传统方法调用hi方法
    public static void m1(){
        Cat cat = new Cat();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 900000000; i++) {
            cat.hi();
        }
        long end = System.currentTimeMillis();

        System.out.println("使用传统方法调用hi 耗时:"+(end - start));
    };
    //反射机制调用hi方法
    public static void m2() throws Exception {

        Class cal = Class.forName("com.hgz.javaDemo.reflection.model.Cat");
        Object o = cal.newInstance();
        Method hi = cal.getMethod("hi");
        long start = System.currentTimeMillis();
        for (int i = 0; i < 900000000; i++) {
            hi.invoke(o);
        }
        long end = System.currentTimeMillis();

        System.out.println("使用反射调用hi 耗时:"+(end - start));
    }
}
// 控制台输出 : 
// 使用传统方法调用hi 耗时:8
// 使用反射调用hi 耗时:953

可以看到确实是会消耗更多的性能

2、反射的优化
  • 关闭访问检查

    // 关闭访问检查
    hi.setAccessible(true); 
    // 关闭访问检查后 反射机制调用hi方法
    public static void m3() throws Exception {
        Class cal = Class.forName("com.hgz.javaDemo.reflection.model.Cat");
        Object o = cal.newInstance();
        Method hi = cal.getMethod("hi");
        hi.setAccessible(true); // 关闭访问检查
        long start = System.currentTimeMillis();
        for (int i = 0; i < 900000000; i++) {
            hi.invoke(o);
        }
        long end = System.currentTimeMillis();
        System.out.println("关闭访问检查后 使用反射调用hi 耗时:"+(end - start));
    }
    
    // 控制台输出
    // 使用传统方法调用hi 耗时:0
    // 使用反射调用hi 耗时:871
    // 关闭访问检查后 使用反射调用hi 耗时:645
    

    可以看出来确实是优化了些

六、Class类详细介绍

  • Class 也是类,因此也继承Object类
  • Class 类对象不是new 出来的,而是系统创建的
  • 对于某个类的Class 类对象,在内存中只有一份,因此类只加载一次。
  • 每个类的实例都会记得自己是由哪个 Class 实例所生成
  • 通过Class 可以完整地得到一个类的完整结构,通过一系列API
  • Class 对象是存放在堆中的
  • 类的字节码二进制数据,是放在方法区中的,有的地方称为类的元数据(包括 方法代码,变量名,方法名,访问权限等等)
方法名功能说明
static Class forName(String name)返回指定类名的nameClass 对象
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
String classALlPath = "com.hgz.javaDemo.reflection.model.Car";
// 获取到Car类 对应的Class对象
// 1、获取cls类
// <?> 表示不确定的Java类型
Class<?> cls = Class.forName(classALlPath);
// 2、获取cls的运行类型
System.out.println(cls); // 输出是哪一个类的Class对象
System.out.println(cls.getClass());// 获取cls的运行类型
// 3、 得到包名
System.out.println(cls.getPackage().getName());
// 4、得到全类名
System.out.println(cls.getName());
// 5、通过cls创建对象实例
Car car = (Car)cls.newInstance();
System.out.println(car);
// 6、通过反射获取属性
Field brand = cls.getField("brand");
System.out.println(brand.get(car));
// 7、通过反射给属性赋值
brand.set(car,"奔驰");
System.out.println(brand.get(car));
// 8、我希望大家可以获得所有的属性
Field[] fields = cls.getFields();
for (Field field : fields) {
    System.out.print(field.getName() + " ");
}
  • 获取Class对象的6种方式(4主要)

    1. 在代码编译编写阶段,使用Class.forName("") 来获取

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

      • 应用场景:多用于配置文件,读取类的全路径,加载类。

      • Class cls1 = Class.forName("");
        
    2. 在类加载阶段,通过类的class获取Car.class

      • 前提:已知具体的类

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

      •         Class cls2 = Car.class;
        
    3. 在代码运行阶段,可以通过某个对象的实例调用getClass()方法

      • 前提:已知某个类的实例

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

      • Car car = new Car();
        Class cls3 = car.getClass();
        
    4. 使用类加载器获取 演示:

      // (1) 先获取到类加载器
      ClassLoader classLoader = car.getClass().getClassLoader();
      // (2) 通过类加载器得到Class对象
      Class cls4= classLoader.loadClass("路径");
      
    5. 基本数据类型(intcharbooleanfloatdoublebytelongshort)按如下方式得到Class对象

      • Class integerClass = int.class;
        
    6. 基本数据类型对应的包装类,可以通过.TYPE得到Class对象

      • Class type = Integer.TYPE;
        
  • 哪些类型有Class对象

    1. 外部类,成员内部类,静态内部类,局部内部类,匿名内部类
    2. interface:接口
    3. 数组
    4. enum :枚举
    5. annotation :注解
    6. 基本数据类型
    7. void
  • 类加载

    • 静态加载:编译是加载相关的类,如果没有则报错,依赖性太强

    • 动态加载:运行时加载需要的类,如果运行时不用该类,则不报错,降低了依赖性

      • //静态加载
        Scanner scanner = new Scanner(System.in);
        switch (scanner.next()){
            case "1" :
        //Dog dog = new Dog(); //静态加载如果没有当前这个类直接报错,无法编译,依赖性强
                System.out.println("你好");
                break;
            case "2" :
        // 使用反射动态加载,如果没有这个类也不会报错,当运行到这里才会报错
                Class person = Class.forName("Person");
                Object o = person.newInstance();
                Method hi = person.getMethod("hi");
                Object invoke = hi.invoke(o);
            break;
        }
        
    • 类加载的时机

      • 当创建对象时(new) -- 静态加载
      • 当子类被加载时,父类也加载 -- 静态加载
      • 调用类中的静态成员时 -- 静态加载
      • 通过反射 -- 动态加载
  • 类加载的三个阶段

    • 加载阶段

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

      • 验证

        • 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
        • 包括:文件格式验证(是否已魔数 oxcafedbabe开头)、元数据验证,字节码验证和符号引用验证
        • 可以考虑使用 -Xverify:none参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
      • 准备

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

        • public class AA {
              /**
               * 属性 - 成员变量 - 字段
               * 准备阶段,属性是如何处理的
               * 1、n1 是实例属性,不是静态变量,因此在准备阶段,是不会分配内存
               * 2、n2 是静态变量,分配内存 n2 是默认初始化 0,而不是20
               * 3、n3 是static final是常量,他和静态变量不一样,因为一单赋值就不变, n3 = 30
               */
              public int a1;
              public static int n2 = 20;
              public static final int n3 = 30;
          }
          
      • 解析

        • 虚拟机将常量池内的符号引用替换为直接引用的过程
    • initialization 初始化阶段

      • 到初始化阶段,才真正开始执行类中定义的Java程序代码,此阶段是执行<Clinit>()方法的过程
      • <Clinit>()方法是由编辑器按语句在原文中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。
      • 虚拟机会保证一个类的<Clinit>()方法在多线程环境中被正确的枷锁、同步、如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<Clinit>()方法,其它线程都需要阻塞等待,直到活动线程执行<Clinit>()方法完毕。

七、常用API方法

  • 常用API方法 (Class对象)
方法名功能说明
getName获取全类名
getSimpleName获取简单类名
getFields获取所有public修饰的属性,包含本类以及父类
getDeclaredFields获取本类中的所有方法
getMethods获取所有public 修饰的方法,包含本类及其父类的
getDeclaredMethods获取本类中的所有方法
getConstructors获取所有public修饰的构造器,包含本类
getDeclaredConstructors获取本类中的所有构造器
getPackage以Package形式返回父类信息
getSuperClass以Class形式返回父类信息
getInterfaces以Class[] 形式返回接口信息
getAnnotations以Annotation[] 形式返回注解信息
  • 常用API方法 Field对象

    方法名功能说明
    getModifiers以int形式返回修饰符 [说明 : 默认修饰符 是0 ,public 是1,private 是 2,protected 是 4,static 是 8 ,final 是 16] 如果是两个 public static 那么就会相加 public(1) + static(8) = 9
    getType以Class形式返回类型
    getName返回属性名
  • 常用API方法 Method 对象

    方法名功能说明
    getModifiers以int形式返回修饰符 [说明 : 默认修饰符 是0 ,public 是1,private 是 2,protected 是 4,static 是 8 ,final 是 16] 如果是两个 public static 那么就会相加 public(1) + static(8) = 9
    getReturnType以Class新式获取返回值类型
    getName返回方法名
    getParameterTypes以Class[] 返回参数类型
  • 常用API方法 Constructor 对象

    方法名功能说明
    getModifiers以int形式返回修饰符 [说明 : 默认修饰符 是0 ,public 是1,private 是 2,protected 是 4,static 是 8 ,final 是 16] 如果是两个 public static 那么就会相加 public(1) + static(8) = 9
    getName返回方法名
    getParameterTypes以Class[] 返回参数类型

八、使用反射创建对象、获取字段值、调用方法

1、使用反射创建对象
// 演示对象对象
class User{
    private int age;
    private String name;

    public User() {
    }

    public User(String name) {
        this.name = name;
    }

    private User(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", User.class.getSimpleName() + "[", "]")
                .add("age=" + age)
                .add("name='" + name + "'")
                .toString();
    }
}
  • 获取user类的class对象

    • Class<?> cls = Class.forName("com.xxx.User");
      
  • 无参构造器创建实例

    • Object o = cls.newInstance();
      
  • 使用有参构造器创建实例

    • Constructor<?> constructor = cls.getConstructor(String.class);
      constructor.newInstance("小明");
      
  • 使用私有的有参构造器创建对象

    • setAccessible() : 可以让当前私有的方法,可以访问,暴力反射 ,true开启,false 关闭

    • Constructor<?> constructor = cls.getDeclaredConstructor(int.class, String.class);
      constructor.setAccessible(true);
      constructor.newInstance(18,"小罗");
      
2、使用反射来给属性赋值
class Student{
    public int age;
    private static String name;
    public Student(){}

    @Override
    public String toString() {
        return new StringJoiner(", ", Student.class.getSimpleName() + "[", "]")
                .add("age=" + age + " name="+name)
                .toString();
    }
}
  • 创建对象

    • Object o = cls.newInstance();
      
  • 给公共的属性赋值与获取

    • Field age = cls.getDeclaredField("age");
      age.set(o,18);
      age.get(o)
      
  • 操作私有静态的变量

    • Field name = cls.getDeclaredField("name");
      name.setAccessible(true);
      name.set(null,"小罗");
      name.get(null);
      
    • 注意:静态变量与对象无关,所以可以不用对象来获取,但是单纯私有的还是需要使用对象的,与公共修饰符public一致,只是需要暴力破解

3、使用反射调用方法

class Boss {
    public int age;
    private static String name;

    public Boss(){}
    private static String say(int n,String s,char c){
        return n+" "+s+" "+c;
    }
    public void hi(String s){
        System.out.println("hi "+s);
    }
}
  • 创建对象

    • Class<?> cls = Class.forName("com.xxx.reflection.Boss");
      Object o = cls.newInstance();
      
  • 调用公共方法

    • Method hi = cls.getMethod("hi", String.class);
      hi.invoke(o,"小罗");
      
  • 调用私有静态方法:

    • Method say = cls.getDeclaredMethod("say", int.class, String.class, char.class);
              say.setAccessible(true);
              System.out.println(say.invoke(null, 18, "你好", 'a'));
      
    • 注意:静态修饰的方法与对象无关,所以可以不用对象来获取,但是单纯私有的还是需要使用对象的,与公共修饰符public一致,只是需要暴力破解

九、练习巩固

1、练习一
  • 描述:获取PrivateTest对象,修改name的值为hellokitty 并使用getName打印

  • 使用的对象

    • public class PrivateTest {
          private String name;
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      }
      
  • 答案:

    • //  获取PrivateTest对象,修改name的值为hellokitty 并使用getName打印
      Class<?> cls = Class.forName("com.hgz.javaDemo.reflection.homework.model.PrivateTest");
      // 创建对象
      Object o = cls.newInstance();
      // 获取属性
      Field name = cls.getDeclaredField("name");
      // 暴力破解
      name.setAccessible(true);
      // 设置里面的值
      name.set(o,"hellokitty01");
      // 获取公共方法
      Method getName = cls.getMethod("getName");
      // 输出验证是否成功
      System.out.println(getName.invoke(o));
      
2、练习二
  • 描述:使用Java 内置对象 File 对象来创建一个文件

  • 提示:创建文件要使用File 对象的createNewFile 方法

  • 答案:

    • // 使用Class获取File对象
      Class<?> cls = Class.forName("java.io.File");
      // 打印所有的构造器
      Constructor<?>[] declaredConstructors = cls.getDeclaredConstructors();
      for (Constructor<?> declaredConstructor : declaredConstructors) {
          System.out.println(declaredConstructor);
      }
      // 创建File对象,并创建文件 my.text
      Constructor<?> declaredConstructor = cls.getDeclaredConstructor(String.class);
      Object o = declaredConstructor.newInstance("要创建的文件地址\\my.text");
      // 创建文件
      Method createNewFile = cls.getDeclaredMethod("createNewFile");
      createNewFile.invoke(o);