反射概述与 Class 对象 | JavaSE

反射概述与 Class 对象

一、学习目标

学完本章,你应该能够:

  • 准确解释反射(Reflection)是什么,以及它与普通 Java 编程方式的区别。
  • 理解 Class 对象代表什么,以及为什么反射的第一步是获取 Class 对象。
  • 掌握获取 Class 对象的三种常见方式:类名.classClass.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();

整个过程是:

程序员提前知道类
        ↓
直接编写类型
        ↓
创建对象
        ↓
调用方法

但是现在提出一个完全不同的问题:

如果程序运行之前,我根本不知道以后要处理的是 StudentTeacherUser 还是其他类,该怎么办?

例如一个框架收到:

com.example.Student

这样的字符串。

框架希望自己:

  1. 找到这个类。
  2. 知道它有哪些构造器。
  3. 知道它有哪些字段。
  4. 知道它有哪些方法。
  5. 创建对象。
  6. 调用方法。
  7. 读取或修改字段。

这时候就不能简单写:

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 知识问答

  1. 什么是反射?
  2. 为什么说反射可以在运行时“解剖类”?
  3. Class 对象是什么?
  4. 反射为什么通常先获取 Class
  5. 获取 Class 对象的三种常见方式是什么?
  6. .class 字节码文件和 Class 对象有什么区别?
  7. 类名.class对象.getClass() 有什么区别?
  8. Class.forName() 为什么特别适合配置驱动场景?
  9. 为什么框架不应该为每一种业务类写大量 instanceof
  10. Class<?> 中的 ? 表示什么?

7.2 代码阅读

阅读:

Animal animal = new Dog();

Class<?> c1 = Animal.class;
Class<?> c2 = animal.getClass();
Class<?> c3 = Dog.class;

回答:

  1. c1 表示哪个类型?
  2. c2 表示哪个类型?
  3. c3 表示哪个类型?
  4. c1 == c2 是否成立?
  5. c2 == c3 是否成立?
  6. 这个案例和多态有什么关系?

7.3 手写代码

定义:

public class Book {

    private String name;
    private double price;
}

要求不查看资料完成:

  1. 使用 Book.class 获取 Class
  2. 使用 Class.forName() 获取 Class
  3. 使用 book.getClass() 获取 Class
  4. 判断三者是否为同一个 Class 对象。
  5. 输出:
    • 完整类名
    • 简单类名
    • 包名

7.4 Debug

下面代码存在问题:

Class<?> clazz =
        Class.forName("Student");

项目中的实际类是:

com.example.reflect.Student

回答:

  1. 可能出现什么异常?
  2. 错误产生在哪个阶段?
  3. 应该如何修改?
  4. 为什么框架配置文件里的类名通常使用全限定类名?

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

如果能够完整解释这条链路,就已经抓住了反射的第一核心。