1. 引言
反射(Reflection)是 Java 语言中一个强大且颇具争议的特性。它允许程序在运行时动态地获取类的信息、创建对象、调用方法和访问字段,而这一切在编译期并不需要知道目标类的具体类型。正是这种「动态性」,让 Spring、MyBatis、Hibernate 等众多框架得以实现配置驱动、注解解析和对象映射等核心能力。
本文将从反射的基本概念讲起,逐步深入 Class 对象的获取、构造器操作、方法调用、字段访问,并结合实战场景分析其性能问题与使用注意事项,帮助你在实际开发中正确、高效地使用反射。
2. 反射是什么
在 Java 中,通常的编程方式是:编译期确定类型,通过new关键字创建对象,通过对象直接调用方法。这种方式称为静态编译,代码简洁且运行高效。
而反射则是反过来:把原本在编译期完成的工作推迟到运行期。程序可以在运行时拿到任意一个类的Class对象,进而:
- 获取类的全限定名、包名、修饰符、父类、接口等元信息;
- 获取类中声明的构造器、方法、字段;
- 动态创建对象,即使构造器是私有的;
- 动态调用方法,即使方法是私有的;
- 动态读写字段,即使字段是私有的;
- 获取并解析类、方法、字段上的注解。
用一句话概括:反射是在运行时检查和操作类的能力。它把 Java 从「编译期类型安全」的牢笼中解放出来,换取了极大的灵活性。
3. 反射的核心:Class 对象
反射的一切都始于Class对象。JVM 在加载某个类时,会为其在方法区(Java 8 之后为元空间)创建一个唯一的Class实例,用来保存该类的类型信息。我们通常有三种方式获取它:
// 方式一:通过类的全限定名(最常用,适合配置文件场景)Class<?>clazz1=Class.forName("com.example.User");// 方式二:通过类字面量(编译期已知类型,性能最好)Class<User>clazz2=User.class;// 方式三:通过对象实例Useruser=newUser();Class<?extendsUser>clazz3=user.getClass();三者获取到的是同一个Class对象:
System.out.println(clazz1==clazz2);// trueSystem.out.println(clazz2==clazz3);// true拿到Class对象后,就可以读取各种元信息:
System.out.println("类名:"+clazz.getName());System.out.println("简单类名:"+clazz.getSimpleName());System.out.println("包名:"+clazz.getPackageName());System.out.println("是否接口:"+clazz.isInterface());System.out.println("是否注解:"+clazz.isAnnotation());System.out.println("父类:"+clazz.getSuperclass());System.out.println("实现的接口:"+Arrays.toString(clazz.getInterfaces()));4. 通过反射创建对象
4.1 使用 Class.newInstance()
这是最古老的方式,要求目标类提供无参构造器:
Class<?>clazz=Class.forName("com.example.User");Useruser=(User)clazz.newInstance();但由于它无法传递构造参数,且抛出的异常类型不友好,从 Java 9 开始已被标记为过时,官方推荐使用Constructor。
4.2 使用 Constructor 对象
先通过getConstructor或getDeclaredConstructor获取构造器,再调用newInstance:
publicclassUser{privateStringname;privateintage;publicUser(){}publicUser(Stringname,intage){this.name=name;this.age=age;}}// 获取带两个参数的公有构造器Constructor<User>constructor=User.class.getConstructor(String.class,int.class);Useruser=constructor.newInstance("张三",20);System.out.println(user);如果需要访问私有构造器,则使用getDeclaredConstructor并调用setAccessible(true):
Constructor<User>privateConstructor=User.class.getDeclaredConstructor(String.class);privateConstructor.setAccessible(true);// 突破访问权限Useruser=privateConstructor.newInstance("李四");需要注意的是,从 Java 9 模块化之后,setAccessible(true)对 JDK 内部类的访问受到了更严格的限制,但对开发者自己编写的类依然有效。
5. 通过反射调用方法
方法与构造器类似,先获取Method对象,再调用invoke:
publicclassCalculator{publicintadd(inta,intb){returna+b;}privateStringformat(intresult){return"结果是:"+result;}}Calculatorcalculator=newCalculator();// 调用公有方法Methodadd=Calculator.class.getMethod("add",int.class,int.class);Objectresult=add.invoke(calculator,3,5);System.out.println(result);// 8// 调用私有方法Methodformat=Calculator.class.getDeclaredMethod("format",int.class);format.setAccessible(true);Objectmessage=format.invoke(calculator,8);System.out.println(message);// 结果是:8invoke的第一个参数是目标对象实例,后面的参数是方法的实参。如果是静态方法,第一个参数可以传null。
6. 通过反射访问字段
字段访问使用Field对象:
publicclassUser{privateStringname="默认姓名";publicintage=18;}Useruser=newUser();// 获取公有字段FieldageField=User.class.getField("age");intage=ageField.getInt(user);System.out.println("修改前 age:"+age);// 18ageField.setInt(user,25);// 获取私有字段FieldnameField=User.class.getDeclaredField("name");nameField.setAccessible(true);Stringname=(String)nameField.get(user);System.out.println("修改前 name:"+name);// 默认姓名nameField.set(user,"张三");System.out.println("修改后:"+user);getField只能获取public字段(包括父类继承的公有字段),getDeclaredField可以获取本类声明的所有字段(不含继承),包括私有字段。
7. 反射读取与解析注解
反射是注解机制的基石。框架之所以能识别@Component、@Autowired、@Table等注解,靠的就是反射:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public@interfaceTableName{Stringvalue();}@TableName("t_user")publicclassUser{}Class<User>clazz=User.class;if(clazz.isAnnotationPresent(TableName.class)){TableNametableName=clazz.getAnnotation(TableName.class);System.out.println("表名:"+tableName.value());}方法、字段上的注解解析方式类似,通过Method.getAnnotation或Field.getAnnotation即可。需要注意的是,注解的@Retention必须是RUNTIME,否则运行时反射无法读取。
8. 反射的应用场景
反射在日常业务代码中可能不常见,但它几乎渗透在每一个主流框架中:
- 框架与容器:Spring 的 IoC 容器通过反射创建 Bean、注入依赖;Spring MVC 通过反射调用 Controller 方法。
- ORM 映射:MyBatis、Hibernate 通过反射把查询结果封装成实体对象。
- 序列化与反序列化:Jackson、Gson 通过反射把 JSON 转换成对象。
- 动态代理:JDK 动态代理本质上是基于接口 + 反射实现的。
- 注解处理:各种自定义注解的运行时解析都依赖反射。
- 工厂模式与通用工具:根据类名或配置动态加载不同实现,实现插件化架构。
一个经典的工厂示例:
publicclassAnimalFactory{publicstaticAnimalcreate(StringclassName){try{return(Animal)Class.forName(className).newInstance();}catch(Exceptione){thrownewRuntimeException("创建失败:"+className,e);}}}9. 反射的性能问题与优化
反射功能强大,但代价是性能损耗。主要原因在于:
- 运行时进行类型检查、安全性检查;
Method.invoke需要做参数装箱拆箱和数组分配;- 无法享受 JIT 编译器的部分优化。
因此,在高频调用场景(如循环内、高并发接口)中,应尽量避免直接使用反射。常见优化手段:
- 缓存
Class、Method、Field等对象:获取元信息是较昂贵的操作,拿到后应缓存复用。 - 调用
setAccessible(true):可以关闭一次安全检查,减少一部分开销。 - 尽量减少装箱拆箱:必要时使用
invokeExact(MethodHandle 场景)。 - 优先使用直接调用或 Lambda:现代 Java 推荐使用方法引用、
MethodHandle或VarHandle作为反射的替代方案。
实际上,对于仅调用一次的反射,开销通常可以接受;需要警惕的是把它放在循环或热路径中未加优化。
10. 反射的局限性与注意事项
反射并非万能,使用时需要注意:
- 破坏封装:
setAccessible(true)可以绕过访问控制,滥用会破坏类的封装性。 - 绕过泛型检查:反射可以把不符合泛型约束的值塞进集合中,导致隐藏的类型安全隐患。
- 错误延迟到运行期:编译期无法检查方法名、字段名的正确性,拼写错误在运行时才暴露。
- 异常类型复杂:反射相关操作会抛出
ClassNotFoundException、NoSuchMethodException、InvocationTargetException等多种受检异常。 - 可读性下降:反射代码绕来绕去,维护成本高于直接的静态调用。
因此,绝大多数业务代码应优先使用直接调用,反射更适合框架、工具类、基础设施等需要高度抽象的场景。
11. 总结
反射是 Java 提供的一把「万能钥匙」:它让我们可以在运行时窥探和操作任何类,从而支撑起框架化、配置化、插件化的现代软件开发范式。但正如钥匙能开锁也能破门,反射带来灵活性的同时,也牺牲了编译期安全、封装性和运行性能。
理解反射的关键,是理解Class对象以及Constructor、Method、Field这一套运行时元信息 API。掌握它们之后,再去阅读 Spring、MyBatis 的源码,你会发现自己看到的底层逻辑豁然开朗。
建议你亲手完成以下练习巩固:
- 写一个通用的对象拷贝工具,通过反射实现两个对象属性拷贝;
- 写一个简单的依赖注入容器,通过反射完成对象创建与字段注入;
- 通过反射解析自定义注解,实现一个轻量级 ORM 映射。