Java Class类与反射机制详解

本文详细介绍了Java反射机制,包括Class类的加载、获取Class对象的方式、类初始化、类型转换以及反射与Array类的结合使用。通过反射,开发者可以在运行时动态获取类信息并调用方法和属性,增强了代码的灵活性。文章通过实例展示了Constructor、Field和Method类的使用,以及如何创建和访问数组。
摘要由CSDN通过智能技术生成

概述

反射是 java 编程语言所具备的一种高级特性:对于任何一个类,都能知道这个类的属性和方法;对于任何对象,都能调用它的方法和属性,我们把这种动态获取属性信息以及动态调用对象方法的方式称为 反射机制。由于功能的强大性,反射在很多开源框架或是类库中获得非常广泛的使用,本篇博客我就来简单整理一下 java 反射相关的知识。


一、Java 反射

本篇博客我打算分以下几个模块展开:

  • Java Class 类
  • Java 反射

1、Java Class 类

Java 反射基于 Class 类对象实现,这里我首先整理 Class 类对象相关知识。

Java 是面向对象程序设计语言,在程序运行过程中时时刻刻伴随着对象的工作。对于如此庞大数量的对象,JVM 需要确定每个对象的类型和对应类型所包含的信息。Java 代码就是通过 Class 类对象来保存对应类型信息的,并且每个对象的对象头模块包含指针(引用)指向 Class 类对象确定类型。其中 Class 是一个实实在在的类,它在源码中表示如下:

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
   
                              
    private static final int ANNOTATION= 0x00002000;
    private static final int ENUM      = 0x00004000;
    private static final int SYNTHETIC = 0x00001000;

    private static native void registerNatives();
    static {
   
        registerNatives();
    }

    private Class(ClassLoader loader) {
   
        classLoader = loader;
    }
	
	// 省略部分方法
}

每个类都存在一个 Class 类对象和它对应,每当我们编写并编译一个新类时就会创建一个新的 Class 对象,并且这个对象将会被保存在同名 .class 文件中(编译后的字节码文件实际就是 Class 对象)。需要特别注意的一点是:JVM 中所有同类型对象共享一个 Class 类对象。下面我通过抽象示图描述这三者之间的关系:
对比图
简单来说,每个通过关键字 class 标识的类,都仅存一个 Class 对象记录它的类型信息,无论创建多少个该类的实例对象,所有对象共用这一个 Class 类对象。


1-1、Class 对象的加载

由于 Class 类只包含私有的构造方法,因此对应对象只能由 JVM 来创建以及加载。关于 JVM 何时加载 Class 对象我做了如下总结:

  • 所有类在第一次使用时,JVM 加载对应类对象
  • 当程序调用某个类的静态变量或静态方法时,JVM 会加载对应类对象

由此也就可以看出,JVM 并没有在运行前加载所有类对象,而是按需加载。其中每个类对象只会被加载一次,类加载器在加载前会首先判断当前类对象是否已经被加载。如果还没有被加载,默认会根据类名查找 .class 文件,在正式加载 .class 文件前会校验文件格式是否正常,在确保文件正常不存在安全性隐患后,才会将该类对象加载到内存。

Class 对象被加载到内存后,就可以根据该类型创建对象。下面我通过简单的代码演示类加载的过程:

public class ReflexTest {
   

    static {
   
        System.out.println("执行当前类的静态方法");
    }

    public ReflexTest() {
   
        System.out.println("执行当前类的构造方法");
    }

    public static void main(String[] args) {
   
        new ReflexTest();
        new ReflexTest();
    }   
}

执行结果

执行当前类的静态方法
执行当前类的构造方法
执行当前类的构造方法

从执行结果可以看出,第一次创建对象时,JVM 加载 ReflexTest 类对象,加载过程中执行静态方法,加载完成后执行该类的构造方法。第二次创建对象时,此时 Class 类对象已经被加载,因此直接执行构造方法即可。


###/1-2、如何获取 Class 对象
Class 类对象被加载到内存后,除了在创建对象时提供类型数据外,还可以在代码层使用,这种在代码层调用 Class 类对象的方式就叫 反射。这里我先简单列举常见的获取 Class 对象的几种方式:

  • Class.forName()
  • getClass()
  • Class 字面常量

Class.forName()

该方法会返回某个类的 Class 对象,方法参数为类的路径,具体示例如下:

package com.qhd.worker;

class A {
   
    static {
   
        System.out.println("JVM 加载A类的 Class 对象");
    }
}

@Test
public void test() throws ClassNotFoundException {
   
    Class.forName("com.qhd.worker.A");
}

执行结果

JVM 加载A类的 Class 对象

需要注意的一点是:该方法需要捕获 ClassNotFoundException 异常。因为方法参数是 String 类型的,可能出现类不存在的情况。

该方法最大的好处是没有限制,只要知道对应类的路径就可以获取该类的 Class 对象,缺点是如果类路径发生改变,则必须通过修改代码完成,无法做到一劳永逸。


getClass()

该方法是一个实例对象方法,继承于 Object 类,下面我们具体看示例:

@Test
public void test2() throws ClassNotFoundException {
   
    A a = new A();
    a.getClass();
}

该方法最大的好处就是使用方便,但缺点也非常明显:需要使用实例对象,如果当前方法中没有权限创建对应示例对象的话,就不能通过该方法获取对象信息。


Class 字面常量

Class 字面常量相比上述两种方法更简单以及高效,下面我们具体看示例:

@Test
public void test3() throws ClassNotFoundException {
   
    Class c = A.class;
}

除了可以获取普通类的 Class 对象外,字面常量同样适用于基础类型数据,并且基础类型的包装类存在属性 Type 指向类对象,其等价关系如下:

int.class = Integer.TYPE;
long.class = Long.TYPE;
char.class = Character.TYPE;
boolean.class = Boolean.TYPE;
float.class = Float.TYPE;
void.class = Void.TYPE;
...

需要特别注意的一点是:通过 Class 字面常量获取的 Class 类对象不会初始化该类。下面我们主要来验证一下,在正式开始前,我们先大概了解类从加载到回收的全过程:

类周期

  • 加载:类加载的第一步,查找 .class 文件,并将该文件读取进内存,创建 Class 对象
  • 验证:验证字节码的安全性和完成性
  • 准备:为静态资源分配存储空间
  • 解析:解析这个类中所有对其它类的引用
  • 初始化:类加载的最后一步,如果该类含有父类,首先执行父类的静态代码块以及为静态资源分配空间

也就是说,当我们通过字面常量获取 Class 对象时,只会触发上述过程中的加载阶段,后续步骤并不会触发。下面我通过一个简单的案例加以证明:

class Demo1 {
   
    static final int num = 1;
    static {
   
        System.out.println("Demo1 类被初始化");
    }
}
class Demo2 {
   
    static int num = 2;
    static {
   
        System.out.println("Demo2 类被初始化");
    }
}
public static void main(String[] args) throws ClassNotFoundException {
   
    System.out.println("使用字面常量获取Class类");
    Class c = Demo1.class;
    System.out.println("使用Class.forName()获取Class类");
    Class c2 = Class.forName("Demo2");
}

执行结果

使用字面常量获取Class类
使用Class.forName()获取Class类
Demo2 类被初始化

从输出结果可以看出,使用字面常量获取 Class 对象时,静态方法并没有执行。也就是说没有执行到类加载过程中的初始化那一步。


1-2、Java 类初始化

前面我们验证了通过字面常量获取 Class 类对象时不会初始化类。除此之外,通过类名访问不同类型的静态常量也可能不初始化类,具体我们看示例:

class Demo3 {
   
    static final int num = 2;
    static final int num2 = new Random().nextInt();

    static {
   
        System.out.println("Demo3 类被初始化");
    }
}

public static void main(String[] args) throws ClassNotFoundException {
   
   System.out.println("访问编译期静态常量");
   int temp = Demo3.num;

   System.out.println("访问非编译期静态常量");
   temp = Demo3.num2;
}

执行结果

访问编译期静态常量
访问非编译期静态常量
Demo3 类被初始化

从输出结果可以看出,访问编译期静态常量不会导致类的初始化,只有访问非编译期静态常量时,类加载才会执行到初始化那一步。这是因为 num 属性在编译阶段通过 常量传播优化 的方式保存在了常量池中,后序访问该常量实际上都是在访问常量池对象。而 num2 属性虽然也是常量,但它的值不能在编译期确定,只能等到类初始化完成后才能确定。这也是导致上述执行结果的主要原因。

初始化是类加载的最后一步,完成这个阶段后类对象才会真正加载到内存中,此时才可以通过类对象创建实例对象、引用类对象的静态变量。JVM 规定在以下几种情况一定会执行初始化操作:

  • 使用 new 关键字创建实例对象时,初始化类
  • 调用类的静态变量或是调用类的静态方法时,初始化类
  • 对类进行反射调用时,初始化类
  • 初始化类并且该类的父类还没有初始化时,首先初始化父类
  • 虚拟机启动,执行 main() 方法前必须初始化当前类

1-3、Class 类对象的类型转换

回到 Class 类,通过前面 Java Class 类的源码可以看出,它本身支持泛型。如果我们在创建 Class 对象时声明泛型类型的话,在具体给类对象添加值时会多一步判断操作。具体代码如下:

Class intType = int.class;
Class<Double> doubleType = double.class;
// 代码检测无异常
intType = double.class;
// 编译器报错
doubleType = int.class;

从代码可以看出,没有使用泛型的 Class 对象可以赋任何类对象,如果存在错误只会在运行期检测出来。一旦声明了泛型类型,编译器会在编译器检查类型是否匹配,确保没有出现类型错误。

除了不同类型外,使用泛型时,子类类对象也不能通过父类类对象赋值,具体我们看代码:

class Father {
   }

class Son extends Father {
   }

@Test
public void test4() {
   
	// 编译期报错
	Class
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值