深入理解Java的反射机制

本文详细阐述了Java程序从编译到运行的全过程,包括编译阶段生成字节码和运行阶段的类加载。类加载分为加载、验证、准备、解析和初始化五个步骤,由ClassLoader及其子类负责。重点介绍了类加载的双亲委托模型和四种触发初始化的场景。此外,还探讨了Java反射机制,它是如何在运行时动态获取和操作类信息的。
摘要由CSDN通过智能技术生成

首先我们需要先了解程序的运行过程:从源文件创建到程序运行,Java程序要经过两大步骤:编译,运行;1、源文件由编译器编译成字节码(ByteCode); 2、字节码由java虚拟机解释运行。

第一步(编译): 创建完源文件之后,程序会先被编译为.class文件。Java编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后引用,否则直接引用。如果java编译器在指定目录下找不到该类所其依赖的类的.class文件或者.java源文件的话,编译器话报“cant find symbol”的错误。编译后的字节码文件格式主要分为两部分:常量池和方法字节码。常量池记录的是代码出现过的所有token(类名,成员变量名等等)以及符号引用(方法引用,成员变量引用等等);方法字节码放的是类中各个方法的字节码。下面是MainApp.class通过反汇编的结果,我们可以清楚看到.class文件的结构:
在这里插入图片描述
在这里插入图片描述
第二步(运行):java类运行的过程大概可分为两个过程:1、类的加载 2、类的执行。需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

1 类加载过程

在这里插入图片描述

java程序经过编译后形成(.class)文件。通过类加载器将字节码(.class)加载入JVM的内存中。JVM将类加载过程分成加载,连接,初始化三个阶段,其中连接阶段又可分为验证,准备,解析三个阶段。

JVM 的类加载是通过 ClassLoader 及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
在这里插入图片描述
虚拟机自带的加载器:启动类加载器 (引导类加载器, Bootstrap ClassLoader)使用c/c++实现的,用来加载java的核心库(rt.jar resources.jar)处于安全考虑,Bootstrap启动类加载器只加载包java javax sun开头的类;

扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现

1 加载

加载时类加载过程的第一个阶段:在加载阶段,虚拟机需要完成以下三件事情:

1 通过一个类的全限定名来获取其定义的二进制字节流。
2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到用的时候才把它加载进来,而且只加载一次。

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLoader 逐层检查,只要某个 Classloader 已加载就视为已加载此类,保证此类只所有 ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
在这里插入图片描述

2 验证

验证的目的是为了确保 Class 文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。

3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
这里所设置的初始值通常情况下是数据类型默认的零值(如 00L、null、false 等),而不是被在 Java 代码中被显式地赋予的值。
4 解析

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。

5 初始化

类初始化是类加载过程的最后一个阶段,才真正开始执行类中的 Java 程序代码。虚拟机规范严格规定了有且只有四种情况必须立即对类进行初始化:

1、遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的 Java 代码场景是:使用 new 关键字实例化对象时、读取或设置一个类的静态字段(static)时(被 static 修饰又被 final 修饰的,已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法时。
2、使用 Java.lang.refect 包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类。

虚拟机规定只有这四种情况才会触发类的初始化,称为对一个类进行主动引用,除此之外所有引用类的方式都不会触发其初始化,称为被动引用。

2 静态加载和动态加载

Java初始化一个类的时候可以用new 操作符来初始化;也可通过Class.forName的方式来得到一个Class类型的实例,然后通过这个Class类型的实例的newInstance来初始化。我们把前者叫做JAVA的静态加载,把后者叫做动态加载。

静态加载的时候如果在运行环境中找不到要初始化的类,抛出的是NoClassDefFoundError,它在JAVA的异常体系中是一个Error。
动态态加载的时候如果在运行环境中找不到要初始化的类,抛出的是ClassNotFoundException,它在JAVA的异常体系中是一个受检异常,在写代码的时候就需要catch。

3 引出正题—反射

Java的反射就是利用上面第二步加载到jvm中的.class文件来进行操作的。.class文件中包含java类的所有信息,当你不知道某个类具体信息时,可以使用反射获取class,然后进行各种操作。

在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。

Java有个Object 类,是所有Java 类的继承根源,其内声明了数个应该在所有Java 类中被改写的方法:hashCode()、equals()、clone()、toString()、getClass()等。其中getClass()返回一个Class 对象。Class 类十分特殊。它和一般类一样继承自Object。当一个class被加载,或当加载器(class loader)的defineClass()被JVM调用,JVM 便自动产生一个Class 对象。

Class是Reflection故事起源。针对任何您想探勘的类,唯有先为它产生一个Class 对象,接下来才能经由后者唤起为数十多个的Reflection APIs。Reflection机制允许程序在正在执行的过程中,利用Reflection APIs取得任何已知名称的类的内部信息,包括:package、 type parameters、 superclass、 implemented interfaces、 inner classes、 outer classes、 fields、 constructors、 methods、 modifiers等,并可以在执行的过程中,动态生成instances、变更fields内容或唤起methods。

从Class中获取信息:Class类提供了大量的实例方法来获取该Class对象所对应的详细信息,Class类大致包含如下方法,其中每个方法都包含多个重载版本,因此我们只是做简单的介绍,详细请参考JDK文档

获取类内信息:

获取内容    方法签名
构造器 Constructor<T> getConstructor(Class<?>... parameterTypes)
包含的方法   Method getMethod(String name, Class<?>... parameterTypes)
包含的属性   Field getField(String name)
包含的Annotation   <A extends Annotation> A getAnnotation(Class<A> annotationClass)
内部类 Class<?>[] getDeclaredClasses()
外部类 Class<?> getDeclaringClass()
所实现的接口  Class<?>[] getInterfaces()
修饰符 int getModifiers()
所在包 Package getPackage()
类名  String getName()
简称  String getSimpleName()

一些判断类本身信息的方法:

判断内容    方法签名
注解类型?   boolean isAnnotation()
使用了该Annotation修饰?   boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
匿名类?    boolean isAnonymousClass()
数组? boolean isArray()
枚举? boolean isEnum()
原始类型?   boolean isPrimitive()
接口? boolean isInterface()
obj是否是该Class的实例 boolean isInstance(Object obj)
1 获取class

在这里插入图片描述

1、类型.class  如: String.class使用`类名加“.class”`的方式即会返回与该类对应的Class对象。这个方法可以直接获得与指定类关联的Class对象,而并不需要有该类的对象存在。
2、Class.forName("类名");该方法可以根据字符串参数所指定的类名获取与该类关联的Class对象。如果该类还没有被装入,该方法会将该类装入JVM。forName方法的参数是类的完整限定名(即包含包名)。通常用于在程序运行时根据类名动态的载入该类并获得与之对应的Class对象。
3、obj.getClass();所有Java对象都具备这个方法,该方法用于返回调用该方法的对象的所属类关联的Class对象
        System.out.println(Doctor.class); // class com.zs.entity.mapstruct.Doctor
        
        Class<?> name = Class.forName("com.zs.entity.mapstruct.Doctor");
        System.out.println(name);// class com.zs.entity.mapstruct.Doctor
        
        Doctor doctor = new Doctor();
        System.out.println(doctor.getClass());// class com.zs.entity.mapstruct.Doctor

2 获取构造方法

Class类提供了四个public方法,用于获取某个类的构造方法:

1、Constructor getConstructor(Class[] params)根据构造函数的参数,返回一个具有public属性的构造函数
2、Constructor getConstructors()     返回所有具有public属性的构造函数数组
3、Constructor getDeclaredConstructor(Class[] params)     根据构造函数的参数,返回一个具体的构造函数(不分public和非public属性)
4、Constructor getDeclaredConstructors()    返回该类中所有的构造函数数组(不分public和非public属性)
    @Test
    public void test() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Constructor<Person> constructor = Person.class.getConstructor(Integer.class, String.class);
        System.out.println(constructor); // public com.zs.entity.Person(java.lang.Integer,java.lang.String)

        Constructor<?>[] constructors = Person.class.getConstructors();
        for (Constructor<?> value : constructors) {
            System.out.println(value);
        }
        /**
         * public com.zs.entity.Person(java.lang.Integer,java.lang.String)
         * public com.zs.entity.Person()
         */
    }
3 获取类的成员方法
1、Method getMethod(String name, Class[] params) 根据方法名和参数,返回一个具体的具有public属性的方法
2、Method[] getMethods() 返回所有具有public属性的方法数组
3、Method getDeclaredMethod(String name, Class[] params)  根据方法名和参数,返回一个具体的方法(不分public和非public属性)
4、Method[] getDeclaredMethods() 返回该类中的所有的方法数组(不分public和非public属性)
    @Test
    public void test() {
        Class<Person> personClass = Person.class;
        Method[] methods = personClass.getMethods();
        for (Method method : methods) {
            System.out.println(method);
        }
    }
    @Test
    public void test() throws NoSuchMethodException {
        Class<Person> personClass = Person.class;
        Method drink = Person.class.getDeclaredMethod("drink", String.class);
        System.out.println(drink); // public void com.zs.entity.Person.drink(java.lang.String)
    }
4 获取类的成员变量(成员属性)
1、Field getField(String name)  根据变量名,返回一个具体的具有public属性的成员变量
2、Field[] getFields()  返回具有public属性的成员变量的数组
3、Field getDeclaredField(String name) 根据变量名,返回一个成员变量(不分public和非public属性)
4、Field[] getDelcaredFields() 返回所有成员变量组成的数组(不分public和非public属性)
    @Test
    public void test() throws NoSuchMethodException {
        Class<Person> personClass = Person.class;
        Field[] declaredFields = personClass.getDeclaredFields();
        for (Field value : declaredFields) {
            System.out.println(value);
        }
        /**
         * private java.lang.Integer com.zs.entity.Person.id
         * private java.lang.String com.zs.entity.Person.name
         */
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值