一, 引言
反射机制是一个非常好用的机制, C# 和 Java 中都有反射, 反射机制简单来说就是在程序运行状态时, 对于任意一个类, 能够知道这个类的所有属性和方法, 对于任意一个对象, 能够调用它的任意属性和方法, 其实初听就知道反射是一个比较暴力的机制, 它可能会破坏封装性.
通过反射的定义我们可以想到反射的好处: 可以灵活的编写代码, 代码可以在运行时装配, 降低代码的耦合度, 动态代理的实现也离不开反射.
为了更好的理解反射, 我们先了解下 JVM 中的类加载机制.
二, 类加载机制
当程序要使用某个类时, 如果这个类还未加载到内存, 则需要将其加载到内存中, JVM 会通过加载, 连接, 初始化三个步骤来对该类进行初始化.
2.1 类加载
类的加载由类加载器完成, 类加载器通常由 JVM 提供的, JVM 提供的类加载器通常称为系统加载器, 开发者可以通过继承 ClassLoader 基类来创建自己的类加载器.
类加载加载的是一个二进制数据, 这些数据的来源有几种:
本地文件系统加载 class 文件.
从 JAR 包中记载 class 文件.
通过网络加载 class 文件.
把一个 java 文件动态编译, 并执行加载
类加载不一定是要等到首次使用时才加载, 虚拟机允许系统预先加载某些类
2.2 类连接
在类被加载后, 系统会为之生成一个对应的 Class 对象, 接着进入连接阶段, 连接阶段是把类的二进制数据合并到 JRE 中, 类连接分为三个阶段:
验证: 检验被加载的类是否有正确的内部结构, 并和其他的类协调一致.
准备: 负责为类变量分配内存, 并设置默认初始化值.
解析: 将类的二进制数据中的符号引用替换成直接引用.
2.3 类的初始化
虚拟机负责对类进行初始化, 主要是对变量进行初始化, 对类变量指定初始值有两种方式:
声明类变量时指定初始值.
使用静态初始化块为类变量指定初始值.
JVM 初始化的几个步骤:
假如该类还没有被加载和连接, 则程序先加载或连接该类.
假如该类的直接父类没有初始化, 则先初始化这该类的父类.
假如类中有初始化语句, 则系统依次执行这些初始化语句.
可以看出当程序主动使用某个类时, 一定会保证该类及其所有父类都被初始化. 那么在什么情况下系统会初始化一个类活着接口呢?
创建类的实例, 既包括使用 new 来创建, 也包括通过反射来创建和反序列化来创建.
调用某个类的静态方法.
访问某个类或接口的类变量.
使用反射方式来强制创建某个类或接口对应的 java.jang.class 对象.
初始化某个类的子类.
使用 java.exe 命令来运行某个主类.
三, 类加载器
类加载器是负责将. class 文件加载到内存中, 并生成对应的 java.lang.Class 实例, 一旦一个类被载入到 JVM 中, 就不会被载入了, 这里就存在一个唯一标识的问题, JVM 是通过其全限定类名和其加载器来做唯一标识的, 即是通过包名, 类名, 及加载器名. 这也意味着不同类加载器加载的同一个类是不同的.
3.1 类加载机制
当 JVM 启动时, 会形成三个类加载器组成的初始类加载器层次结构:
Bootstrap ClassLoader: 根类加载器, 负责加载 Java 的核心类, 是由 JVM 自身提供的.
Extension ClassLoader: 扩展类记载器, 负责加载 JRE 的扩展目录 (%JAVA_HOME%/jre/lib/ext) 中 JAR 包中的类.
System ClassLoader: 系统类加载器, 负责在 JVM 启动时加载来自 Java 命令的 - classpath 选项, java.class.path 系统属性或 CLASSPATH 环境变量指定的 JAR 包和类路径.
JVM 的类加载机制主要有三种:
全盘负责: 就是当一个类加载器负责加载某个 Class 时, 该 class 所依赖的和引用的其他 Class 也将由该类加载器负责载入, 除非显示使用另外一个类加载器载入.
父类委托: 先让父类加载器试图加载该 Class, 只有当父类加载器无法加载该类时才尝试从自己的类路径中加载该类.
缓存机制: 所有加载过的 Class 都会被缓存, 这就是为什么修改代码后必须重新启动 JVM 修改才会生效的原因.
类加载器加载 Class 大致如下:
检测此 Class 是否载入过(通过缓存查询), 跳至第 8 步.
如果父类加载器不存在, 则跳至第 4 步, 如果父类加载器存在, 则执行第 3 步.
请求使用父类加载器去载入目标类, 成功则跳到第 8 步, 否则跳到 5 步.
请求使用根类加载器来载入目标类, 成功则调到第 8 步, 否则跳至 7 步.
当前类加载器尝试寻找 Class 文件, 找到执行第 6 步, 找不到则跳入 7 步.
从文件中载入 Class, 成功跳入第 8 步.
抛出 ClassNotFoundException 异常.
返回对应的 java.lang.Class 对象.
3.2 自定义类加载器
JVM 中除了根类加载器外的所有类加载器都是 ClassLoader 子类的实例, 我们可以扩展 ClassLoader 的子类, 并重写其中的方法来实现自定义加载器. ClassLoader 有两个关键方法:
loadClass(String name, boolean resolve): 该方法为 ClassLoader 的入口点, 根据指定名称来加载类
findClass(String name): 根据指定名称来查找类.
通常推荐 findClass(String name), 重写 findClass()方法可以避免覆盖默认类加载器的父类委托和缓存机制两种策略. ClassLoader 中还有一个核心方法 Class<?> defineClass(String name, byte[] b, int off, int len), 该方法负责将指定类的字节码文件读入到数组中, 并把它转换成 Class 对象, 不过这个方法是 final, 不需要我们重写.
四, 反射
前面的通过类加载机制我们知道, 每个类被加载后, 系统会为该类生成一个对应的 Class 对象, 通过这个对象就可以访问到 JVM 中的这个类, 在程序中获取到 Class 的方式有三种:
使用 Class 类中的 forName(String name)静态方法, 传入的参数是某个类的全限定名.
调用某个类的 class 属性来获取该类对应的的 Class 对象.
调用某个对象的 getClass()方法.
在第一个和第二个方法中, 都是通过类来获取到 Class 对象, 但是很明显第二种更安全也效率更高.
4.1 获取 Class 信息
当我们获取到 Class 对象后我们可以根据 Class 来获取信息, Class 类提供了大量的方法来获取 Class 对应类的详细信息, 类中主要的信息包括: 构造函数, 方法, 属性, 注解, 另外还有一些基本属性如: 类的修饰符, 类名, 所在包等.
构造函数:
Constructor<?>[] getConstructors(): 返回此 Class 对象对应类的所有 public 构造函数.
Constructor<?>[] getDeclaredConstructors(): 返回此 Class 对象对应类的所有构造函数, 无权限限制
Constructor<T>getConstructor(Class<?>... parameterTypes): 返回此 Class 对象对应类的, 带指定形参的列表的 public 构造函数.
Constructor<T>getDeclaredConstructor(Class<?>... parameterTypes): 返回此 Class 对象对应类的, 带指定形参的列表的构造函数, 无权限限制.
方法中存在 Declared 表示无权限限制, 后面的也与此相同, 后面就不列出
方法:
Method getMethod(String name, Class<?>... parameterTypes): 返回此 Class 对象对应类的, 带指定形参的列表的 public 方法.
Method[] getMethods(): 返回此 Class 对象对应类的所有 public 方法
成员变量:
Field[] getFields(): 返回此 Class 对象对应类的所有 public 成员变量.
Field[] getField(String name): 返回此 Class 对象对应类的, 指定名称的 public 成员变量
注解:
Annotation[] getAnnotations(): 返回修饰该 Class 对象对应类的所有注解
<A extends Annotation>A getAnnotation(Class<A>annotationClass): 获取该 Class 对象对应类存在的, 指定类型的注解, 如果不存在, 则返回 null.
内部类:
Class<?>[] getDeclaredClasses(): 获取该 Class 对象对应类里包含的全部内部类.
外部类:
Class<?> getDeclaringClass(): 获取该 Class 对象对应类里包含的所在的外部类
接口:
Class<?>[] getInterfaces(): 获取该 Class 对象对应类里所实现的全部接口
基本信息:
int getModifiers(): 返回此类或接口的所有修饰符, 返回的 int 类型需要解码.
Package getPackage(): 获取此类包名.
String getName(): 获取该 Class 对象对应类的名称.
String getSimpleName(): 获取该 Class 对象对应类的简称.
boolean isAnnotation(): 返回 Class 对象是否表示一个注解类型.
boolean isArray():Class 对象是否是一个数组.
这里将大体能够获取的类信息列出来了:
- public class ClassTest {
- private ClassTest() {
- }
- public ClassTest(String name) {
- System.out.println("有参数的构造函数");
- }
- public void info() {
- System.out.println("无参数的方法");
- }
- public void info(String name) {
- System.out.println("有参数的方法");
- }
- // 内部类
- class inner {
- }
- public static void main(String[] args) throws NoSuchMethodException {
- Class<ClassTest> clazz = ClassTest.class;
- Constructor<?>[] constructors = clazz.getConstructors();
- System.out.println("全部 public 构造器如下:");
- for (Constructor constructor : constructors) {
- System.out.println(constructor);
- }
- Constructor<?>[] pubConstructors = clazz.getDeclaredConstructors();
- System.out.println("全部构造器如下:");
- for (Constructor constructor : pubConstructors) {
- System.out.println(constructor);
- }
- Method[] methods = clazz.getMethods();
- System.out.println("全部 public 方法如下");
- for (Method method : methods) {
- System.out.println(method);
- }
- System.out.println("名称为 info, 并且入参为 String 类型的方法:" + clazz.getMethod("info", String.class));
- }
- }
4.2 生成并操作对象
4.2.1 生成对象
生成对象的方式有两种, 一种是直接调用 newInstance()方法, 这种方式是用默认构造器来创建对象, 还有一种方式是先获得 Constructor 对象, 然后用 Constructor 调用 newInstance()来创建对象.
- Class<ClassTest> clazz = ClassTest.class;
- ClassTest classTest = clazz.newInstance();
- classTest.info();
- Constructor<ClassTest> constructor = clazz.getConstructor(String.class);
- ClassTest class2 = constructor.newInstance("你好");
- class2.info();
4.2.2 调用方法
上面的例子中, 我们可以明确的知道返回的是哪个类, 所有调用的方法也和之前对象调用方法没有区别, 但是一般在用反射机制时, 我们是不知道具体类的, 这个时候我们可以使用 getMethod 获取方法, 然后使用 invoke 来进行方法调用:
- Class<?> aClass = Class.forName("com.yuanqinnan.api.reflect.ClassTest");
- // 创建了对象
- Object object = aClass.newInstance();
- // 获取到方法
- Method info = aClass.getMethod("info", String.class);
- // 调用方法
- info.invoke(object, "你好");
4.2.3 访问成员变量
一般情况下, 我们会使用 getXXX()方法和 setXXX(XXX)方法来设置或者获取成员变量, 但是有了反射后, 我们可以直接对成员变量进行操作:
- public class Person {
- private int age;
- private String name;
- public String toString() {
- return name + ":" + age;
- }
- public static void main(String[] args) throws Exception {
- Person p = new Person();
- Class<Person> personClass = Person.class;
- Field name = personClass.getDeclaredField("name");
- // 去掉访问限制
- name.setAccessible(true);
- name.set(p, "张三");
- Field age = personClass.getDeclaredField("age");
- age.setAccessible(true);
- age.set(p, 20);
- System.out.println(p.toString());
- }
- }
从这里可以看出来, 反射破坏了封装性.
来源: https://www.cnblogs.com/yuanqinnan/p/11050300.html