首页 / Java 入门教程 / 反射基础(Class 对象)

Java 入门教程

反射基础(Class 对象)

本教程共 100 篇 · 第 52 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程反射ReflectionClass对象运行期

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() 输出 [IString[].class.getName() 输出 [Ljava.lang.String;。想看人类可读的形式,改用 getSimpleName()(得到 int[])或 getCanonicalName()

小结

  • 反射让程序在运行期查看并操作任意类,前提是不知道(或不必写死)具体类型。
  • 每个被加载的类在 JVM 里对应唯一一个 Class 对象,封装了它的全部结构信息。
  • 获取 Class 三种方式:类名.class、对象.getClass()Class.forName("全类名")
  • getMethods()/getFields()/getConstructors() 只看 public 且含继承;getDeclaredXxx() 只看本类(含非 public)。
  • 拿到 Class 后就能查父类、接口、方法、字段、构造方法——这是下一章「动态创建对象、调用方法」的基础。

下一章我们用反射真正去「动手」:创建对象、读写字段、调用方法,并理解框架底层是怎么用它的。