# 反射基础

RTTI(Run-Time Type Identification)运行时类型识别。在《Thinking in Java》一书第十四章中有提到,其作用是在运行时识别一个对象的类型类的信息。主要有两种方式:一种是 “传统的” RTTI,它假定我们在编译时已经知道了所有的类型;另一种是 “反射” 机制,它允许我们在运行时发现和使用类的信息。

反射就是把 java 类中的各种成分映射成一个个的 Java 对象(比如 Method 对象, Field 对象)。

Q:反射如何允许我们在运行时使用类信息?这些类信息哪来的?

# Class 类

Class 类的实例表示 java 应用运行时的类 (class ans enum) 或接口 (interface and annotation),位于 java.lang 包下。

  • 类被编译后会产生一个 Class 对象,表示的是创建的类的类型信息,而且这个 Class 对象保存在同名.class 的文件中 (字节码文件)

  • 每个通过关键字 class 标识的类,在内存中有且只有一个与之对应的 Class 对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个 Class 对象。

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

  • Class 类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要。

类加载机制是 JVM 相关知识,可以参考:类加载机制

# 获取 Class 类

public static void main(String[] args) throws ClassNotFoundException {
    // 使用 class 关键字,通过类名获取
    Class<String> clazz = String.class;   
    
    // 使用 Class 类静态方法 forName (),通过包名。类名获取,注意返回值是 Class<?>
    Class<?> clazz2 = Class.forName("java.lang.String");
    
    // 通过实例对象获取
    Class<?> clazz3 = new String("cpdd").getClass();  
}

注意,第一种指定了泛型,后两种使用了 class<?> ,其实都是同一个 class 对象。,在 JVM 中每个类始终只存在一个 Class 对象,无论通过什么方法获取,都是一样的。

  • 基本数据类型也可以获取 class 对象,包装类也可以,这两个不是相同的:
boolean isSame1 = int.class == Integer.class; // false
boolean isSame2 = int.class == Integer.TYPE; // true

包装类中定义如下:

@SuppressWarnings("unchecked")
public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
  • 数组类型: Class<String[]> clazz = String[].class;

# 使用 Class 对象

使用 Class 类的很多方法就是所见即所得,大部分用不到的话,就需要知道有这么个东西即可。

获得类名

  • getName() :获得全限定的类名(类的完整名字),jvm 中 Class 的表示,可以用于动态加载 Class 对象。
  • getSimpleName() :只获取类型
  • getCanonicalName() :也是全限定类名,和 getName 一样的。

getCanonicalName 返回更容易理解的表示,主要用于输出(toString)或 log 打印,大多数情况下和 getName 一样,但是在内部类、数组等类型的表示形式就不同了。

获得 Class

  • getSuperclass() :获取父类的 Class 对象。
  • getInterfaces() :返回 Class 对象数组,表示 Class 对象所引用的类所实现的所有接口。
  • isInterface() :判断 Class 对象是否表示一个接口。

获得字段

  • getFields() :获得所有公共字段,包括父类的公共字段,类似的还有 getMethod()getConstructors()
  • getDeclaredFields() :该方法就不会获取父类的公共字段,只会获取自己声明的字段(包括私有),类似的还有 getDeclaredMethods()getDeclaredConstructors()

# Constructor 类

Constructor 类存在于反射包 (java.lang.reflect) 中,反映的是 Class 对象所表示的类的构造方法。

要获取指定类的 Constructor 类需要使用 Class 对象提供的对应的 getConstructors 方法,对于一个方法来说,方法名和参数列表限定了一个方法唯一,所以我们如果想要获取一个特定的(构造)方法,就可以使用 getConstructor(Class<?>... parameterTyoes) ,指定参数列表的信息即可。

Class 类关于 Constructor 类的具体的相关方法:

方法返回值方法名称方法说明
ConstructorgetConstructor(Class<?>... parameterTypes)返回指定参数类型、public 的构造函数对象
Constructor<?>[]getConstructors()返回所有 public 的构造函数的 Constructor 对象数组
ConstructorgetDeclaredConstructor(Class<?>... parameterTypes)返回指定参数类型、所有声明的(包括 private)构造函数对象
Constructor<?>[]getDeclaredConstructors()返回所有声明的(包括 private)构造函数对象
TnewInstance()调用无参构造器创建此 Class 对象所表示的类的一个新实例。

上面的都是 Class 的方法,我们获得了 Constructor 如何使用呢?

@Data
public class Parent {
    private String name;
    public Parent(String name) {
        this.name = name;
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> c = Parent.class;
        // 通过指定参数列表,获取指定构造器
        Constructor<?> constructor = c.getConstructor(String.class);
        Parent p = (Parent) constructor.newInstance("Cyan");
        System.out.println(p.getName());
    }
}

如果构造方法是私有的:

@Data
public class Parent {
    private String name;
    private Parent(String name) {
        this.name = name;
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> c = Parent.class;
        // 通过指定参数列表,获取指定构造器
        Constructor<?> constructor = c.getDeclaredConstructor(String.class);
        
        //private 必须设置可访问
        constructor.setAccessible(true);
        Parent p = (Parent) constructor.newInstance("Cyan");
        System.out.println(p.getName());
    }
}

对于一个 Constructor 类,还可以获取其参数列表的参数类型:

Class<?>[] clazzs = constructor.getParameterTypes();

# Field 类

Constructor,Field,Method 这几个类的方法设计其实都差不多的。

Class 对象获取 Field 的相关方法:

方法返回值方法名称方法说明
FieldgetDeclaredField(String name)获取指定 name 名称的 (包含 private 修饰的) 字段,不包括继承的字段
Field[]getDeclaredFields()获取 Class 对象所表示的类或接口的所有 (包含 private 修饰的) 字段,不包括继承的字段
FieldgetField(String name)获取指定 name 名称、public 的字段,包含继承字段
Field[]getFields()获取修饰符为 public 的字段,包含继承字段

你应该注意的是,在获取这三个类获取自己的实例时是如何设计的,Constructor 的方法名是固定的,所以只需要传入参数列表即可,Field 只需要传入属性名,Methd 需要传入方法名和参数列表

Field 方法

  • set/get :需要指定操作的对象, set(parent1, "cyan")/get(parent1)
  • getName() :此 Field 对象表示的字段的名称。
  • getDeclaringClass() :声明该字段的类。
  • setAccessible(boolean flag) :设置其可访问性。

需要特别注意的是,如果该字段在类中被 final 修饰,通过反射可以修改吗?答案是可以的,只不过需要修改一下访问属性: setAccessible(true) 。所以说,反射的权限很高,要谨慎使用。但是反射修改 final 时,要考虑编译器优化的因素,比如:

@Data
public class Parent {
    public final int id = 10;
    
   	//lombok 自动加了 getId
}
public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Parent.class;
        Field f = clazz.getField("id");
        Parent p = new Parent();
        System.out.println("修改前:" + p.getId());
        f.setAccessible(true);
        f.set(p,11);
        System.out.println("修改后,通过getId()获取:" + p.getId());
        System.out.println("修改后,通过反射获取:" + f.get(p));
    }
}
/**
结果:
修改前:10
修改后,通过 getId () 获取:10
修改后,通过反射获取:11
*/

为什么会这样呢?因为编译器优化代码时,发现 idfinal 且已经被赋值了,所以 getId() 就不是 return id; 而是 return 10; 了。

但是如果 id 没有一开始指定值,而是在构造器或者其他方法中赋值(间接赋值),编译器就无法优化,此时结果都是 10。

# Method 类

这里就不再赘述 Class 获取 Method 对象的方法了。

Method 类的自带的方法需要注意:

方法返回值方法名称方法说明
Objectinvoke(Object obj, Object... args)对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
Class<?>getReturnType()返回一个 Class 对象,该对象描述了此 Method 对象所表示的方法的正式返回类型,即方法的返回类型
Class<?>[]getParameterTypes()按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型。即返回方法的参数类型组成的数组
Type[]getGenericParameterTypes()按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型的,也是返回方法的参数类型
StringgetName()以 String 形式返回此 Method 对象表示的方法名称,即返回方法的名称
booleanisVarArgs()判断方法是否带可变参数,如果将此方法声明为带有可变数量的参数,则返回 true;否则,返回 false。
StringtoGenericString()返回描述此 Method 的字符串,包括类型参数。

# 反射执行流程

这里就不讲源码了,摘录一下全栈知识体系的总结,也是参考 https://www.cnblogs.com/yougewe/p/10125073.html:

  1. 反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;
  2. 每个类都会有一个与之对应的 Class 实例,从而每个类都可以获取 method 反射方法,并作用到其他实例身上;
  3. 反射也是考虑了线程安全的,放心使用;
  4. 反射使用软引用 relectionData 缓存 class 信息,避免每次重新从 jvm 获取带来的开销;
  5. 反射调用多次生成新代理 Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;
  6. 当找到需要的方法,都会 copy 一份出来,而不是使用原来的实例,从而保证数据隔离;
  7. 调度反射方法,最终是由 jvm 执行 invoke0 () 执行;

反射学了后,建议看看类加载器,本文开头部分推荐了类加载的文章,看完那个后可以看看类加载器,相关链接

# 参考

https://zhuanlan.zhihu.com/p/107267834

https://www.yuque.com/qingkongxiaguang/javase/muwq85#85bf19a6