# 反射基础
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
类的具体的相关方法:
方法返回值 | 方法名称 | 方法说明 |
---|---|---|
Constructor | getConstructor(Class<?>... parameterTypes) | 返回指定参数类型、public 的构造函数对象 |
Constructor<?>[] | getConstructors() | 返回所有 public 的构造函数的 Constructor 对象数组 |
Constructor | getDeclaredConstructor(Class<?>... parameterTypes) | 返回指定参数类型、所有声明的(包括 private)构造函数对象 |
Constructor<?>[] | getDeclaredConstructors() | 返回所有声明的(包括 private)构造函数对象 |
T | newInstance() | 调用无参构造器创建此 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 的相关方法:
方法返回值 | 方法名称 | 方法说明 |
---|---|---|
Field | getDeclaredField(String name) | 获取指定 name 名称的 (包含 private 修饰的) 字段,不包括继承的字段 |
Field[] | getDeclaredFields() | 获取 Class 对象所表示的类或接口的所有 (包含 private 修饰的) 字段,不包括继承的字段 |
Field | getField(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 | |
*/ |
为什么会这样呢?因为编译器优化代码时,发现 id
是 final
且已经被赋值了,所以 getId()
就不是 return id;
而是 return 10;
了。
但是如果 id 没有一开始指定值,而是在构造器或者其他方法中赋值(间接赋值),编译器就无法优化,此时结果都是 10。
# Method 类
这里就不再赘述 Class 获取 Method 对象的方法了。
Method 类的自带的方法需要注意:
方法返回值 | 方法名称 | 方法说明 |
---|---|---|
Object | invoke(Object obj, Object... args) | 对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。 |
Class<?> | getReturnType() | 返回一个 Class 对象,该对象描述了此 Method 对象所表示的方法的正式返回类型,即方法的返回类型 |
Class<?>[] | getParameterTypes() | 按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型。即返回方法的参数类型组成的数组 |
Type[] | getGenericParameterTypes() | 按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型的,也是返回方法的参数类型 |
String | getName() | 以 String 形式返回此 Method 对象表示的方法名称,即返回方法的名称 |
boolean | isVarArgs() | 判断方法是否带可变参数,如果将此方法声明为带有可变数量的参数,则返回 true;否则,返回 false。 |
String | toGenericString() | 返回描述此 Method 的字符串,包括类型参数。 |
# 反射执行流程
这里就不讲源码了,摘录一下全栈知识体系的总结,也是参考 https://www.cnblogs.com/yougewe/p/10125073.html:
- 反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;
- 每个类都会有一个与之对应的 Class 实例,从而每个类都可以获取 method 反射方法,并作用到其他实例身上;
- 反射也是考虑了线程安全的,放心使用;
- 反射使用软引用 relectionData 缓存 class 信息,避免每次重新从 jvm 获取带来的开销;
- 反射调用多次生成新代理 Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;
- 当找到需要的方法,都会 copy 一份出来,而不是使用原来的实例,从而保证数据隔离;
- 调度反射方法,最终是由 jvm 执行 invoke0 () 执行;
反射学了后,建议看看类加载器,本文开头部分推荐了类加载的文章,看完那个后可以看看类加载器,相关链接。
# 参考
https://zhuanlan.zhihu.com/p/107267834
https://www.yuque.com/qingkongxiaguang/javase/muwq85#85bf19a6