“造轮子”-反射机制的三大基本操作


theme: smartblue

我正在参加「掘金·启航计划」

反射概述

反射就是在运行时期,动态的获取类中成员信息(构造器,字段,方法)的过程!
反射存在的作用,在不知道对象的真实类型的情况下去调用对象真实存在的方法,所以再回过来看上面我们抛出的问题,那么使用反射技术就能解决了。

1、字节码对象

在 Java 中,万物皆对象.我们可以通过多个事物,发现他们的共性,来抽象成一个类,类就是对象的模板,而一个个的个体,就是对象. 比如人类和学生.
Class.jpg
当对象多了以后,我们使用类来进行描述所有对象的特征。

那么类多了以后呢 ?

类和类之间也有共性(比如每个类都构造器,每个类都用方法,每个类都有字段),我们java中用Class来描述所有的类的共同特征。

用Class 类 来描述所有的类的特征,所以我们成Class 为类的类型

通过Class 这个类,创建出的对象,成为字节码对象

通过Class来描述所有类的共性的信息,把这些共性的信息以面向对象的思想使用对象进行了封装。所以在Class类中把类中的成员分成了三大类对象来进行管理。分别为构造器对象,方法对象,字段对象。

具体类中的成员信息和对象是怎么样一个对应关系呢?

类中的每一个方法 /每一个字段 都被封装了一个对应的对象。

JDK 中定义好的 Class 类: java.lang.Class

image-20220605192401149.png

该类中有大量的 get 开头的方法.表示可以使用字节码对象来获取信息.所以当我们拿到了字节码对象,就可以直接操作当前字节码中的构造器,方法,字段.

2、 获取字节码对象的三种方式

通过 查看API ,我们得知 Class ,没有公共的构造器,其原因是 Class 对象是在加载类时由 Java 虚拟机自动构造的。

Snip20210815_4.png

该字节码对象不是有我们去创建的,而是自动创建的 。 继续查看API发现获取字节码对象有三种方式:

Snip20210815_3.png

获取字节码的方式:

  1. 通过 Class 类的 forName() 方法来获取字节码对象.

    • Class.forName(String classsName) : 通过类的全限定名获取字节码对象

      1. 全限定名: 包名.类型 例如:Class.forName("java.lang.String");
  2. 通过对象的 getClass() 方法来获取字节码对象

    • 对象.getClass();

      1. User u = new User();
      2. u.getClass(); // 这个 getClass() 方法,是来源于父类 Object 中的
  3. 通过类型(基本类型)的 class 字段来获取字节码对象

    • int.class

      1. 为何基本数据类型可以通过.class属性呢?
      2. jdk文档中是这样描述的
      3. 原文:
      4. The primitive Java types (boolean, byte, char, short, int, long, float, and double), and the keyword void are also represented as Class objects.
      5. 理解:
      6. 基本Java类型(booleanbytecharshortintlongfloatdouble)以及关键字void也表示为类对象。

三种方式的区别:

代码实现:

image-20200426010732744.png

思考:

  1. 三种方式获取到的字节码是同一个吗?

    字节码只会加载一次,所有不管用的哪种方式去获取字节码,都是同一个

  2. int 类型和 int[] 它们的字节码是同一个吗?

    int 类型和int数据类型不是同一个

3、 通过反射,创建类的真实实例对象

我们创建一个实例对象,是通过调用构造方法来完成的。如:

  1. Person p = new Person() ; Person对象,就是通过调用无参数的构造器完成的。

步骤:

1、 获取构造方法对应的 构造器对象

2、 通过该构造器对象,调用构造方法,创建真实对象

获取构造器对象

通过查看API,发现获取构造器对象的方法有四个:

  • 获取所有的构造器对象 public Constructor<?>[] getConstructors(): 获取所有的 public 修饰的构造器 public Constructor<?>[] getDeclaredConstructors(): 获取所有的构造器(包括非public)

  • 获取指定的构造器对象 public Constructor<T> getConstructor(Class... parameterTypes) public Constructor<T> getDeclaredConstructor(Class... parameterTypes): parameterTypes : 参数的类型(构造方法的参数列表的类型).

    注意: 找构造器/方法,传递的是参数的类型.

    结论 : 带着 s 表示获取多个.带着 Declared 表示忽略权限,包括私有的也可以获取到.

准备一个Person类,通过反射来操作Person类中的构造器

  1. public class Person {
  2. public Person(){
  3. System.out.println("这是公共的构造器");
  4. }
  5. public Person(int age){
  6. System.out.println("这是公共的构造器并带一个int类型的参数"+ age);
  7. }
  8. private Person(String name){
  9. System.out.println("这是私有的构造器并带一个字符串类型的参数" + name);
  10. }
  11. private Person(String name,Long age){
  12. System.out.println("这是私有的构造器并带两个参数,一个String类型,一个Long类型" + name + age);
  13. }
  14. }

代码演示:

  1. @Test
  2. public void testGetAllConstructors() throws NoSuchMethodException {
  3. // 获取字节码对象
  4. Class clz = Person.class;
  5. //获取所有 public 构造器
  6. Constructor[] cons1 = clz.getConstructors();
  7. for(Constructor con : cons1){
  8. System.out.println(con);
  9. }
  10. //获取所有构造器,包括 private
  11. Constructor[] cons2 = clz.getDeclaredConstructors();
  12. for(Constructor con : cons2){
  13. System.out.println(con);
  14. }
  15. //获取公共的无参构造器
  16. Constructor con1 = clz.getConstructor();
  17. System.out.println(con1);
  18. //获取公共的带一个参数的构造器
  19. Constructor con2 = clz.getConstructor(int.class);
  20. System.out.println(con2);
  21. //获取指定 private并且带两个参数的 构造器
  22. Constructor con3 = clz.getDeclaredConstructor(String.class, Long.class);
  23. System.out.println(con3);
  24. }

调用构造器方法,创建真实对象

JDK给我们提供一个newInstance的方法,用来创建真实对象

  1. public Object newInstance(Object... initargs)
  2. // initargs: 调用该构造器传递的实际参数.参数列表一定要匹配(类型,个数,顺序).

通过代码演示真实对象的创建

代码演示:

  1. //通过调用公共的带一个参数的构造方法,来创建对象
  2. @Test
  3. public void testCreateObject() throws Exception {
  4. // 获取字节码对象
  5. Class clz = Class.forName("cn.wolfcode._04_reflect.Person");
  6. // 获取公共的带一个参数的构造器,参数为参数类型
  7. Constructor con = clz.getConstructor(int.class);
  8. //调用构造器
  9. Object obj = con.newInstance(24);
  10. System.out.println(obj);
  11. }
  1. //通过调用私有的带两个参数的构造方法,来创建真实类的对象
  2. @Test
  3. public void testCreateObject2() throws Exception {
  4. // 获取带有参数的 private 构造器
  5. Constructor con2 = clz.getDeclaredConstructor(String.class,Long.class);
  6. Object obj2 = con2.newInstance("小狼",12L);
  7. System.out.println(obj2);
  8. }

上述的代码报错了,错误如下,错误信息为非法访问。
image-20200426081810803.png

问题:不能直接访问没有权限(非public)的成员

解决方案: 反射中给出一个可以访问的方案,想要使用反射去操作非public的成员.必须设置一个可以访问的标记.

  1. public void setAccessible(boolean flag): 传递一个true,表示可以访问,表示不管权限.

这个方法的出现是在AccessibleObject 类中,那么API为何这样设计呢?

image-20200426081857403.png

从 API 中我们可以发现,Constructor,Field,MethodAccessibleObject 的子类,

因为这三种成员都可能有被访问private 修饰符修饰的.因此每一个类(Constructor,Field,Method)中要提供setAccessible方法,那放到父类中更为妥当。

所以可以改为如下这种方式获取:

  1. //通过调用私有的带两个参数的构造方法,来创建真实类的对象
  2. @Test
  3. public void testCreateObject() throws Exception {
  4. // 获取带有参数的 private 构造器
  5. Constructor con2 = clz.getDeclaredConstructor(String.class,Long.class);
  6. // 调用私有构造器,必须先设置为可访问
  7. con2.setAccessible(true);
  8. // 创建真实对象
  9. Object obj2 = con2.newInstance("小狼",12L);
  10. System.out.println(obj2);
  11. }

在Class类,同样也提供一个newInstance方法,来创建真实对象的。

我们把这种方式称为——创建对象的快捷方式

创建对象的快捷方式必须满足一个条件: 类中必须提供一个公共的无参数的构造器

代码如下图:

image-20200426082319103.png

经验 :

只要看到传入全限定名,基本上都是要使用反射,通过全限定名来获取字节码对象.

只要看到无指定构造器但是能创建对象,基本上都是要通过Class对象的 newInstance 去创建对象.

4、 通过反射,调用对象中的真实方法

通过反射来调用对象中的方法,必须先要获取到方法对应的方法对象。所以要把上面的目标进行分解,分解为两个小目标:

步骤:

1、 获取方法对应的 方法对象

2、通过方法对象,调用方法

反射获取方法对象

通过查看API,发现获取方法对象的方法有四个:

获取所有方法:

  • public Method[] getMethods(): 可以获取到所有的公共的方法,包括继承的.+
  • public Method[] getDeclaredMethods():获取到本类中所有的方法,包括非public的,不包括继承的.

获取指定的方法:

  • public Method getMethod(String name, Class<?>... parameterTypes):
  • public Method getDeclaredMethod(String name, Class<?>... parameterTypes): name: 方法名 parameterTypes: 当前方法的参数列表的类型.
  • 注意,要找到某一个指定的方法,必须要使用方法签名才能定位到.而方法签名=方法名+参数列表,还记得我们获取构造器的经验吗?带着s表示获取多个,带着declared表示忽略访问权限.

准备一个Person类,通过反射来操作Person类中的方法

  1. public class Person {
  2. public void sayHello(String name){
  3. System.out.println("公共的普通的方法,带一个String类型的参数");
  4. }
  5. private void doWork(String name){
  6. System.out.println("私有的普通方法,带一个String类型的参数");
  7. }
  8. public static void sayHello(String name,Long id){
  9. System.out.println("调用静态方法");
  10. }
  11. private void doWork(){
  12. System.out.println("doWork");
  13. }
  14. }

代码演示

  1. @Test
  2. public void testGetAllMethod() throws Exception {
  3. // 获取字节码对象
  4. Class clz = Class.forName("cn.wolfcode._01_reflect.Person");
  5. //获取所有 public 方法,包括父类的
  6. Method[] methods = clz.getMethods();
  7. for(Method m : methods){
  8. System.out.println(m);
  9. }
  10. System.out.println("------------------");
  11. /获取所有方法,包括 private 不包括父类的
  12. Method[] methods2 = clz.getDeclaredMethods();
  13. for(Method m : methods2){
  14. System.out.println(m);
  15. }
  16. System.out.println("------------------");
  17. //获取指定参数的 public 的方法
  18. Method sayHelloMethod = clz.getMethod("sayHello", String.class);
  19. System.out.println(sayHelloMethod);
  20. System.out.println("------------------");
  21. //获取指定参数的private 方法
  22. Method doWorkMethod = clz.getDeclaredMethod("doWork", String.class);
  23. System.out.println(doWorkMethod);
  24. }

调用方法

通过查看 Method这个类的 API发现 : 给我们提供了一个 invoke 方法 ,来完成真实方法的

  1. public Object invoke(Object obj, Object... args):

通过代码演示方法的被调用

代码实现:

  1. //调用sayHello带一个参数的方法
  2. @Test
  3. public void testGetMethod() throws Exception {
  4. // 步骤1. 获取字节码对象
  5. Class clz = Class.forName("cn.liu.reflect.Person");
  6. // 步骤2. 创建真实实例对象
  7. Object obj = clz.newInstance(); // 使用公共的无参数的构造器
  8. // 步骤3. 获取sayHello方法并且不带参数的方法对象
  9. Method sayHelloM = clz.getMethod("sayHello",String.class);
  10. // 步骤4. 调用方法
  11. sayHelloMethod.invoke(obj, "小liu");
  12. }
  1. //调用sayHello带两个参数的方法
  2. @Test
  3. public void testGetMethod() throws Exception {
  4. // 步骤1. 获取字节码对象
  5. Class clz = Class.forName("cn.liu.reflect.Person");
  6. // 步骤2. 创建真实实例对象
  7. Object obj = clz.newInstance(); // 使用公共的无参数的构造器
  8. // 步骤3. 获取sayHello方法并且不带参数的方法对象
  9. Method sayHelloM = clz.getMethod("sayHello",String.class,Long.class);
  10. // 步骤4. 调用方法
  11. sayHelloMethod.invoke(obj, "小狼",10L);
  12. }
  13. // 步骤4. 调用方法
  14. sayHelloMethod.invoke(null, "小狼",10L);
  1. //调用doWork不带参数的方法
  2. @Test
  3. public void testGetMethod() throws Exception {
  4. // 步骤1. 获取字节码对象
  5. Class clz = Class.forName("cn.wolfcode._01_reflect.Person");
  6. // 步骤2. 创建真实实例对象
  7. Object obj = clz.newInstance(); // 使用公共的无参数的构造器
  8. // 步骤3. 获取sayHello方法并且不带参数的方法对象
  9. Method sayHelloM = clz.getMethod("doWork");
  10. // 步骤4. 设置可访问
  11. doWorkMethod.setAccessible(true);
  12. // 步骤5. 调用方法
  13. sayHelloMethod.invoke(obj);
  14. }
  1. //调用doWork带一个String类型的参数的类修
  2. @Test
  3. public void testGetMethod() throws Exception {
  4. // 步骤1. 获取字节码对象
  5. Class clz = Class.forName("cn.wolfcode._01_reflect.Person");
  6. // 步骤2. 创建真实实例对象
  7. Object obj = clz.newInstance(); // 使用公共的无参数的构造器
  8. // 步骤3. 获取sayHello方法并且不带参数的方法对象
  9. Method sayHelloM = clz.getMethod("doWork",String.class);
  10. // 步骤4. 设置可访问
  11. doWorkMethod.setAccessible(true);
  12. // 步骤5. 调用方法
  13. sayHelloMethod.invoke(obj,"小狼");
  14. }

注意:

  1. 方法也是可以被访问私有修饰符修饰的,所以,如果要访问非 public 修饰的方法,需要在访问之前设置可访问 method.setAccessible(true);
  2. 如果调用的是静态方法,是不需要对象的,所以此时在invoke方法的第一个参数,对象直接传递一个null 即可.

5、通过反射,操作对象中的属性

通过反射来调用对象中的字段,必须先要获取到字段对应的字段对象。所以要把上面的目标进行分解,分解为两个小目标:

步骤:

1、 获取字段对应的字段对象

2、 通过字段对象,修改字段内容

准备一个Person类,通过反射来操作Person类中的字段

  1. public class Person {
  2. private String name;
  3. public Long id;
  4. public Integer age;
  5. }

获取字段对象

通过查看字段(Field)的API发现,操作字段信息的方法有四个:

获取所有字段

public Field[] getFields() public Field[] getDeclaredFields()

获取单个字段:

public Field getField(String name) : name 要获取的字段的名称 public Field getDeclaredField(String name) :

通过代码演示字段对象的获取

  1. @Test
  2. public void testField() throws Exception {
  3. // 获取字节码对象
  4. Class clz = Person.class;
  5. //获取所有公共的字段信息
  6. Field[] fs = clz.getFields();
  7. for(Field f: fs){
  8. System.out.println(f);
  9. }
  10. System.out.println("---------------------");
  11. //获取所有字段信息包括私有
  12. Field[] fs2 = clz.getDeclaredFields();
  13. for(Field f: fs2){
  14. System.out.println(f);
  15. }
  16. System.out.println("----------------------");
  17. //获取自定名称的字段对象:name
  18. Field nameField = clz.getDeclaredField("name");
  19. System.out.println(nameField);
  20. }

操作字段

通过查看文档API,发现操作字段的就是set开头 和 get开头方法

  1. get(Object obj);
  2. set(Object obj,Object value)

通过代码演示操作字段的内容

  1. @Test
  2. public void testField() throws Exception {
  3. // 获取字节码对象
  4. Class clz = Person.class;
  5. // 注意:如果给字段设置内容,
  6. // 必须保证设置字段传入的对象和获取字段传入的对象是同一个
  7. Object obj = clz.newInstance();
  8. // 获取单个字段
  9. Field nameField = clz.getDeclaredField("name");
  10. System.out.println(nameField);
  11. // 设置私有字段可访问
  12. nameField.setAccessible(true);
  13. // 操作name字段
  14. // 设置那么字段的数据
  15. nameField.set(obj, "小狼");
  16. // 获取name字段的数据
  17. Object nameValue = nameField.get(obj);
  18. System.out.println(nameValue);
  19. }

文章标签:

原文连接:https://juejin.cn/post/7105725723406663694

相关推荐

Flask框架——消息闪现

34个图片压缩工具集合,包含在线压缩和CLI工具

入门即享受!coolbpf 硬核提升 BPF 开发效率 | 龙蜥技术

基于 OPLG 从 0 到 1 构建统一可观测平台实践

全链路灰度在数据库上我们是怎么做的?

冴羽答读者问:过程比结果重要吗?如果是,怎么理解?如果不是,又怎么解?

接口文档管理工具,选yapi 还是 Apifox? 这里列出了两款软件的深度分析,看完再下载不迟。

基于 Docker 来部署 Vue 或 React 前端项目及 Node 后端服务

三十岁的我,自由了!

如何实现带timeout的input?

统计千行代码Bug率,有没有意义?

814. 二叉树剪枝 : 简单递归运用题

【综合笔试题】难度 3.5\u002F5,多解法热门二叉树笔试题

为什么设计的软件不好用?那是因为不熟悉软件开发模型!一文熟悉软件开发模型

作为前端,我是这样从零实现CI\u002FCD二(node服务部署及前后端联调)

极智开发 | 讲解 Nginx 特性之一:反向代理

Netty 案例之 IM 方案设计

从 Google 离职,前Go 语言负责人跳槽小公司

最终一致性性分布式事务 TCC

不谈源码,聊聊位运算的实际应用