反射基础(Class 对象)
本教程共 100 篇 · 第 52 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
52. 反射基础(Class 对象)
本节目标:理解「反射」是什么、Class 对象是什么,掌握获取 Class 对象的三种方式,并学会用反射「查看」一个类有哪些字段和方法。
反射解决什么问题
正常情况下,你写代码时已经知道要用哪个类,直接 new、直接调方法就行。但有些场景,代码里根本不知道具体类是谁。
比如框架加载你写的某个类:框架只知道一个类名字符串 "com.xxx.MyService",它并不 import 这个类,却要在运行期把对象创建出来、调它的方法。这种「运行期、对类型一无所知情况下操作对象」的能力,就叫反射(Reflection)。
Note一句话理解反射:通常是我们写代码去「指挥」类;反射是程序在运行期反过来「查看并操纵」类和对象自己。Java 把每个类的结构信息(字段、方法、构造方法……)都封装成了对象,反射就是拿到这些对象来用。
一切从 Class 对象开始
JVM 每加载一个 .class 文件,就会在内存里生成一个对应的 Class 对象,里面装着这个类的全部结构信息。我们做反射,第一步永远是先拿到这个 Class 对象。
拿到之后,就能调用它的一系列方法:getFields()、getMethods()、getConstructors()、getSuperclass() 等等,把类的家底翻出来。
获取 Class 对象的三种方式
方式一:类名.class(编译期已知类型)
Class<String> cls = String.class;
```bash
最简单,不会触发类的初始化(不执行静态代码块),只是拿到类型引用。
**方式二:对象.getClass()(已经有实例)**
```java
String s = "hi";
Class<? extends String> cls = s.getClass();
通过实例反查它属于哪个类。因为 getClass() 定义在 Object 上,任何对象都能调用。
方式三:Class.forName(“全类名”)(运行期才知道类名)
Class<?> cls = Class.forName("java.lang.String");
```bash
这是反射最常用的入口:类名是字符串,可以来自配置文件、注解、用户输入。**注意要写完整包名**,否则找不到类会抛 `ClassNotFoundException`。
```java
// 三种方式拿到的其实是同一个 Class 对象
System.out.println(String.class == "hi".getClass()); // true
System.out.println(String.class == Class.forName("java.lang.String")); // true
Tip判断两个对象是不是同一类型,可以直接用
==比较它们的Class对象(而不必用equals)。因为同一个类在 JVM 里只有一个Class实例。
用反射查看类的结构
拿到 Class 后,可以「检视」它有哪些公开成员:
import java.lang.reflect.*;
public class Main {
public static void main(String[] args) {
Class<?> cls = String.class;
// 类名
System.out.println(cls.getName()); // java.lang.String
// 所有公开方法
for (Method m : cls.getMethods()) {
System.out.println("方法:" + m.getName());
}
// 所有公开字段
for (Field f : cls.getFields()) {
System.out.println("字段:" + f.getName());
}
// 直接父类
System.out.println("父类:" + cls.getSuperclass());
}
}
```bash
> [!NOTE]
> `getMethods()` 返回的是**公开(public)方法**,并且包含从父类继承来的;如果你只想看「这个类自己声明的」(不管是不是 public),用 `getDeclaredMethods()`。字段、构造方法同理,都有 `getXxx()` 和 `getDeclaredXxx()` 两套:带 `Declared` 的只看本类,不带的有继承、只限 public。
## getXxx 与 getDeclaredXxx 速查
这两套 API 名字只差一个词,行为却是两条轴上的区别:一条是「看不看父类」,一条是「管不管访问修饰符」。
| 方法 | 访问级别 | 是否含父类成员 |
| --- | --- | --- |
| `getFields()` | 仅 public | 含(包括接口里的常量) |
| `getDeclaredFields()` | 全部(含 private) | 不含,只看本类 |
| `getMethods()` | 仅 public | 含 |
| `getDeclaredMethods()` | 全部 | 不含 |
| `getConstructors()` | 仅 public | 构造方法本就不继承 |
| `getDeclaredConstructors()` | 全部 | 同上 |
看个能跑的对比:
```java
class Base { public int pub; private int hidden; }
class Child extends Base { private String own; }
Class<?> c = Child.class;
System.out.println(c.getFields().length); // 1 → 继承来的 pub
System.out.println(c.getDeclaredFields().length); // 1 → 自己的 own
一个是 pub,一个是 own,数字碰巧都是 1,但含义完全不同。
想读私有字段却顺手写成 getField("own"),运行时就会挨这一下:
Exception in thread "main" java.lang.NoSuchFieldException: own
at java.base/java.lang.Class.getField(Class.java:2610)
```bash
名字明明没敲错,找不到只因为 `getField` 只认 public。方法那一侧对应的是 `NoSuchMethodException`,成因一模一样。
> [!WARNING]
> 初学者调反射,十次报「找不到成员」有八次是 `Declared` 那个词漏了。记住口诀:要碰私有的,就带 `Declared`。
## 查看构造方法
```java
for (Constructor<?> c : cls.getConstructors()) {
System.out.println("构造方法参数个数:" + c.getParameterCount());
}
这为下一章「动态创建对象」做准备——我们要挑一个构造方法,再用它去 new 实例。
查看父类与接口
Class<?> superCls = cls.getSuperclass(); // 直接父类
Class<?>[] interfaces = cls.getInterfaces(); // 实现的接口
```bash
这在框架里很常用:比如判断「某个类是不是实现了 `Runnable` 接口」,就能用 `cls.getInterfaces()` 遍历,或更直接地用 `Runnable.class.isAssignableFrom(cls)`。
## 完整可运行示例
```java
import java.lang.reflect.*;
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> cls = Class.forName("java.util.ArrayList");
System.out.println("类名:" + cls.getName());
System.out.println("父类:" + cls.getSuperclass().getName());
System.out.println("实现的接口数:" + cls.getInterfaces().length);
System.out.println("声明的构造方法数:" + cls.getDeclaredConstructors().length);
}
}
运行后会输出 ArrayList 的类名、父类 AbstractList、实现的若干接口以及构造方法数量——这些都是 JVM 在运行期「现查」出来的,没有一句硬编码。
反射能碰 private 吗
可以,但要先「松绑」。getDeclaredField / getDeclaredMethod 能拿到私有的成员,直接读写或调用会抛 IllegalAccessException。这时调用 setAccessible(true) 关掉访问检查即可操作——下一章会演示。代价是破坏了封装,只在工具、测试、序列化等场景合理。
反射的性能与安全
反射比直接写死调用略慢,因为运行期要解析类结构、做访问检查。对框架来说这点开销可以忽略;但业务 hot 路径上反复用反射,就需要留意。另外,Java 9 引入的模块化(JPMS)之后,跨模块反射可能被判为非法访问而被拒绝,这是做反射要留意的边界。
Warning反射跳过了编译期类型检查:方法名拼错、参数类型不对,编译时不会报错,要等到运行期才暴露。所以反射代码务必写好测试,关键字符串尽量用常量。
常见疑问
Class.forName() 和 类名.class 只是写法不同吗?
不止。Class.forName() 默认会触发类的初始化,也就是执行静态代码块和静态字段赋值;类名.class 不会。用第三个参数可以关掉这个行为:
Class<?> c = Class.forName("com.demo.Foo", false, loader); // 加载但不初始化
```java
老式 JDBC 教程里那句 `Class.forName("com.mysql.cj.jdbc.Driver")`,靠的正是「初始化时执行静态块完成驱动注册」这个副作用。
**基本类型有 Class 对象吗?**
有,而且和包装类的不是同一个:
```java
System.out.println(int.class == Integer.class); // false
System.out.println(int.class == Integer.TYPE); // true
按参数类型查方法时这点很要命——目标方法参数是 int,你传 Integer.class 就找不到,会抛 NoSuchMethodException。
数组的类名为什么打印出来是一串怪符号?
数组也是类,int[].class.getName() 输出 [I,String[].class.getName() 输出 [Ljava.lang.String;。想看人类可读的形式,改用 getSimpleName()(得到 int[])或 getCanonicalName()。
小结
- 反射让程序在运行期查看并操作任意类,前提是不知道(或不必写死)具体类型。
- 每个被加载的类在 JVM 里对应唯一一个
Class对象,封装了它的全部结构信息。 - 获取
Class三种方式:类名.class、对象.getClass()、Class.forName("全类名")。 getMethods()/getFields()/getConstructors()只看 public 且含继承;getDeclaredXxx()只看本类(含非 public)。- 拿到 Class 后就能查父类、接口、方法、字段、构造方法——这是下一章「动态创建对象、调用方法」的基础。
下一章我们用反射真正去「动手」:创建对象、读写字段、调用方法,并理解框架底层是怎么用它的。