Java 笔记 21:反射

什么是反射?

正射: 编译期就确定要操作的类,代码写死

1
2
User user = new User(); 
user.setName("张三");

特点:编译期类型检查,性能高,灵活性差

反射运行期才动态获取类的信息、操作类的成员

1
Class.forName("com.example.User").getMethod("setName").invoke(obj, "张三");

特点:运行期动态解析,性能略低+灵活性极强

反射的底层原理

Java 类加载机制是反射的基础:

  1. .java 源码编译为 .class 字节码文件
  2. 类加载器将 .class 加载到 JVM 内存,生成唯一的 Class 对象(包含类的所有元信息)
  3. 反射就是操作这个 Class 对象;反向获取和操作类的一切

反射相关的主要类

类名所在包核心作用
Classjava.lang反射的入口,代表一个类的元信息
Constructorjava.lang.reflect代表类的构造器,用于动态创建对象
Fieldjava.lang.reflect代表类的成员变量,用于动态获取 / 修改属性值
Methodjava.lang.reflect代表类的成员方法,用于动态调用方法
AccessibleObjectjava.lang.reflect上述 3 个类的父类,提供关闭访问检查的能力

反射的作用(为什么要用反射?)

反射是 Java 生态的基石,没有反射就没有现代 Java 框架,核心作用有 3 个:

1. 实现框架的动态性和可扩展性

这是反射最核心的应用场景:

  • Spring IOC:读取配置文件 / 注解中的类全限定名,通过反射动态创建 Bean 并管理
  • MyBatis:Mapper 接口的动态代理,通过反射生成实现类,动态调用 SQL
  • JDBC 驱动加载Class.forName("com.mysql.cj.jdbc.Driver") 就是通过反射加载驱动

2.编写通用工具类

避免为每个类写重复逻辑,实现代码复用:

  • 对象属性拷贝:Spring BeanUtils.copyProperties()、Apache BeanUtils,底层都是反射
  • JSON 序列化:Jackson、Gson,通过反射获取对象所有属性;序列化为 JSON

3. 突破封装限制(特殊场景)

可以操作类的 private 成员,处理特殊需求(如单元测试、框架内部逻辑)。

反射的优缺点

优点

  1. 极强的灵活性:编译期不依赖具体类,运行期动态加载,是框架可扩展性的核心
  2. 代码复用性高:可以编写通用工具类,避免重复代码
  3. 突破封装:可以操作私有成员,处理特殊场景

缺点

  1. 性能损耗:反射是运行期动态解析,比直接调用慢 10~100 倍,频繁使用会严重影响性能
  2. 破坏封装性:操作私有成员违反了面向对象的封装原则,可能导致安全问题
  3. 编译期无法检查错误:类、方法、属性是否存在,只有运行期才会抛异常,增加调试难度
  4. 代码可读性差:反射代码比常规代码复杂,不利于后期维护

反射调用优化(3 个核心手段)

1.缓存反射对象(最有效、最常用) 不要每次都重新获取 Class、Constructor、Method、Field 对象,缓存起来复用,性能可以提升几十倍。 代码示例:优化前后对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.lang.reflect.Method;

public class ReflectOptimization {
    // 优化前:每次调用都重新获取Method对象
    public static void noCache() throws Exception {
        for (int i = 0; i < 100000; i++) {
            User user = new User();
            Method method = User.class.getMethod("setName", String.class);
            method.invoke(user, "张三");
        }
    }

    // 优化后:缓存Method对象,复用
    private static final Method SET_NAME_METHOD;
    static {
        try {
            SET_NAME_METHOD = User.class.getMethod("setName", String.class);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    public static void withCache() throws Exception {
        for (int i = 0; i < 100000; i++) {
            User user = new User();
            SET_NAME_METHOD.invoke(user, "张三");
        }
    }

    // 性能测试
    public static void main(String[] args) throws Exception {
        long start1 = System.currentTimeMillis();
        noCache();
        System.out.println("无缓存耗时:" + (System.currentTimeMillis() - start1) + "ms");

        long start2 = System.currentTimeMillis();
        withCache();
        System.out.println("有缓存耗时:" + (System.currentTimeMillis() - start2) + "ms");
    }
}

2.使用 MethodHandle(JDK 7+ 推荐) MethodHandle 是 JDK 7 引入的轻量级反射 API,性能比传统反射高很多,接近直接调用。 代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class MethodHandleDemo {
    public static void main(String[] args) throws Throwable {
        // 1. 获取MethodHandles.Lookup
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        // 2. 定义方法类型:返回值void,参数String
        MethodType methodType = MethodType.methodType(void.class, String.class);
        // 3. 查找方法
        MethodHandle setNameHandle = lookup.findVirtual(User.class, "setName", methodType);

        // 4. 调用方法
        User user = new User();
        setNameHandle.invoke(user, "李四");
        System.out.println(user.getName()); // 输出:李四
    }
}

避免频繁调用 setAccessible(true) setAccessible(true) 本身也有性能损耗,只需要调用一次,后续复用即可(配合缓存使用)。

关闭访问检查:setAccessible(true)

作用: AccessibleObjectConstructor、Field、Method 的父类,提供 setAccessible(boolean flag) 方法:

  • 默认 false:开启访问检查,无法访问 private 成员
  • 设为 true关闭 JVM 的访问权限检查,可以访问 private 成员(俗称「暴力反射」) 代码示例:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.lang.reflect.Field;

public class SetAccessibleDemo {
    public static void main(String[] args) throws Exception {
        User user = new User();
        // 获取private属性name
        Field nameField = User.class.getDeclaredField("name");

        // ❌ 不关闭访问检查,直接操作会抛异常:IllegalAccessException
        // nameField.set(user, "张三");

        // ✅ 关闭访问检查
        nameField.setAccessible(true);
        nameField.set(user, "张三");
        System.out.println(nameField.get(user)); // 输出:张三
    }
}

注意事项

  1. 不是修改访问修饰符setAccessible(true) 只是关闭了 JVM 的检查,private 依然是 private,代码层面的访问修饰符没有改变
  2. 安全管理器限制:如果 JVM 启动了安全管理器(SecurityManager),可能会禁止调用 setAccessible(true)
  3. 只需要调用一次:配合缓存使用,不要每次都调用

Class类

什么是 Class 类?

当 JVM 加载一个 .class 字节码文件时,会在堆内存中自动生成唯一的一个 Class 对象。这个对象就像类的 “身份证”,包含了该类的所有结构信息,反射就是通过操作这个对象,反向获取和操作类的一切。

Class 类的核心特点

  • 没有公共构造方法

    • 不能用 new Class() 创建实例,只能通过 JVM 自动生成或 3 种固定方式获取
    • 构造方法是私有的:private Class(ClassLoader loader)
  • 单例性

    • JVM 中,一个类永远只有一个 Class 实例
    • 无论用哪种方式获取,得到的都是同一个对象
1
2
3
4
Class<?> c1 = Class.forName("com.example.User");
Class<?> c2 = User.class;
Class<?> c3 = new User().getClass();
System.out.println(c1 == c2 && c2 == c3); // true
  • 不可变性

    • Class 对象一旦被 JVM 加载,其内容就不可改变
    • 不能通过 Class 对象修改类的结构(只能读取和调用)
  • 会触发类的加载和初始化

    • Class.forName("全类名") 会触发类的静态代码块执行
    • 类名.class对象.getClass() 不会触发静态初始化

Class 类的常用方法及演示

演示对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example;

public class User {
    private String name;
    private int age;
    public String gender;

    public User() {}
    private User(String name, int age) { this.name = name; this.age = age; }

    public void sayHello() { System.out.println("Hello, " + name); }
    private String getSecret() { return "I love Java"; }

    // getter/setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

获取 Class 对象的方法

这是所有反射操作的第一步,3 种方式各有适用场景: 1.Class.forName(String className) 例:Class.forName("com.example.User")

框架首选,编译期不依赖类,运行期动态加载,会触发静态初始化

2.类名.class 例:User.class

编译期确定,需要导入类包,不会触发静态初始化,多用于参数传递

3.对象.getClass() 例:new User().getClass()

已有对象时使用,能获取对象的实际运行类(适配多态)

创建对象的方法

通过 Class 对象动态创建类的实例: 1.T newInstance():通过无参构造创建对象

JDK9 已废弃,推荐使用其他方法

2.Constructor<T> getConstructor(Class<?>... parameterTypes):获取 public 无参 / 有参构造器

只能获取 public 的

3.Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes):获取任意构造器(包括 private)

配合 setAccessible(true) 使用

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.lang.reflect.Constructor;

public class ClassCreateObjectDemo {
    public static void main(String[] args) throws Exception {
        Class<User> userClass = User.class;

        // 1. 推荐方式:通过public无参构造创建对象
        Constructor<User> noArgConstructor = userClass.getConstructor();
        User user1 = noArgConstructor.newInstance();
        user1.setName("张三");
        System.out.println("无参构造创建:" + user1.getName());

        // 2. 通过private有参构造创建对象(暴力反射)
        Constructor<User> privateConstructor = userClass.getDeclaredConstructor(String.class, int.class);
        privateConstructor.setAccessible(true); // 关闭访问检查
        User user2 = privateConstructor.newInstance("李四", 20);
        System.out.println("私有构造创建:" + user2.getName());
    }
}

获取构造器的方法

获取类的所有构造器信息: 1.Constructor<?>[] getConstructors():获取所有 public 构造器 2.Constructor<?>[] getDeclaredConstructors():获取所有构造器(包括 private、protected、default)

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.lang.reflect.Constructor;

public class ClassGetConstructorsDemo {
    public static void main(String[] args) {
        Class<User> userClass = User.class;

        System.out.println("所有public构造器:");
        Constructor<?>[] publicConstructors = userClass.getConstructors();
        for (Constructor<?> constructor : publicConstructors) {
            System.out.println("- " + constructor);
        }

        System.out.println("\n所有构造器(包括私有):");
        Constructor<?>[] allConstructors = userClass.getDeclaredConstructors();
        for (Constructor<?> constructor : allConstructors) {
            System.out.println("- " + constructor);
        }
    }
}

获取成员变量的方法

获取类的所有属性信息: 1.Field[] getFields():获取所有 public 属性 2.Field[] getDeclaredFields():获取所有属性(包括 private) 3.Field getField(String name):获取指定名称的 public 属性 4.Field getDeclaredField(String name):获取指定名称的任意属性(包括 private)

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java.lang.reflect.Field;

public class ClassGetFieldsDemo {
    public static void main(String[] args) throws Exception {
        Class<User> userClass = User.class;
        User user = userClass.getConstructor().newInstance();

        // 获取并操作private属性
        Field nameField = userClass.getDeclaredField("name");
        nameField.setAccessible(true);
        nameField.set(user, "王五");
        System.out.println("name属性值:" + nameField.get(user));
    }
}

获取成员方法的方法

获取类的所有方法信息: 1.Method[] getMethods():获取所有 public 方法(包括父类继承的) 2.Method[] getDeclaredMethods():获取本类所有方法(包括 private,不包括继承的 3.Method getMethod(String name, Class<?>... parameterTypes):获取指定名称和参数的 public 方法 4.Method getDeclaredMethod(String name, Class<?>... parameterTypes):获取指定名称和参数的任意方法(包括 private)

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.lang.reflect.Method;

public class ClassGetMethodsDemo {
    public static void main(String[] args) throws Exception {
        Class<User> userClass = User.class;
        User user = userClass.getDeclaredConstructor(String.class, int.class).newInstance("赵六", 22);
        user.setAccessible(true);

        // 调用public方法
        Method sayHelloMethod = userClass.getMethod("sayHello");
        sayHelloMethod.invoke(user); // 输出:Hello, 赵六

        // 调用private方法
        Method getSecretMethod = userClass.getDeclaredMethod("getSecret");
        getSecretMethod.setAccessible(true);
        String secret = (String) getSecretMethod.invoke(user);
        System.out.println("私有方法返回值:" + secret);
    }
}

获取类基本信息的方法

获取类的元数据信息: 1.String getName():获取全类名(包名 + 类名) 2.String getSimpleName():获取简单类名 3.Package getPackage():获取包信息 4.Class<? super T> getSuperclass():获取父类 Class 对象 5.Class<?>[] getInterfaces():获取实现的所有接口 6.boolean isInterface():是否是接口 7.boolean isArray():是否是数组 8.boolean isPrimitive():是否是基本数据类型

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class ClassInfoDemo {
    public static void main(String[] args) {
        Class<User> userClass = User.class;

        System.out.println("全类名:" + userClass.getName()); // com.example.User
        System.out.println("简单类名:" + userClass.getSimpleName()); // User
        System.out.println("包名:" + userClass.getPackage().getName()); // com.example
        System.out.println("父类:" + userClass.getSuperclass().getName()); // java.lang.Object
        System.out.println("是否是接口:" + userClass.isInterface()); // false
        System.out.println("是否是数组:" + userClass.isArray()); // false
    }
}

注意事项:

  • Class.forName() 会触发静态初始化

    • 如果类中有静态代码块,Class.forName() 会执行它
    • 类名.class对象.getClass() 不会执行静态代码块
  • 基本数据类型的 Class 对象

    • 基本数据类型也有对应的 Class 对象:int.classboolean.class
    • 它们的包装类有一个 TYPE 常量,指向对应的基本类型 Class 对象:Integer.TYPE == int.class
  • 数组的 Class 对象

    • 数组的 Class 对象是 元素类型[].classString[].classint[].class
    • 所有同类型同维度的数组共享同一个 Class 对象
  • newInstance() 方法已废弃

    • JDK9 之后,Class.newInstance() 被废弃,因为它会传播未检查的异常
    • 推荐使用 Constructor.newInstance() 方法创建对象
1
2
3
4
5
6
7
8
9
import java.lang.reflect.Constructor;

Class<User> userClass = User.class;
// ✅ 调用者是 Class 对象 → 执行 Class.newInstance()
User user1 = userClass.newInstance();

// ✅ 调用者是 Constructor 对象 → 执行 Constructor.newInstance()
Constructor<User> constructor = userClass.getConstructor();
User user2 = constructor.newInstance();

获取 Class 类对象

哪些类型有 Class 对象?

Java 中几乎所有类型都有对应的 Class 对象,JVM 会为每一种类型在内存中生成唯一的 Class 实例。具体包括以下 8 大类:

类型分类示例Class 对象写法
普通类User、String、ArrayListUser.class
接口List、Runnable、SerializableList.class
数组int[]、String[]、User[][]int[].class、String[][].class
基本数据类型int、boolean、char、doubleint.class、boolean.class
枚举enum Color {RED, GREEN}Color.class
注解@Override、@ServiceOverride.class
泛型参数T、E无法直接获取,通过反射间接获取
void无返回值void.class

关键验证:Class 对象的单例性

同一个类型在同一个 JVM 中永远只有一个 Class 实例,无论用哪种方式获取,得到的都是同一个对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ClassSingletonDemo {
    public static void main(String[] args) {
        // 验证普通类
        Class<?> c1 = User.class;
        Class<?> c2 = new User().getClass();
        System.out.println(c1 == c2); // true

        // 验证数组
        Class<?> arr1 = int[].class;
        Class<?> arr2 = new int[10].getClass();
        System.out.println(arr1 == arr2); // true

        // 验证基本数据类型
        System.out.println(int.class == int.class); // true
    }
}

class User {}

获取 Class 对象的 3 种核心方法

方法 1:Class.forName(String 全类名) 最灵活、框架首选的方式,编译期不需要依赖类,运行期动态加载。

1
Class<?> clazz = Class.forName("包名.类名");

核心特点

  1. 会触发类的完整初始化:执行静态代码块、静态变量赋值
  2. 编译期不依赖类:只要运行时类存在即可,是框架实现可扩展性的核心
  3. 抛出检查异常ClassNotFoundException(类不存在时)

适用场景

  • 框架底层动态加载类(Spring IOC、MyBatis、JDBC)
  • 配置文件中指定类名,运行时动态创建对象
  • 插件化开发 代码示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ForNameDemo {
    public static void main(String[] args) {
        try {
            // 1. 加载普通类
            Class<?> userClass = Class.forName("com.example.User");
            System.out.println("加载User类成功:" + userClass.getName());

            // 2. JDBC 加载驱动(经典应用)
            Class.forName("com.mysql.cj.jdbc.Driver");
            System.out.println("MySQL驱动加载成功");

        } catch (ClassNotFoundException e) {
            System.err.println("类不存在:" + e.getMessage());
        }
    }
}

// 验证静态代码块执行
class User {
    static {
        System.out.println("User类的静态代码块执行了");
    }
}

方法 2:类名.class 最安全、性能最高的方式,编译期就确定了 Class 对象。 语法:

1
2
3
Class<User> userClass = User.class;
Class<Integer> intClass = int.class;
Class<List> listClass = List.class;

核心特点

  1. 不会触发类的初始化:不执行静态代码块,性能最好
  2. 编译期类型检查:如果类不存在,编译直接报错,更安全
  3. 支持泛型:可以得到带泛型的 Class 对象,避免类型转换

适用场景

  • 方法参数传递(比如获取方法的参数类型、返回值类型)
  • 已知类名的情况下,优先使用这种方式
  • 基本数据类型只能用这种方式获取 Class 对象 代码示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ClassLiteralDemo {
    public static void main(String[] args) {
        // 1. 普通类
        Class<User> userClass = User.class;
        System.out.println("User类:" + userClass.getSimpleName());

        // 2. 基本数据类型(只能用这种方式)
        Class<Integer> intClass = int.class;
        System.out.println("int类型:" + intClass.getName());

        // 3. 接口
        Class<Runnable> runnableClass = Runnable.class;
        System.out.println("Runnable接口:" + runnableClass.getName());

        // 4. 数组
        Class<String[]> stringArrayClass = String[].class;
        System.out.println("String数组:" + stringArrayClass.getName());

        // 5. void
        Class<Void> voidClass = void.class;
        System.out.println("void类型:" + voidClass.getName());
    }
}

方法 3:对象.getClass() 已有对象时使用,可以获取对象的实际运行时类型(适配多态)。 语法:

1
2
Object obj = new User(); 
Class<?> clazz = obj.getClass();

核心特点

  1. 获取的是对象的实际运行类,不是声明的类型(多态场景下非常重要)
  2. 不会触发类的初始化(因为对象已经创建了)
  3. 只能用于引用类型,基本数据类型不能使用

适用场景

  • 多态场景下,获取对象的真实类型
  • 已有对象实例,需要获取其 Class 对象 代码示例:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class GetClassDemo {
    public static void main(String[] args) {
        // 声明类型是 Animal,实际类型是 Dog
        Animal animal = new Dog();
        
        // getClass() 返回实际运行时的类 Dog.class
        Class<?> clazz = animal.getClass();
        System.out.println("对象的实际类型:" + clazz.getName()); // com.example.Dog

        // 对比:类名.class 返回声明的类型 Animal.class
        System.out.println("声明的类型:" + Animal.class.getName()); 
        // com.example.Animal
    }
}

class Animal {}
class Dog extends Animal {}

3 种方法对比与选型

对比维度Class.forName()类名.class对象.getClass()
触发初始化✅ 是❌ 否❌ 否
编译期检查❌ 否(运行时才检查)✅ 是✅ 是
性能最低最高
灵活性最高最低中等
适用场景框架动态加载、插件化已知类名、参数传递已有对象、多态场景

特殊场景下的 Class 对象获取

1. 基本数据类型 vs 包装类的 Class 对象 基本数据类型和其包装类的 Class 对象是不同的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class PrimitiveVsWrapperDemo {
    public static void main(String[] args) {
        // 基本数据类型
        Class<Integer> intClass = int.class;
        // 包装类
        Class<Integer> integerClass = Integer.class;

        System.out.println(intClass == integerClass); // false

        // 包装类的 TYPE 常量指向对应的基本类型 Class 对象
        System.out.println(intClass == Integer.TYPE); // true
        System.out.println(boolean.class == Boolean.TYPE); // true
    }
}

2. 数组的 Class 对象

  • 所有同类型、同维度的数组共享同一个 Class 对象
  • 不同维度的数组,Class 对象不同
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class ArrayClassDemo {
    public static void main(String[] args) {
        int[] arr1 = new int[10];
        int[] arr2 = new int[100];
        int[][] arr3 = new int[5][5];

        System.out.println(arr1.getClass() == arr2.getClass()); // true(同类型同维度)
        System.out.println(arr1.getClass() == arr3.getClass()); // false(不同维度)
    }
}

3.匿名内部类的 Class 对象 匿名内部类也有对应的 Class 对象,类名格式为 外部类名$数字

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class AnonymousClassDemo {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {}
        };

        System.out.println(runnable.getClass().getName()); 
        // 输出:com.example.AnonymousClassDemo$1
    }
}

常见坑与注意事项

  • Class.forName() 必须写全类名(包名 + 类名),否则会报 ClassNotFoundException
  • 基本数据类型只能用 类名.class 获取 Class 对象,不能用 Class.forName()对象.getClass()
  • getClass() 返回的是实际运行时类型,不是声明的类型,多态场景下要特别注意
  • 数组的 Class 对象与元素类型和维度都有关,不同维度的数组 Class 对象不同
  • Class 对象是单例的,可以用 == 比较两个 Class 对象是否相等

类加载

类加载的基本介绍

什么是类加载? JVM 在第一次使用某个类时,会将该类的.class字节码文件从磁盘(或网络、内存等)读取到内存中,并对其进行验证、准备、解析、初始化,最终生成一个可以被程序使用的Class对象。这个完整的过程,就是类加载。

核心特点

  • 按需加载:JVM 不会一次性加载所有类,只有在第一次使用时才会加载
  • 一次加载,终身复用:一个类在 JVM 的生命周期中只会被加载一次,生成的Class对象会被缓存起来
  • 可扩展:通过自定义类加载器,可以实现从任意来源加载类(如网络、加密文件、动态生成的字节码)

类加载与反射的关系 反射的本质是操作 JVM 加载后生成的Class对象。如果一个类没有被 JVM 加载,就无法获取它的Class对象,也就无法进行任何反射操作。

类加载的时机

JVM 只有在主动引用一个类时,才会触发类的初始化(类加载的最后一个阶段)。如果是被动引用,则不会触发类的初始化。

1. 主动引用(一定会触发类加载)

以下 6 种情况属于主动引用,JVM 会立即加载并初始化该类:

  1. 创建类的实例new User()
  2. 调用类的静态成员:访问或修改静态变量、调用静态方法
  3. 使用反射Class.forName("com.example.User")
  4. 初始化子类:初始化子类时,会先初始化其父类
  5. 启动类:执行main()方法的类
  6. JDK 8+ 接口默认方法:如果一个接口定义了默认方法,那么实现该接口的类初始化时,会先初始化该接口

2. 被动引用(不会触发类加载)

以下 3 种情况属于被动引用,JVM 不会触发类的初始化:

  1. 通过子类引用父类的静态变量:只会初始化父类,不会初始化子类
  2. 定义类的数组User[] users = new User[10]; 只会初始化数组本身,不会初始化User
  3. 引用类的常量:常量在编译阶段会被存入调用类的常量池,本质上没有直接引用定义常量的类

代码示例:主动引用 vs 被动引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class ClassLoadTimingDemo {
    public static void main(String[] args) {
        // 被动引用1:通过子类引用父类的静态变量
        // 只会输出"父类初始化",不会输出"子类初始化"
        System.out.println(Child.parentStaticVar);

        // 被动引用2:定义类的数组
        // 不会输出任何初始化信息
        User[] users = new User[10];

        // 被动引用3:引用类的常量
        // 不会输出"常量类初始化"
        System.out.println(ConstantClass.CONSTANT);

        // 主动引用:创建对象
        // 会输出"User类初始化"
        User user = new User();
    }
}

class Parent {
    public static int parentStaticVar = 100;
    static {
        System.out.println("父类初始化");
    }
}

class Child extends Parent {
    static {
        System.out.println("子类初始化");
    }
}

class User {
    static {
        System.out.println("User类初始化");
    }
}

class ConstantClass {
    public static final String CONSTANT = "hello";
    static {
        System.out.println("常量类初始化");
    }
}

类加载的完整过程

类加载分为5 个连续的阶段,其中前 4 个阶段(加载、验证、准备、解析)称为连接阶段,最后一个阶段是初始化阶段

1
2
3
类加载流程:
加载 → 验证 → 准备 → 解析 → 初始化
└────────── 连接阶段 ─┘

阶段 1:加载(Loading)

核心任务:找到字节码文件,读取到内存,生成Class对象。

具体完成 3 件事:

  1. 获取字节码:通过类的全限定名(包名 + 类名),找到对应的.class字节码文件
    • 字节码来源:本地磁盘、网络(如 Tomcat 加载 war 包)、动态生成(如 ASM、CGLIB)、加密文件
  2. 转化数据结构:将字节码的静态二进制结构,转化为方法区的运行时数据结构
  3. 生成 Class 对象:在堆内存中生成一个对应的java.lang.Class对象,作为方法区中类数据的访问入口

关键说明:

  • 加载阶段由 类加载器(ClassLoader) 完成
  • 加载阶段和连接阶段是交叉进行的,加载阶段还没结束,连接阶段可能已经开始
  • 数组类的加载比较特殊:数组类本身由 JVM 直接创建,其元素类型由类加载器加载

阶段 2:验证(Verification)

核心任务:确保字节码文件的正确性,防止恶意代码攻击 JVM。 这是 JVM 的安全屏障,确保加载的字节码符合 JVM 规范,不会导致 JVM 崩溃。

验证分为 4 个步骤:

  1. 文件格式验证:验证字节码文件的格式是否符合规范

    • 检查魔数(0xCAFEBABE)、版本号、常量池格式等
  2. 元数据验证:验证类的元数据信息是否符合 Java 语法规范

    • 检查类是否有父类、是否继承了不允许继承的类(如final类)、方法和字段是否符合语法
  3. 字节码验证:最复杂的一步,验证方法的字节码逻辑是否合法

    • 检查操作数栈的类型是否正确、跳转指令是否指向正确的位置、类型转换是否合法
  4. 符号引用验证:验证常量池中的符号引用是否存在且合法

    • 检查符号引用指向的类、方法、字段是否存在,访问权限是否合法

阶段 3:准备(Preparation)

核心任务:为类的静态变量分配内存,并设置默认初始值

关键细节:

  1. 只处理静态变量:实例变量的内存分配是在对象创建时(堆中)进行的
  2. 设置默认初始值:不是代码中写的初始值,而是 JVM 规定的默认值
  3. final修饰的静态常量:在准备阶段就会被赋值为代码中写的初始值(因为常量在编译期就确定了)
类型默认初始值
int0
long0L
float0.0f
double0.0d
booleanfalse
char‘\u0000’
引用类型null
代码示例:准备阶段 vs 初始化阶段的赋值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class PreparePhaseDemo {
    // 准备阶段:分配内存,设置默认值0
    // 初始化阶段:赋值为100
    public static int a = 100;

    // 准备阶段:直接赋值为200(final修饰的静态常量)
    public static final int b = 200;

    public static void main(String[] args) {
        System.out.println(a); // 100
        System.out.println(b); // 200
    }
}

阶段 4:解析(Resolution)

核心任务:将常量池中的符号引用,转化为直接引用

核心概念:

  • 符号引用:用一组字符串来描述所引用的目标,比如com.example.User#sayHello()
    • 编译期无法确定目标的内存地址,只能用符号引用表示
  • 直接引用:指向目标内存地址的指针、偏移量或句柄
    • 解析后,JVM 可以直接通过直接引用找到目标

解析的主要内容:

  1. 类或接口的解析
  2. 字段的解析
  3. 类方法的解析
  4. 接口方法的解析

关键说明:

  • 解析阶段可以在初始化阶段之后再执行(称为动态解析),这是 Java 实现动态绑定(多态)的基础
  • 解析阶段只会执行一次,解析后的直接引用会被缓存起来

阶段 5:初始化(Initialization)

核心任务:执行类的静态变量赋值语句静态代码块,完成类的初始化。 这是类加载的最后一个阶段,也是我们最熟悉的阶段。

核心执行逻辑:

JVM 会自动生成一个类构造器方法<clinit>(),初始化阶段就是执行这个方法的过程。 <clinit>()方法由编译器自动生成,包含:

  1. 所有静态变量的赋值语句
  2. 所有静态代码块(static{})中的代码

初始化的执行顺序:

  1. 父类先初始化,子类后初始化:执行子类的<clinit>()方法前,会先执行父类的<clinit>()方法
  2. 静态变量和静态代码块按顺序执行:在类中定义的顺序,就是执行的顺序
  3. <clinit>()方法只会执行一次:JVM 会保证<clinit>()方法在多线程环境下被正确加锁和同步,确保一个类只会被初始化一次 代码示例:初始化顺序
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class InitializationOrderDemo {
    public static void main(String[] args) {
        new Son();
    }
}

class Father {
    static {
        System.out.println("父类静态代码块");
    }

    public Father() {
        System.out.println("父类构造方法");
    }
}

class Son extends Father {
    static {
        System.out.println("子类静态代码块");
    }

    public Son() {
        System.out.println("子类构造方法");
    }
}

类加载各阶段任务总结

阶段核心任务关键细节
加载读取字节码,生成 Class 对象由类加载器完成,字节码来源多样
验证确保字节码的正确性和安全性4 个验证步骤,JVM 的安全屏障
准备为静态变量分配内存,设置默认初始值final 静态常量直接赋值,实例变量不处理
解析符号引用转化为直接引用支持动态解析,是多态的基础
初始化执行静态变量赋值和静态代码块执行<clinit>()方法,父类先初始化

常见问题与注意事项

  • 类加载的顺序:静态代码块 → 构造代码块 → 构造方法
  • 静态变量的赋值:准备阶段赋默认值,初始化阶段赋代码中的值
  • 被动引用不会触发初始化:这是很多面试题的考点
  • <clinit>() vs <init>()
    • <clinit>():类构造器,初始化类的静态成员,只执行一次
    • <init>():对象构造器,初始化对象的实例成员,每次创建对象都会执行
  • 类加载器的双亲委派模型:JVM 默认采用双亲委派模型加载类,确保核心类的安全性

通过反射获取类的结构信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 父类
class Base {
    public String basePublicField;
    private int basePrivateField;

    public void basePublicMethod() {}
    private void basePrivateMethod() {}
}

// 子类(演示用)
@Deprecated
public class Student extends Base {
    // 不同修饰符的属性
    public String name;
    protected int age;
    private String address;
    public static final String SCHOOL = "清华大学";

    // 不同修饰符的构造器
    public Student() {}
    public Student(String name) { this.name = name; }
    private Student(String name, int age) { this.name = name; this.age = age; }

    // 不同修饰符的方法
    public void study() {}
    protected void eat() {}
    private void sleep() {}
    public static void run() {}
}

java.lang.Class 类

![[Pasted image 20260407211101.png]] 所有获取类结构信息的操作,都从 Class 对象开始。 代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionUtilsDemo {
    public static void main(String[] args) throws Exception {
        Class<Student> studentClass = Student.class;

        // 1. getName: 获取全类名(包名+类名)
        System.out.println("1. 全类名:" + studentClass.getName());
        // 输出:com.hspedu.reflection.Student

        // 2. getSimpleName: 获取简单类名
        System.out.println("\n2. 简单类名:" + studentClass.getSimpleName());
        // 输出:Student

        // 3. getFields: 获取所有public修饰的属性,包含本类以及父类的
        // 不能访问私有成员
        System.out.println("\n3. 所有public属性(含父类):");
        Field[] publicFields = studentClass.getFields();
        for (Field field : publicFields) {
            System.out.println("- " + field.getName());
        }
        // 输出:name, SCHOOL, basePublicField

        // 4. getDeclaredFields: 获取本类中所有属性(不包含父类)
        // 配合 setAccessible(true)能访问私有成员
        System.out.println("\n4. 本类所有属性(不含父类):");
        Field[] allFields = studentClass.getDeclaredFields();
        for (Field field : allFields) {
            System.out.println("- " + field.getName());
        }
        // 输出:name, age, address, SCHOOL

        // 5. getMethods: 获取所有public修饰的方法,包含本类以及父类的
        System.out.println("\n5. 所有public方法(含父类):");
        Method[] publicMethods = studentClass.getMethods();
        for (Method method : publicMethods) {
            System.out.println("- " + method.getName());
        }
        // 输出:study, run, basePublicMethod, wait, equals, toString, hashCode...(Object类的方法)

        // 6. getDeclaredMethods: 获取本类中所有方法(不包含父类)
        System.out.println("\n6. 本类所有方法(不含父类):");
        Method[] allMethods = studentClass.getDeclaredMethods();
        for (Method method : allMethods) {
            System.out.println("- " + method.getName());
        }
        // 输出:study, eat, sleep, run

        // 7. getConstructors: 获取本类所有public修饰的构造器
        System.out.println("\n7. 所有public构造器:");
        Constructor<?>[] publicConstructors = studentClass.getConstructors();
        for (Constructor<?> constructor : publicConstructors) {
            System.out.println("- " + constructor.getName());
        }
        // 输出:com.hspedu.reflection.Student(无参)、com.hspedu.reflection.Student(一个参数)

        // 8. getDeclaredConstructors: 获取本类中所有构造器
        System.out.println("\n8. 本类所有构造器:");
        Constructor<?>[] allConstructors = studentClass.getDeclaredConstructors();
        for (Constructor<?> constructor : allConstructors) {
            System.out.println("- " + constructor.getName());
        }
        // 输出:无参、一个参数、两个参数(私有)

        // 9. getPackage: 以Package形式返回包信息
        System.out.println("\n9. 包信息:" + studentClass.getPackage().getName());
        // 输出:com.hspedu.reflection

        // 10. getSuperClass: 以Class形式返回父类信息
        System.out.println("\n10. 父类信息:" + studentClass.getSuperclass().getName());
        // 输出:com.hspedu.reflection.Base

        // 11. getInterfaces: 以Class[]形式返回接口信息
        System.out.println("\n11. 实现的接口:");
        Class<?>[] interfaces = studentClass.getInterfaces();
        for (Class<?> anInterface : interfaces) {
            System.out.println("- " + anInterface.getName());
        }
        // 输出:无(Student类没有实现接口)

        // 12. getAnnotations: 以Annotation[]形式返回注解信息
        System.out.println("\n12. 类上的注解:");
        Annotation[] annotations = studentClass.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println("- " + annotation.annotationType().getName());
        }
        // 输出:java.lang.Deprecated
    }
}

java.lang.reflect.Field 类

用于获取和操作类的属性信息。 ![[Pasted image 20260407211739.png]] 代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class FieldDemo {
    public static void main(String[] args) {
        Class<Student> studentClass = Student.class;
        Field[] allFields = studentClass.getDeclaredFields();

        for (Field field : allFields) {
            System.out.println("属性名:" + field.getName());
            
            // 1. getModifiers: 以int形式返回修饰符
            int modifiers = field.getModifiers();
            // 将int值转换为对应的修饰符字符串
            String modifierStr = Modifier.toString(modifiers);
            System.out.println("修饰符:" + modifierStr + "(int值:" + modifiers + ")");
            
            // 2. getType: 以Class形式返回属性类型
            Class<?> type = field.getType();
            System.out.println("属性类型:" + type.getName());
            System.out.println("------------------------");
        }
    }
}

多个修饰符是相加的关系:public(1) + static(8) + final(16) = 25

修饰符int 值
默认(包访问权限)0
public1
private2
protected4
static8
final16

java.lang.reflect.Method 类

用于获取和调用类的方法信息。 ![[Pasted image 20260407211751.png]] 代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class MethodDemo {
    public static void main(String[] args) {
        Class<Student> studentClass = Student.class;
        Method[] allMethods = studentClass.getDeclaredMethods();

        for (Method method : allMethods) {
            System.out.println("方法名:" + method.getName());
            
            // 1. getModifiers: 以int形式返回修饰符
            int modifiers = method.getModifiers();
            String modifierStr = Modifier.toString(modifiers);
            System.out.println("修饰符:" + modifierStr);
            
            // 2. getReturnType: 以Class形式获取返回类型
            Class<?> returnType = method.getReturnType();
            System.out.println("返回类型:" + returnType.getName());
            
            // 3. getParameterTypes: 以Class[]返回参数类型数组
            Class<?>[] parameterTypes = method.getParameterTypes();
            System.out.print("参数类型:");
            for (Class<?> parameterType : parameterTypes) {
                System.out.print(parameterType.getName() + " ");
            }
            System.out.println("\n------------------------");
        }
    }
}

java.lang.reflect.Constructor 类

用于获取和调用类的构造器信息。 ![[Pasted image 20260407211802.png]] 代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

public class ConstructorDemo {
    public static void main(String[] args) {
        Class<Student> studentClass = Student.class;
        Constructor<?>[] allConstructors = studentClass.getDeclaredConstructors();

        for (Constructor<?> constructor : allConstructors) {
            System.out.println("构造器名:" + constructor.getName());
            
            // 1. getModifiers: 以int形式返回修饰符
            int modifiers = constructor.getModifiers();
            String modifierStr = Modifier.toString(modifiers);
            System.out.println("修饰符:" + modifierStr);
            
            // 2. getParameterTypes: 以Class[]返回参数类型数组
            Class<?>[] parameterTypes = constructor.getParameterTypes();
            System.out.print("参数类型:");
            for (Class<?> parameterType : parameterTypes) {
                System.out.print(parameterType.getName() + " ");
            }
            System.out.println("\n------------------------");
        }
    }
}