Java核心技术梳理-类加载机制与反射

Posted yuanqinnan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java核心技术梳理-类加载机制与反射相关的知识,希望对你有一定的参考价值。

一、引言

反射机制是一个非常好用的机制,C#和Java中都有反射,反射机制简单来说就是在程序运行状态时,对于任意一个类,能够知道这个类的所有属性和方法,对于任意一个对象,能够调用它的任意属性和方法,其实初听就知道反射是一个比较暴力的机制,它可能会破坏封装性。

通过反射的定义我们可以想到反射的好处:可以灵活的编写代码,代码可以在运行时装配,降低代码的耦合度,动态代理的实现也离不开反射。

为了更好的理解反射,我们先了解下JVM中的类加载机制。

二、类加载机制

当程序要使用某个类时,如果这个类还未加载到内存,则需要将其加载到内存中,JVM会通过加载、连接、初始化三个步骤来对该类进行初始化。

2.1 类加载

类的加载由类加载器完成,类加载器通常由JVM提供的,JVM提供的类加载器通常称为系统加载器,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

类加载加载的是一个二进制数据,这些数据的来源有几种:

  • 本地文件系统加载class文件。

  • 从JAR包中记载class文件。

  • 通过网络加载class文件。

  • 把一个java文件动态编译,并执行加载

类加载不一定是要等到首次使用时才加载,虚拟机允许系统预先加载某些类

2.2 类连接

在类被加载后,系统会为之生成一个对应的Class对象,接着进入连接阶段,连接阶段是把类的二进制数据合并到JRE中,类连接分为三个阶段:

  1. 验证:检验被加载的类是否有正确的内部结构,并和其他的类协调一致。

  2. 准备:负责为类变量分配内存,并设置默认初始化值。

  3. 解析:将类的二进制数据中的符号引用替换成直接引用。

2.3 类的初始化

虚拟机负责对类进行初始化,主要是对变量进行初始化,对类变量指定初始值有两种方式:

  • 声明类变量时指定初始值。

  • 使用静态初始化块为类变量指定初始值。

JVM初始化的几个步骤:

  1. 假如该类还没有被加载和连接,则程序先加载或连接该类。

  2. 假如该类的直接父类没有初始化,则先初始化这该类的父类。

  3. 假如类中有初始化语句,则系统依次执行这些初始化语句。

可以看出当程序主动使用某个类时,一定会保证该类及其所有父类都被初始化。那么在什么情况下系统会初始化一个类活着接口呢?

  • 创建类的实例,既包括使用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大致如下:

  1. 检测此Class是否载入过(通过缓存查询),跳至第8步。

  2. 如果父类加载器不存在,则跳至第4步,如果父类加载器存在,则执行第3步。

  3. 请求使用父类加载器去载入目标类,成功则跳到第8步,否则跳到5步。

  4. 请求使用根类加载器来载入目标类,成功则调到第8步,否则跳至7步。

  5. 当前类加载器尝试寻找Class文件,找到执行第6步,找不到则跳入7步。

  6. 从文件中载入Class,成功跳入第8步。

  7. 抛出ClassNotFoundException异常。

  8. 返回对应的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的方式有三种:

  1. 使用Class类中的forName(String name)静态方法,传入的参数是某个类的全限定名。

  2. 调用某个类的class属性来获取该类对应的的Class对象。

  3. 调用某个对象的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());
    

从这里可以看出来,反射破坏了封装性。

以上是关于Java核心技术梳理-类加载机制与反射的主要内容,如果未能解决你的问题,请参考以下文章

类加载机制与反射

Java Review(三十九类加载机制与反射)

Java Review(三十九类加载机制与反射)

Java基础java类加载过程与反射机制

Java注解与反射——获取反射对象 & 类加载机制

类加载机制与反射