目录

一、引言:为什么我们需要反射?

二、实验目标与准备

环境准备

三、编写目标类:Student

四、实验核心:三种方式获取 Class 对象

五、运行结果与分析

🔍 结果解读

🧠 原理深挖:为什么只有一个 Class 对象?

六、三种方式的适用场景(个人体悟)

七、扩展思考:Class 对象能做什么?

八、总结与作业反思

本次实验核心收获

踩坑记录

下一步预告

九、参考文献


一、引言:为什么我们需要反射?

在常规 Java 编程中,我们写的代码在编译期就要确定类型:new Student()student.study()。一切都必须是明确已知的。但如果我告诉你:类的名字在编译时根本不存在,而是写在配置文件里,程序运行时才能读到,比如:

payService=com.mybank.WeChatPayService

你该怎么办?用 new 是做不到的,因为你不知道类名。这时候,Java 的反射机制就出场了——它允许程序在运行时动态获取一个类的完整信息(构造器、方法、字段),并动态调用它们。

反射是框架(Spring、MyBatis、Hibernate)的底层基石。理解了反射,你就拿到了看懂框架源码的第一把钥匙。


二、实验目标与准备

本次实验只做一件事:用三种不同的方式拿到一个类的 Class 对象,并验证它们其实是同一个对象

环境准备

  • JDK 17

  • VS Code + Java 扩展包

  • 项目结构:


    java_reflection_blog/
    └── src/
        ├── com/reflection/demo/Student.java
        └── Experiment1.java

    【截图1:VS Code 中的项目包结构】


三、编写目标类:Student

我们先写一个普通的 Java 类,它包含:

  • 私有字段 nameage

  • 无参构造、有参构造

  • 公有方法 study()

  • 私有方法 secret()(为了后面演示暴力反射)


    package com.reflection.demo;
    
    public class Student {
        private String name;
        private int age;
    
        public Student() {
            this.name = "默认学生";
            this.age = 18;
        }
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void study() {
            System.out.println(name + " 正在学习,年龄:" + age);
        }
    
        private void secret() {
            System.out.println("这是一个私有方法,只能被反射调用");
        }
    
        @Override
        public String toString() {
            return "Student{name='" + name + "', age=" + age + "}";
        }
    }

    【截图2:Student.java 完整代码】

这里故意写了一个私有方法 secret(),目的是为后续实验展示 setAccessible(true) 的威力。但目前第一步只关注 Class 对象,所以暂时不用它。


四、实验核心:三种方式获取 Class 对象

新建 Experiment1.java

import com.reflection.demo.Student;

public class Experiment1 {
    public static void main(String[] args) throws ClassNotFoundException {
        // 方式一:Class.forName() —— 最动态,字符串驱动
        Class<?> clazz1 = Class.forName("com.reflection.demo.Student");
        System.out.println("方式1:" + clazz1.getName());

        // 方式二:类名.class —— 编译时已知,最简洁
        Class<Student> clazz2 = Student.class;
        System.out.println("方式2:" + clazz2.getName());

        // 方式三:对象.getClass() —— 运行时获取实际类型
        Student stu = new Student();
        Class<?> clazz3 = stu.getClass();
        System.out.println("方式3:" + clazz3.getName());

        // 关键验证:是否为同一个 Class 对象?
        System.out.println("clazz1 == clazz2 : " + (clazz1 == clazz2));
        System.out.println("clazz2 == clazz3 : " + (clazz2 == clazz3));
    }
}

【截图3:Experiment1.java 代码】


五、运行结果与分析

运行后,终端输出如下:

方式1:com.reflection.demo.Student
方式2:com.reflection.demo.Student
方式3:com.reflection.demo.Student
clazz1 == clazz2 : true
clazz2 == clazz3 : true

【截图4:运行结果终端截图】


🔍 结果解读

  1. 三种方式打印的类名完全相同,都是 com.reflection.demo.Student

  2. 最关键的:clazz1 == clazz2 和 clazz2 == clazz3 都是 true
    在 Java 中,== 比较的是对象的内存地址true 意味着这三个引用指向堆中同一个对象

🧠 原理深挖:为什么只有一个 Class 对象?

JVM 的类加载机制保证了:对于同一个类加载器加载的同一个全限定类名,Class 对象在 JVM 中有且仅有一份

当我们第一次使用 Student 类(无论是 Class.forNameStudent.class 还是 new Student() 触发初始化)时,JVM 会:

  • 查找 .class 文件

  • 读取字节码

  • 在堆内存中创建一个 java.lang.Class 的实例,代表 Student 类的元数据

  • 之后所有对该类的反射或常规操作,都复用这个唯一的 Class 对象。

这就是为什么 clazz1clazz2clazz3 指向同一块内存。这种设计节约内存,也保证了类型信息的全局一致性。


六、三种方式的适用场景(个人体悟)

方式 代码示例 典型场景 优点 缺点
Class.forName() Class.forName("com.demo.Student") 从配置文件、网络、用户输入中获取类名 动态性最强,完全解耦 需处理受检异常,字符串拼写错误运行时才发现
类名.class Student.class 编译时已知类型,如工具类、静态工厂 类型安全,无异常,效率最高 硬编码类名,不灵活
对象.getClass() obj.getClass() 接收一个 Object 参数,需要知道其实际子类型 运行时动态,适合多态场景 需要已有对象实例

我的体会:刚学反射时,总觉得 Student.class 最简单,直接拿来用就好了。但后来写了一个小型 IOC 容器,从 application.properties 读取 DAO 实现类名时,才发现 Class.forName() 才是真正的“银弹”。它让程序在编写时完全不知道类名,运行时才动态加载——这才是反射最迷人的地方。

另外,很多同学误解 getClass() 和 class 的区别:Student.class 是编译时确定的静态类型;stu.getClass() 是运行时获取的动态类型。当 stu 实际指向一个子类 CollegeStudent 时,getClass() 返回的是 CollegeStudent 的 Class 对象,而不是 Student 的。多态在 Class 层面依然生效


七、扩展思考:Class 对象能做什么?

拿到 Class 对象只是反射的第一步。后续你可以:

  • 通过 newInstance() 创建对象(已过时,现用 getDeclaredConstructor().newInstance()

  • 获取 Method 并调用

  • 获取 Field 并修改值(即使是 private)

  • 获取 Annotation 信息

这些会在后续实验中逐一实现。但请记住:没有 Class 对象,就没有后续的一切。它是一切反射操作的入口。


八、总结与作业反思

本次实验核心收获

  1. 三种获取 Class 对象的方式,各自的适用场景。

  2. 验证了同一个类在 JVM 中只有一份 Class 对象,由类加载器保证。

  3. 理解了 Class 对象是反射的“元数据入口”。

踩坑记录

  • 警告 The method secret() is never used locally:这是静态代码检查的误报,因为反射是动态调用,工具无法识别。可以忽略,或加 @SuppressWarnings("unused") 消除。

  • 包结构错误导致 ClassNotFoundException:如果运行 Experiment1 时出现 ClassNotFoundException,检查 Student.java 第一行 package com.reflection.demo; 是否与文件夹路径匹配,以及 Experiment1.java 是否有 import 语句。

下一步预告

第二篇实验将使用反射:

  • 调用私有构造器创建对象

  • 调用私有方法 secret()

  • 修改私有字段 name 的值

  • 对比反射调用与直接调用的性能差异


    九、参考文献

  • 《Java 核心技术 卷Ⅰ》第 5 章:反射

  • Java 官方文档:java.lang.Class

  • 《深入理解 Java 虚拟机》第 7 章:类加载机制


    写这篇博客时,我特意没有一次把反射全部写完,而是拆成多个小实验。因为反射本身就是个“概念密集”的主题,一次消化太多容易消化不良。建议你也跟着敲一遍代码,把每个输出的含义搞懂,比看十篇文章都有用。

更多推荐