Java笔记——反射

反射机制 Reflection

根据配置文件properties指定的信息,创建对象并调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用Properties类,读写配置文件
Properties prop = new Properties();
prop.load(new FileInputStream("src\\xxx.properties"));
String classfullpath = prop.get("classfullpath").toString();
String method = prop.get("method").toString();

// 创建对象 使用反射机制
// a. 加载类, 返回Class类型的对象cls
Class cls = Class.forName(classfullpath);
// b. 通过 cls 得到加载类 com.company.Cat 的对象实例
Object o = cls.getDeclaredConstructor().newInstance();
System.out.println(o.getClass()); // 运行类型
// c. 通过 cls 得到加载的类 com.company.Cat 的 method “hi” 的方法对象
Method method1 = cls.getMethod(method);
// d. 通过 method1 调用方法, 即通过方法对象来实现调用方法
method1.invoke(o); // 反射机制: 方法.invoke(对象) 传统方法:对象.方法()

通过外部文件配置,在不修改源码的情况下,来控制程序,也符合设计模式的OCP原则(开闭原则:不修改源码,扩容功能)

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

Class 类

  1. Class也是类,因此也继承Object类
  2. Class类对象不是new出来的,而是系统创建的 loadClass()
1
2
3
4
// ClassLoader.java
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
  1. 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
  2. 每个类的实例都会记得自己是由哪个Class实例所生成
  3. 通过Class可以完整地得到一个类的结构,通过一系列API
  4. Class对象是存放在
  5. 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据

获取Class类对象

  • 代码阶段 Class.forName() 【多用于配置文件,读取类全路径,加载类】
  • 类加载器 类加载器得到Class对象
1
2
3
4
// 先得到类加载器 car
ClassLoader classLoader = car.getClass().getClassLoader();
// 通过类加载器得到Class对象
Class cls = classLoader.loadClass(classAllPath);
  • Class类阶段 类名.class 【多用于参数传递】

  • Runtime运行阶段 对象.getClass() 【通过创建好的对象,获取Class对象】

  • Class cls = 基本数据类型.class

  • Class cls = 包装类.TYPE

哪些类型有Class对象

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

类加载

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

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

  • 类加载的时机
    • 当创建对象时 new
    • 当子类被加载时, 父类也加载
    • 直接调用类中的静态成员时
    • 通过反射 // 动态

类加载过程

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

  • 将类的二进制数据合并到jre中

  • JVM负责对类进行初始化,这里主要是指静态成员

类加载的5个阶段

  1. 加载过程

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

  1. 链接阶段——验证

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

    • JVM会在该阶段对静态变量,分配内存并默认初始化(对应数据类型的默认初始值,如0, 0L, null, false等)。这些变量所使用的内存都将在方法区中进行分配。
      1
      2
      3
      4
      5
      6
      7
      8
      // 属性——成员变量——字段
      // 类加载的链接阶段——准备 属性是如何处理
      // 1. n1是实例属性,不是静态变量,因此在准备阶段不会分配内存
      // 2. n2是静态变量,分配内存n2是默认初始化0,而不是20
      // 3. n3是常量,他和静态变量不一样,因为一旦赋值就不变 n3=30
      public int n1 = 10;
      public static int n2 = 20;
      public static final int n3 = 30;
  3. 链接阶段——解析

    • 虚拟机将常量池内的符号引用替换为直接引用的过程。
    • A类和B类的关联
  4. 初始化 Initialization

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

1
2
3
4
5
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// ...
}
}

反射获取类的结构信息

  • Class
  • Field
  • Method
  • Constructor
  • 访问属性
  • 访问方法

java.lang.Class

1
2
3
4
5
6
7
8
9
Class<?> personCls = Class.forName("com.conpany.reflection.Preson");
Constructor<?>[] constructors = personCls.getConstructors();
for (Constructor<?> constructor : constructors) {
System.out.println("所有public修饰的构造器(只有本类) = " + constructor.getName());
}
// getDeclaredConstructors();
// getDeclaredMethods();
// getFields();
// Annotation[] annotations = personCls.getAnnotations();

java.lang.reflect.Field

  1. getModifiers:以 int 形式返回修饰符 【默认修饰符是0,public是1,private是2,protected是4,satatic是8,final是16】
  2. getType:以Class形式返回类型
  3. getName:返回属性名

java.lang.reflect.method

  1. getModifiers:以 int 形式返回修饰符
  2. getReturnType:以Class形式获取返回类型
  3. getName:返回方法名
  4. getParameterTypes:以Class[]返回参数类型数组

反射相关类

1
2
3
4
java.lang.Class // 代表一个类,Class对象表示某个类加载后在堆中的对象
java.lang.reflect.Method // 代表类的方法
java.lang.reflect.Field // 代表类的成员变量
java.lang.reflect.Constructor // 代表类的构造方法
1
2
3
4
5
6
// getField不能得到私有属性
Field nameField = cls.getField("name");
nameField.get(o); // 反射:成员变量对象.get(对象); 传统写法:对象.成员变量

Constructor constructor = cls.getConstructor(); // 无参构造器
Constructor cons2 = cls.getConstructor(String.class); // 有参构造器

反射调用性能优化

反射优点和缺点 > 优点:可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑。 > 缺点:使用反射基本是解释执行,对执行速度有影响。

反射调用优化——关闭访问检查 1. Method和Field、Constructor对象都有setAccessible()方法 2. setAccessible()作用是启动和禁用访问安全检查的开关 3. 参数值为true表示反射的对象在使用时取消访问检查,提高反射的效率。

Class 类常用方法

image-20221101180050510

通过反射创建对象

  1. 方式一:调用类中的public修饰的无参构造器class
  2. 方式二:调用类中的指定构造器
  3. Class类相关方法 newInstance:调用类中的无参构造器,获取对应类的对象 getConstrctor(Class... cls):根据参数列表,获取对应的构造器对象 getDecalaredConstructor(Class... cls)
  4. Constructor类相关方法 setAccessible:暴破 newInstance(Object... ob):调用构造器

通过反射访问类中的属性

  1. 根据属性名获取Field对象 Field f = clazz对象.getDeclaredField(属性名);
  2. 暴破 f.serAccessible(true);
  3. 访问 f.set(o, 值); // o表示对象 syso(f.get(o));
  4. 如果是静态属性,则set和get中的参数o,可以写成null

通过反射反射类的方法

  1. 根据方法名和参数列表获取Method方法对象 Method m = clazz.getDeclaredMethod(方法名, XX.class);
  2. 获取对象 Object o = clazz.newInstance();
  3. 暴破
  4. 访问 Object returnValue = m.invoke(o, 实参列表);
  5. 静态方法,invoke的参数o,可以写成null

Java笔记——反射
https://nessaj7.github.io/2022/11/05/afcd682ebafd.html
作者
kuhn
发布于
2022年11月5日
许可协议