反射

在讲JDK动态代理之前,我先将反射机制疏离一遍。
该博文非原创,大部分抄录自zhihu用户:bravo1988的原创文章。主要用于本人学习梳理

主要内容

  • JVM是如何构建一个实例的
  • 类加载器
  • Class类
  • 反射API

1.JVM是如何构建一个实例的

1.1. 一些名词:

  • 内存:即JVM内存,其中被人为的划分为栈,堆,方法区等等
  • .class文件:就是字节码文件。

1.2. 粗糙的创建对象过程:

假设main方法中有以下代码:

Person p = new Person();

很多时候我们会认为创建对象的过程如下:

在这里插入图片描述
1. 先javac编译源码文件得到.class文件。
2. 再加载.class文件到内存运行。
在这里插入图片描述
1.3. 稍微细致一点的创建对象过程

  • A a = new A();
  • 1.加载类:
    ClassLoader类加载器加载.class文件到呢村
    执行静态代码块和静态初始化语句
  • 2.执行new,申请一片内存空间
  • 3.调用构造器,创建一个空白对象
  • 4.子类调用父类构造器。
  • 5.构造器执行:执行构造代码块和初始化语句

在这里插入图片描述
所以:通过new创建实例和反射创建实例,都绕不开Class对象。

2.类加载器

在上面一节中,了解到.class文件是由类加载器加载的,此处我们先简单的了解一下。

2.1.核心方法loadClass()

  • 核心方法loadClass():告诉它需要加载的类名,它会帮你加载
   protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查是否已经加载该类
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 如果尚未加载,则遵循父优先的等级加载机制(所谓双亲委派机制)
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 模板方法模式:如果还是没有加载成功,调用findClass()
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    // 子类应该重写该方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

2.2.加载.class文件大致可以分为3个步骤:

  • 1.检查是否已经加载,有的话就直接返回,避免重复加载
  • 2.当前缓存中确实没有该类,那么遵循父优先加载机制,加载.class文件
  • 3.如果上面两步都失败了,调用findClass()方法加载。

需要注意的是,ClassLoader类本身是抽象类,而抽象类本身是无法通过new创建对象的。所以它的findClass()方法没有具体的逻辑,留给子类来根据具体情况进行重写。

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

即无法通过父类ClassLoader中的findClass()方法加载.class文件。

正确的做法是,子类重写覆盖findClass(),在里面自定义加载逻辑,比如:

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
	try {
		/*自己另外写一个getClassData()
                  通过IO流从指定位置读取xxx.class文件得到字节数组*/
		byte[] datas = getClassData(name);
		if(datas == null) {
			throw new ClassNotFoundException("类没有找到:" + name);
		}
		//调用类加载器本身的defineClass()方法,由字节码得到Class对象
		return defineClass(name, datas, 0, datas.length);
	} catch (IOException e) {
		e.printStackTrace();
		throw new ClassNotFoundException("类找不到:" + name);
	}
}

defineClass()是ClassLoader中定义的方法,目的是根据.class文件的字节数组byte[] b造出一个对应的Class对象。底层最终会调用一个native方法:

在这里插入图片描述

2.3.反正,目前关于类加载器,只需要大概了如下信息:

  • 我们重写了findClass方法:
    根据文件系统路劲加载class文件,IO读取.class文件,并返回byte数组
    调用ClassLoader提供的方法,将二进制数组转换成Class类的实例对象。
  • 现在我们得到了Class类的实例对象。
    在这里插入图片描述

3.Class类

现在,.class文件已经被加载到内存中了,并且JVM根据其字节数组创建了对应的Class对象。
接下来,我们研究一下Class对象,我们将一步步分析Class类的结构。

  • 3.1.Class类中有一个ReflectionData内部类,用于对反射的支持,里面映射着对应.class字节码文件中地属性字段,方法,构造器等信息。
    在这里插入图片描述
  • 3.2.而且为了更详细的描述这些重要的信息,JDK还写了三个类Field,Method,Constructor。这三个类详细的映射了一个类中的属性,方法,构造器的具体信息。而且Class类中的内部类ReflectionData维系着这三个对象的数组形式的引用。

在这里插入图片描述

  • 3.3.Class类中除了属性,方法,构造器的映射,还有注解,泛型等等这个被映射的类所有的信息。

在这里插入图片描述
在这里插入图片描述

  • 3.4.所以被映射的类A的所有信息,都被“解构”后保存到Class类的实例对象中。其中字段,方法,构造器又用Field,Method,Constructor等类单独映射存储,Class又维系着这三个类的引用属性。

  • 3.5.看看Class类的方法:

  • 1.构造器:private Class(ClassLoader loader) { }

    可以发现,Class类的构造器是私有的,我们无法手动new一个Class对象,只能由JVM创建。
    JVM在构造Class对象时,需要传入一个类加载器,然后就是我们上面分析的一连串加载,创建过程。

  • 2.Class.forName()方法

    静态方法,传入全类名,得到该类的Class类对象。

    底层还是类加载器来做具体实现。
    在这里插入图片描述

  • 3.newInstance()

在这里插入图片描述
也就是说,newInstance()底层就是调用无参构造方法来构造一个实例对象。
所以,本质上Class对象要想创建一个它所映射的类的实例,其实都是通过构造器对象的newInstance来实现的。
在此处,如果没有空参构造器对象,就无法使用clazz.newInstance(),必须要获取其他有参的构造器对象,然后调用构造器对象的newInstance()。

4.反射API

日常开发中使用反射的主要目的有两个:

  • 创建目标对象的实例:
  • 反射调用目标对象中的方法。

创建实例的难点在于,很多人不知道clazz.newInstance()底层还是调用Constructor对象的newInstance()方法,而且这个Constructor对象是一个无参构造器对象。所以要想能够成功创建目标实例,必须保证编写的类有一个无参构造

  • 4.1.看一下反射调用方法的难点

1.先理清楚Class,Field,Method,Constructor四个对象的关系

  • Class类中有一个内部类ReflectionData,这个内部类中维系着Field,Method,Constructor这三个类的对象数组属性。
    在这里插入图片描述

  • Field,Method,Constructor对象内部有对字段,方法,构造器更详细的描述:
    在这里插入图片描述
    2.理清楚关系后,我们再来看看反射调用方法时的两个难点。

  • 1.难点一:根据Class对象获取Method对象时,需要传入方法名+参数的Class类型。
    在这里插入图片描述
    为什么要传入方法名name+参数的Class类型ParameterType呢?

    因为目标.class文件中有多个方法,而且每个方法都对应着一个Method对象。那么怎么唯一标识一个方法呢?------方法签名:方法名称和参数类型

    既然要传参数类型,那么为什么不能直接传入String, int, Integer这些呢?因为这些都是基本类型和引用类型,类型是不能作为参数传递的。我们能传递的要么是值,要么是对象引用。所以要传参数的Class类型对象:String.class, int.class。

    所以必需传入方法名name+参数的Class类型ParameterType。

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

    循环遍历所有Method,根据name和parameterType匹配循环遍历所有Method,根据name和parameterType匹配

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

    上面分析过,.class文件通过IO被加载到内存后,JDK创造了至少四种对象:Class,Field,Method,Constructor。

    对象的本质就是用来存储数据的,是类的具体的、单个的具体形象。而方法是一种行为描述,是所有对象共有的,不属于某个对象独有。既然是共性行为,那么可以抽取出来,放在方法区中共用。

    既然是共用的,那么怎么确定:要执行的方法是来影响哪个对象呢?所以JVM有一个隐性机制,每次调用非静态方法时,都会隐性传递调用调用该方法的当前对象,称为隐性参数。所以方法可以根据这个对象参数知道当前调用本方法的是哪个对象。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值