JAVA Class类与反射

2022-08-30 10:10:34 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

Class类与Class对象

Class对象是Class类的实例,类至少包含以下信息,因此class类又可以被 解构为如下部分:

  • 权限修饰符
  • 类名
  • 参数化类型(泛型信息)
  • 接口Interface
  • 注解Annotation
  • 字段Field(重点)
  • 构造器Constructor(重点)
  • 方法Methd(重点)

以下图为例:

整个.class文件最终都成为字节数组byte[] b,里面的构造器、方法等各个“组件”,其实也是字节。 打开Class类的源代码,发现果然如此:

代码语言:javascript复制
private static class ReflectionData<T> {
        volatile Field[] declaredFields;
        volatile Field[] publicFields;
        volatile Method[] declaredMethods;
        volatile Method[] publicMethods;
        volatile Constructor<T>[] declaredConstructors;
        volatile Constructor<T>[] publicConstructors;
        // Intermediate results for getFields and getMethods
        volatile Field[] declaredPublicFields;
        volatile Method[] declaredPublicMethods;
        volatile Class<?>[] interfaces;

        // Value of classRedefinedCount when we created this ReflectionData instance
        final int redefinedCount;

        ReflectionData(int redefinedCount) {
            this.redefinedCount = redefinedCount;
        }
    }

而且,针对字段、方法、构造器,因为信息量太大了,JDK还单独写了三个类,比如

Method类:

Field类:

Constructor类:

也就是说,Class类准备了很多东西来标识一个.class文件的信息,并写了三个类,Method,Fileld,Constructor来描述方法、字段、构造器等信息。 比如:

也就是说,Class类准备了很多字段用来表示一个.class文件的信息,对于字段、方法、构造器等,为了更详细地描述这些重要信息,还写了三个类,每个类里面都有很详细的对应

理解反射API

没啥好说的,在日常开发中反射最终目的主要两个:

  • 创建实例
  • 反射调用方法

创建实例的难点在于,很多人不知道clazz.newInstance()底层还是调用Contructor对象的newInstance()。所以,要想调用clazz.newInstance(),必须保证编写类的时候有个无参构造。

反射调用方法的难点,有两个,初学者可能会不理解。

在此之前,先来理清楚Class、Field、Method、Constructor四个对象的关系:

Field、Method、Constructor对象内部有对字段、方法、构造器更详细的描述。

  • 难点一:为什么根据Class对象获取Method时,需要传入方法名 参数的Class类型

为什么要传 nameParameterType? 因为.class文件中有多个方法,比如:

所以必须传入name,以方法名区分哪个方法,得到对应的Method。

那参数parameterTypes为什么要用Class类型,我想和调用方法时一样直接传变量名不行吗,比如userName, age。

答案是:我们无法根据变量名区分方法

实际上,调用Class对象的getMethod()方法时,内部会循环遍历所有Method,然后根据方法名和参数类型匹配唯一的Method返回。

代码语言:javascript复制
private static Method searchMethods(Method[] methods,
                                        String name,
                                        Class<?>[] parameterTypes)
    {
        Method res = null;
        String internedName = name.intern();
        for (int i = 0; i < methods.length; i  ) {
            Method m = methods[i];
            if (m.getName() == internedName
                && arrayContentsEq(parameterTypes, m.getParameterTypes())
                && (res == null
                    || res.getReturnType().isAssignableFrom(m.getReturnType())))
                res = m;
        }

        return (res == null ? res : getReflectionFactory().copyMethod(res));
    }

难点二:调用method.invoke(obj, args);时为什么要传入一个目标对象?

上面分析过,.class文件通过IO被加载到内存后,JDK创造了至少四个对象:Class、Field、Method、Constructor,这些对象其实都是0101010的抽象表示。

以Method对象为例,它到底是什么,怎么来的?我们上面已经分析过,Method对象有好多字段,比如name(方法名),returnType(返回值类型)等。也就是说我们在.java文件中写的方法,被“解构”以后存入了Method对象中。所以对象本身是一个方法的映射,一个方法对应一个Method对象。

对象的本质就是用来存储数据的。而方法作为一种行为描述,是所有对象共有的,不属于某个对象独有。比如现有两个Person实例

代码语言:javascript复制
    Person p1 = new Person();
    Person p2 = new Person();

对象 p1保存了”hst”和18,p2保存了”cxy”和20。但是不管是p1还是p2,都会有changeUser(),但是每个对象里面写一份太浪费。既然是共性行为,可以抽取出来,放在方法区共用。

但这又产生了一个棘手的问题,方法是共用的,JVM如何保证p1调用changeUser()时,changeUser()不会跑去把p2的数据改掉呢?

所以JVM设置了一种隐性机制,每次对象调用方法时,都会隐性传递当前调用该方法的对象参数,方法可以根据这个对象参数知道当前调用本方法的是哪个对象!

同样的,在反射调用方法时,本质还是希望方法处理数据,所以必须告诉它执行哪个对象的数据。

所以,把Method理解为方法执行指令吧,它更像是一个方法执行器,必须告诉它要执行的对象(数据)。

当然,如果是invoke一个静态方法,不需要传入具体的对象。因为静态方法并不能处理对象中保存的数据。

参考文章: 【1】https://www.zhihu.com/question/24304289/answer/694344906

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/145162.html原文链接:https://javaforall.cn

0 人点赞