1. 什么是反射
Java 中的反射是一种机制,允许程序在运行时检查甚至修改自身的结构。就好像你能看进自己的内部并发问:“我有哪些字段和方法?我有什么构造器?我究竟是个什么类?”——甚至还能调用自己的私有方法。
正式定义:
反射是一套 API,允许在程序运行时获取关于类、接口、字段、方法和构造器的信息,并对其进行操作。
为什么需要反射?
- 框架:Spring、Hibernate、JUnit 利用反射来做“魔法”:自动创建对象、依赖注入、按名称或注解调用方法。
- 序列化:将对象转换为 JSON/XML 及其逆过程——需要知道字段并能访问它们。
- 测试:自动查找并调用带有注解(例如 @Test)的方法。
- 动态加载类:在运行时按名称加载类(插件、驱动)。
- IDE 与代码分析:自动补全、检查、重构。
生活中的例子
public class Person {
private String name;
private int age;
public void sayHello() {
System.out.println("你好,我叫 " + name);
}
}
借助反射,我们可以在运行时得知类 Person 具有字段 name 和 age,以及方法 sayHello。更进一步,还可以修改私有字段的值或按名称调用方法。
一点幽默
可以说,反射就像《黑客帝国》:你意识到你的全部代码其实都是可在运行时被观察和修改的数据。只是别忘了那颗关于安全性的“红色药丸”!
2. Class 类:反射的核心
在 Java 中,每个对象和每个类型在运行时都关联着一个特殊的 Class 类型的对象。这个对象就是该类型的“元信息”。类 java.lang.Class<T> 是进入反射世界的中心入口。
如何获得对象 Class?
有几种方式:
- 通过 .class
如果类在编译期已知,这是最直接且安全的方式:Class<Person> personClass = Person.class; - 通过对象:getClass()
如果你已经有实例:
这里 clazz 是一个 Class 对象,描述了对象 p 的实际类。Person p = new Person(); Class<?> clazz = p.getClass(); - 通过字符串:Class.forName()
当类名只在运行时才知道时:
适用于加载插件、驱动等动态场景。Class<?> clazz = Class.forName("com.example.Person");
示例:获取类名
Person p = new Person();
Class<?> clazz = p.getClass();
System.out.println(clazz.getName()); // 输出:Person or com.example.Person
表格:获取 Class 对象的主要方式
| 获取方式 | 何时使用 | 示例 |
|---|---|---|
|
当类在编译期已知时 | |
|
当你已有对象时 | |
|
当类名仅在运行时可知时 | |
示意图:对象及其 Class
+-------------------+
| Person p |
+-------------------+
|
v
+-------------------+
| p.getClass() |
+-------------------+
|
v
+-------------------+
| Class<Person> |
+-------------------+
通过 Class 进行类型检查
有时需要判断对象是否属于某个确切的类:
if (p.getClass() == Person.class) {
System.out.println("This is definitely a Person!");
}
或者考虑继承关系——使用常规的 instanceof 运算符:
if (p instanceof Person) {
// 按常规方式工作
}
3. 什么时候需要反射,什么时候不需要
能力越大,责任越大。反射是强大的工具,但只有在确有必要时才应使用。
反射必不可少的场景
- 框架和库:Spring、Hibernate、JUnit、Jackson——自动化、“魔法”,减少“手写”代码。
- 序列化:需要获知对象的字段以将其转换为 JSON/XML。
- 插件与扩展:类是动态加载的,其结构事先未知。
为什么不应滥用反射
- 丧失类型安全:错误在运行时才暴露。
- 维护难度上升:代码不那么直观。
- 性能:比直接调用更慢。
- 安全性:可能绕过封装并暴露私有数据。
示例:何时不需要反射
p.sayHello();
示例:何时必须使用反射
你在编写一个迷你测试框架:用户用注解 @Test 标记方法,程序需要自动查找并调用它们——没有反射就做不到。
4. 演示:认识 Class
示例:输出类名及其父类
public class ReflectionDemo {
public static void main(String[] args) {
String s = "Hello, reflection!";
Class<?> clazz = s.getClass();
System.out.println("Class name: " + clazz.getName());
System.out.println("简单Class name: " + clazz.getSimpleName());
System.out.println("包: " + clazz.getPackageName());
System.out.println("父类: " + clazz.getSuperclass().getName());
}
}
输出:
Class name: java.lang.String
简单Class name: String
包: java.lang
父类: java.lang.Object
示例:获取基本类型的 Class
Class<Integer> intClass = int.class;
System.out.println(intClass.getName()); // int
Class<?> doubleClass = double.class;
System.out.println(doubleClass.getName()); // double
示例:获取数组的 Class
int[] arr = new int[10];
Class<?> arrClass = arr.getClass();
System.out.println(arrClass.getName()); // [I(数组的特定格式)
示例:检查类型归属
if (arrClass.isArray()) {
System.out.println("This is an array!");
}
5. 实践:迷你程序“这是什么类?”
我们编写一个程序,接收类的全限定名(例如,"java.util.ArrayList"),并输出其基本信息。
import java.util.Scanner;
public class ClassInfoPrinter {
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入类的全限定名:");
String className = scanner.nextLine();
Class<?> clazz = Class.forName(className);
System.out.println("Class name: " + clazz.getName());
System.out.println("包: " + clazz.getPackageName());
System.out.println("父类: " + clazz.getSuperclass().getName());
Class<?>[] interfaces = clazz.getInterfaces();
System.out.print("实现的接口: ");
for (Class<?> i : interfaces) {
System.out.print(i.getName() + " ");
}
System.out.println();
}
}
运行示例:
请输入类的全限定名:java.util.ArrayList
Class name: java.util.ArrayList
包: java.util
父类: java.util.AbstractList
实现的接口: java.util.List java.util.RandomAccess java.lang.Cloneable java.io.Serializable
关于 Class 与反射的有趣事实
- JVM 中每个已加载的类型都对应唯一的代表——对象 Class。同一类型的所有实例共享同一个 Class<T>。
- 可以判定类型的类别:isInterface()、isEnum()、isArray()、isPrimitive() 等。
- 如果在 Class.forName 中给出错误的名称,会得到异常 ClassNotFoundException。
- 借助反射,即便在编译期不知道类,也能创建对象——我们将在下一讲讨论。
6. 初识反射时的常见错误
错误 1:指望反射很快。
实际上,反射比普通的方法调用和字段访问要慢。不要把它用在每一件小事上。
错误 2:尝试为不存在的类获取 Class。
如果名称写错,会得到 ClassNotFoundException。请对该异常进行处理。
错误 3:混淆类对象与实例。
对象 Class 描述的是类型的结构,它不是该类型的实例。
错误 4:不必要地使用反射。
如果能用普通代码解决——那就优先这样做。反射适用于动态性、框架、插件等场景。
错误 5:不慎处理私有字段和方法。
反射可以绕过封装,这在大型项目中可能导致错误和安全问题。
GO TO FULL VERSION