反射概述与 Class 对象 | JavaSE
反射概述与 Class 对象
一、学习目标
学完本章,你应该能够:
- 准确解释反射(Reflection)是什么,以及它与普通 Java 编程方式的区别。
- 理解
Class对象代表什么,以及为什么反射的第一步是获取Class对象。 - 掌握获取
Class对象的三种常见方式:类名.class、Class.forName()、对象.getClass()。 - 使用
Class<?>获取类名、包名、父类、接口等基础类型信息。 - 理解“类 → Class → Constructor / Field / Method”的反射知识模型。
- 初步解释反射为什么能够成为 Java 框架实现通用功能的重要基础。
二、核心知识
2.1 什么是反射
先看正常 Java 编程。
假设有一个类:
public class Student {
private String name;
public void study() {
System.out.println("学生正在学习");
}
}
正常情况下,我们提前知道:
类叫 Student
有一个 study() 方法
可以创建 Student 对象
所以可以直接写:
Student student = new Student();
student.study();
整个过程是:
程序员提前知道类
↓
直接编写类型
↓
创建对象
↓
调用方法
但是现在提出一个完全不同的问题:
如果程序运行之前,我根本不知道以后要处理的是
Student、Teacher、User还是其他类,该怎么办?
例如一个框架收到:
com.example.Student
这样的字符串。
框架希望自己:
- 找到这个类。
- 知道它有哪些构造器。
- 知道它有哪些字段。
- 知道它有哪些方法。
- 创建对象。
- 调用方法。
- 读取或修改字段。
这时候就不能简单写:
new Student();
因为框架开发者可能根本不知道未来业务系统中会出现什么类。
Java 为解决这种运行时动态分析问题提供了:
反射(Reflection)机制。
可以把反射理解为:
Java 程序在运行期间获取类的结构信息,并在允许的访问规则下操作构造器、字段和方法的能力。
原课程中将其形象地描述为:
加载类,并以编程方式“解剖”类中的各种成分。
2.2 反射到底学习什么
整个 Java 反射主线其实非常清晰:
Class
│
┌───────────┼───────────┐
↓ ↓ ↓
Constructor Field Method
构造器 字段 方法
│ │ │
↓ ↓ ↓
创建对象 读写数据 调用方法
因此反射主要学习四类对象:
| 反射对象 | 表示什么 | 后续主要作用 |
| ------------- | ------------------------ | -------------------------- |
| Class | 类或接口本身的运行时表示 | 获取类的信息和其他反射对象 |
| Constructor | 构造器 | 创建对象 |
| Field | 字段 | 获取或修改字段值 |
| Method | 方法 | 调用方法 |
所以反射学习顺序是:
第一步:Class
第二步:Constructor
第三步:Field
第四步:Method
第五步:综合应用与框架思想
这一章只解决第一步:
如何在运行时“拿到一个类本身”。
2.3 什么是 Class 对象
这里特别容易混淆两个概念:
Student 类
和:
Class<Student> 对象
它们不是同一个东西。
假设源代码:
public class Student {
}
经过编译:
Student.java
↓ javac
Student.class
JVM 加载这个类以后,会在运行时维护描述这个类型的信息。
Java 使用:
java.lang.Class
来表示这种运行时类型信息。
例如:
Class<Student> clazz = Student.class;
此时:
Student
是我们定义的类。
而:
clazz
是描述 Student 这个类型的 Class 对象。
可以这样建立模型:
Student.java
↓
Student.class 字节码
↓
类加载
↓
JVM 中的 Student 类型
↓
Class<Student> 对象
↓
程序可以通过它查询 Student 的结构
2.4 Class 不只表示普通类
Class 的名字容易让人误以为:
它只能表示 class。
实际上它还能表示:
- 普通类
- 接口
- 枚举
- record
- 注解接口
- 数组类型
- 基本数据类型
void
例如:
Class<String> c1 = String.class;
Class<Runnable> c2 = Runnable.class;
Class<int[]> c3 = int[].class;
Class<Integer> c4 = int.class;
Class<Void> c5 = void.class;
因此更准确的理解是:
Class是 Java 运行时类型的核心表示对象之一。
2.5 Class 对象有什么用
拿到:
Class<?> clazz
以后,我们就获得了反射操作的入口。
后续可以继续:
Class
│
├── 获取类名
├── 获取包信息
├── 获取父类
├── 获取接口
│
├── 获取 Constructor
├── 获取 Field
├── 获取 Method
│
├── 获取注解
└── 判断各种类型特征
所以:
获取 Class 对象是反射的第一步。
没有 Class,后面的构造器、字段、方法反射基本无从谈起。
2.6 为什么 Class 对象这么重要
普通编程通常是:
Student student = new Student();
student.study();
类型名字已经直接写进源代码。
而反射可以把:
类型
本身变成程序能够处理的数据。
例如:
Class<?> clazz = ...;
然后程序可以问:
你叫什么?
你有什么构造器?
你有哪些字段?
你有哪些方法?
你实现了哪些接口?
你继承了谁?
这是反射最关键的思维变化:
以前“类”只是代码里的类型;通过反射,“类的信息”本身也能够成为程序处理的对象。
三、使用方法
3.1 准备 Student 类
package com.example.reflect;
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void study() {
System.out.println(name + "正在学习");
}
}
接下来尝试获得:
Student 的 Class 对象
3.2 方式一:类名.class
最直接的方式:
Class<Student> clazz = Student.class;
System.out.println(clazz);
基本格式:
Class<类型> clazz = 类型.class;
例如:
Class<String> c1 = String.class;
Class<Integer> c2 = Integer.class;
Class<Student> c3 = Student.class;
这种方式的特点是:
编译阶段已经明确知道目标类型。
因此当你已经知道类是谁时,这是非常直接的方式。
3.3 方式二:Class.forName()
第二种:
Class<?> clazz =
Class.forName("com.example.reflect.Student");
注意参数不是:
Student
而是类的:
全限定类名(Fully Qualified Class Name)
通常可以理解成:
包名 + 类名
即:
com.example.reflect.Student
如果写成:
Class.forName("Student");
在这个例子中就无法正确找到目标类。
3.4 Class.forName() 为什么特别重要
假设配置文件:
className=com.example.reflect.Student
程序:
String className = readFromConfig();
Class<?> clazz = Class.forName(className);
现在代码里面根本没有写:
Student.class
目标类型来自:
配置文件
以后配置改成:
className=com.example.reflect.Teacher
Java 代码本身甚至可能完全不用修改。
这就是反射非常关键的一项能力:
将“到底使用哪个类”从编译期固定代码,延迟到运行期决定。
这也是框架思想的重要基础。
3.5 Class.forName() 的异常
如果:
Class.forName("com.example.NotExist");
JVM 找不到这个类,就可能抛出:
ClassNotFoundException
因此示例通常写:
public static void main(String[] args)
throws ClassNotFoundException {
Class<?> clazz =
Class.forName("com.example.reflect.Student");
System.out.println(clazz);
}
3.6 方式三:对象.getClass()
如果已经有一个对象:
Student student = new Student();
可以:
Class<?> clazz = student.getClass();
例如:
Student student = new Student();
Class<?> clazz = student.getClass();
System.out.println(clazz);
getClass() 来自:
Object
因此普通对象都可以调用:
object.getClass();
3.7 三种方式对比
Class<Student> c1 = Student.class;
Class<?> c2 =
Class.forName("com.example.reflect.Student");
Student student = new Student();
Class<?> c3 = student.getClass();
在同一个类加载上下文中,它们描述的是同一个运行时类型:
System.out.println(c1 == c2);
System.out.println(c2 == c3);
通常输出:
true
true
三种方式可以这样理解:
| 获取方式 | 使用场景 |
| ---------------------- | -------------------------------------------- |
| Student.class | 编译时已经知道具体类型 |
| Class.forName("...") | 类型名称来自配置、字符串等运行时数据 |
| object.getClass() | 已经拥有一个对象,希望获取它真正的运行时类型 |
3.8 Class<?> 是什么意思
你会经常看到:
Class<?> clazz
其中:
?
是泛型通配符。
表示:
我知道这是一个
Class对象,但暂时不知道或不关心它具体表示哪一种类型。
例如框架方法:
public static void printClassInfo(Class<?> clazz) {
System.out.println(clazz.getName());
}
它可以接收:
printClassInfo(Student.class);
printClassInfo(String.class);
printClassInfo(Integer.class);
相比直接写原始类型:
Class clazz
通常推荐:
Class<?> clazz
这样泛型语义更加明确。
3.9 获取类的完整名称
Class<?> clazz = Student.class;
System.out.println(clazz.getName());
假设类:
com.example.reflect.Student
则:
clazz.getName()
返回类似:
com.example.reflect.Student
即完整名称。
3.10 获取简单类名
System.out.println(clazz.getSimpleName());
结果:
Student
对比:
getName()
→ com.example.reflect.Student
getSimpleName()
→ Student
3.11 获取包名
System.out.println(clazz.getPackageName());
可能得到:
com.example.reflect
3.12 获取父类
例如:
public class Student extends Person {
}
可以:
Class<?> parent = Student.class.getSuperclass();
System.out.println(parent.getName());
得到:
com.example.reflect.Person
3.13 获取直接实现的接口
例如:
public class Student
implements Comparable<Student>, Serializable {
}
可以:
Class<?>[] interfaces =
Student.class.getInterfaces();
for (Class<?> anInterface : interfaces) {
System.out.println(anInterface.getName());
}
3.14 判断 Class 表示什么类型
常见判断:
clazz.isInterface();
clazz.isEnum();
clazz.isArray();
clazz.isPrimitive();
clazz.isAnnotation();
clazz.isRecord();
例如:
System.out.println(Runnable.class.isInterface());
System.out.println(int.class.isPrimitive());
System.out.println(String[].class.isArray());
这样框架就可以:
根据不同类型特征执行不同逻辑。
四、原理与进阶
4.1 一个类到底有几个 Class 对象
初学阶段经常说:
一个类只有一个 Class 对象。
这个说法适合帮助建立基本概念,但严格来说需要加一个非常重要的运行时前提。
Java 中类的运行时身份不仅取决于:
类的全限定名称
还与:
类加载器
有关。
可以粗略理解:
运行时类型身份
≈
全限定类名 + 类加载器
因此:
对于 JVM 中同一个已定义的运行时类型,对应唯一的 Class 对象。
但是如果不同类加载器分别定义了同名类:
com.example.Student
它们可能实际上是:
两个不同的运行时类型
也就会拥有不同的 Class 对象。
初学阶段记住:
正常同一类加载上下文
Student.class
Class.forName(...)
student.getClass()
↓
得到同一个 Class 对象
即可。
4.2 Class 对象是谁创建的
我们不能这样:
new Class();
因为 Class 没有供普通程序员直接调用的公共构造器。
Class 对象主要由:
JVM + 类加载机制
在类型定义过程中建立。
所以:
Student.class
并不是:
你自己 new 出一个描述 Student 的对象
而是在访问 JVM 对该类型提供的运行时表示。
4.3 .class 文件和 Class 对象不是同一个东西
这是非常重要的区别。
.class 文件
例如:
Student.class
是:
磁盘上的字节码文件
Class<Student> 对象
是:
Java 程序运行期间用于描述 Student 类型的对象
所以:
.class 文件
≠
Class 对象
更合理的过程是:
Student.java
↓
javac
↓
Student.class
↓
ClassLoader / JVM
↓
运行时类型信息
↓
Class<Student>
4.4 获取 Class 不等于创建 Student 对象
例如:
Class<Student> clazz = Student.class;
这里获得的是:
Student 类型的信息
而不是:
Student 实例
真正对象仍然是:
Student student = new Student();
所以要区分:
Student.class
→ 类型对象
new Student()
→ Student 实例对象
4.5 为什么 Class 是后续所有反射操作的入口
因为后续 API 都从 Class 出发:
clazz.getDeclaredConstructors();
clazz.getDeclaredFields();
clazz.getDeclaredMethods();
形成:
Class
│
├─ Constructor
├─ Field
└─ Method
而对应普通 Java:
类
│
├─ 构造器
├─ 字段
└─ 方法
所以反射本质上是在运行时建立了一套:
可以用对象来描述 Java 程序结构的模型。
五、实践应用
5.1 配置驱动
假设配置:
service=com.example.UserService
框架读取:
String className = configuration.get("service");
Class<?> clazz = Class.forName(className);
以后更换实现:
service=com.example.MockUserService
框架主体逻辑仍然可以保持不变。
这体现:
硬编码
↓
配置驱动
5.2 框架为什么需要反射
假设你开发一个通用框架。
你根本不知道其他程序员以后会定义:
User
Student
Order
Product
Article
Teacher
如果你的框架写成:
if (obj instanceof User) {
}
if (obj instanceof Student) {
}
if (obj instanceof Product) {
}
那么每增加一个业务类:
框架代码都要修改
这根本无法成为真正的通用框架。
反射可以让框架处理:
Object object
然后:
Class<?> clazz = object.getClass();
接下来动态分析:
这个对象是什么类?
有哪些字段?
有哪些方法?
有哪些注解?
所以:
反射让程序能够把“未知类型”作为运行时数据处理。
5.3 后续技术中的反射
以后学习 Java Web 与框架技术时,你会频繁遇到:
Spring
MyBatis
Jackson
JUnit
依赖注入
对象映射
注解解析
动态代理
ORM
序列化
其中很多机制都与:
运行时类型信息
有关。
因此反射并不是为了让你以后天天手写:
getDeclaredField(...)
而是为了让你理解:
Java 框架为什么能“自动知道你的类长什么样”。
六、常见问题
6.1 反射就是 Class 吗?
不是。
Class 是反射的核心入口之一。
完整反射体系还包括:
Class
Constructor
Field
Method
Annotation
...
6.2 反射第一步是什么?
通常是:
获取目标类型的
Class对象。
6.3 Class.forName() 里面写什么?
通常写目标类型的:
全限定类名
例如:
Class.forName(
"com.example.reflect.Student"
);
6.4 Student.class 会创建 Student 对象吗?
不会。
Student.class
获取的是:
Student 的 Class 对象
而:
new Student()
才是创建:
Student 实例
6.5 object.getClass() 获得声明类型还是运行时类型?
看:
Animal animal = new Dog();
执行:
Class<?> clazz = animal.getClass();
得到的是:
Dog
因为 getClass() 描述的是对象实际的:
运行时类型。
这与多态知识可以直接连接起来。
6.6 Class.forName() 找不到类怎么办?
可能抛出:
ClassNotFoundException
重点检查:
- 全限定类名是否正确。
- 包名是否正确。
- 类是否确实存在。
- 类是否位于当前运行时 classpath / module path 可见范围中。
6.7 反射是不是可以“无视 Java 所有规则”?
不是。
反射仍然受:
- Java 访问控制
- 模块系统
- 类型规则
- JVM 约束
等机制限制。
后面学习私有构造器、私有字段和私有方法时尤其要注意这一点。
七、练习与验收
7.1 知识问答
- 什么是反射?
- 为什么说反射可以在运行时“解剖类”?
Class对象是什么?- 反射为什么通常先获取
Class? - 获取
Class对象的三种常见方式是什么? .class字节码文件和Class对象有什么区别?类名.class和对象.getClass()有什么区别?Class.forName()为什么特别适合配置驱动场景?- 为什么框架不应该为每一种业务类写大量
instanceof? Class<?>中的?表示什么?
7.2 代码阅读
阅读:
Animal animal = new Dog();
Class<?> c1 = Animal.class;
Class<?> c2 = animal.getClass();
Class<?> c3 = Dog.class;
回答:
c1表示哪个类型?c2表示哪个类型?c3表示哪个类型?c1 == c2是否成立?c2 == c3是否成立?- 这个案例和多态有什么关系?
7.3 手写代码
定义:
public class Book {
private String name;
private double price;
}
要求不查看资料完成:
- 使用
Book.class获取Class。 - 使用
Class.forName()获取Class。 - 使用
book.getClass()获取Class。 - 判断三者是否为同一个
Class对象。 - 输出:
- 完整类名
- 简单类名
- 包名
7.4 Debug
下面代码存在问题:
Class<?> clazz =
Class.forName("Student");
项目中的实际类是:
com.example.reflect.Student
回答:
- 可能出现什么异常?
- 错误产生在哪个阶段?
- 应该如何修改?
- 为什么框架配置文件里的类名通常使用全限定类名?
7.5 综合训练
编写:
public static void printTypeInfo(Object object)
要求输出任意对象:
运行时完整类名
简单类名
包名
父类
直接实现的接口
是否为数组
是否为枚举
是否为接口
要求这个方法不能针对:
Student
Dog
User
等具体业务类型写特殊判断。
7.6 本章验收
不看资料完成:
Student.class
Class.forName("完整类名")
student.getClass()
并能够口述:
.java
↓
.class
↓
类加载
↓
运行时类型
↓
Class
↓
Constructor / Field / Method
如果能够完整解释这条链路,就已经抓住了反射的第一核心。