Java反射11题拆解:Class获取、对象创建、私有访问与性能优化
简介:面向Java求职者与初中级开发者的反射专题面试题整理,围绕11个高频问题系统梳理运行时类信息获取、对象动态创建与方法调用等核心考点,每道题给出简洁答案与关键结论。内容覆盖new与反射创建对象的效率差异、反射在JDBC及Hibernate、Spring等框架中的应用、获取Class对象的多种途径,以及Field、Method、Constructor等API的具体用法;同时延伸到反射优缺点、setAccessible(true)、实例缓存与ReflectASM等性能优化思路,并配有动态创建对象实例的代码片段,便于对照理解调用流程。资源包为1个PDF文件,约569KB,篇幅紧凑,适合打印或移动端随时翻阅。目前已有230人学习下载。可用于面试前快速自查、系统复习与梳理反射知识框架,也能帮助开发者在阅读框架源码或排查反射相关问题时建立清晰的API使用脉络,也可作为日常开发中的反射速查手册。
1. 11 道反射题拆开看:背得出定义,未必答得到点上
面试里问反射,多数人能背出“运行时获取类的信息”,但面试官再追问一句“Class.forName 和类名.class 有什么区别”“为什么 Class.newInstance() 被标成废弃”“setAccessible(true) 到底关了什么东西”,回答就容易接不上。这份 11 题的 PDF 正好对准这些容易卡住的地方:从除 new 之外还能怎么创建对象、反射与 new 的效率差距,到四种获取 Class 的方式、Field/Method/Constructor 的职责边界、优缺点与提速方案,按问答顺序一路排到动态创建实例的完整代码。它的价值在于密度够、答案直接可用,而不是概念堆砌。适合三类人:准备 Java 后端面试的初中级开发、要手写简易依赖注入容器或 ORM 练手的人、被框架源码里的反射调用绕晕、想搞清运行时究竟发生了什么的一线从业者。下面按题库顺序,把每一题的答案还原成能跑的代码。
2. 四种拿到 Class 对象的方式:选型账与初始化时机
反射的一切操作都建立在Class实例上,这一章先把入口打通。题目里给了四条路径,但真正决定你选哪条的,不是“哪种最常用”,而是“哪种会触发类的静态初始化”。这个差别在驱动注册、配置加载这类场景里,直接决定程序启动时会不会多跑一段副作用代码。
2.1 为什么反射的入口一定是 Class 对象
JVM 在加载一个类时,会为它在方法区(新版本叫元空间)里生成一份运行时类型信息,并把这份信息的门牌号包装成一个Class对象。同一个类由同一个类加载器加载,拿到的Class实例就是同一个,==比较为 true;换一个类加载器加载同名类,就是两个不同的Class,这也是热部署和插件隔离里最常翻车的地方。
反射 API 的设计就围绕这个门牌号展开:Field描述字段、Method描述方法、Constructor描述构造器,它们全部由Class派生出来。你不可能绕过Class直接拿到一个Method,因为这四个类本质上都是对同一份元数据的视图。理解了这一点,就能明白为什么第 9 题把“获取 Class 对象”列为反射三步走的第一步——它是唯一入口,拿不到它,后面一步都走不下去。
顺带把一个容易混的点说清:Class对象不是被反射创建出来的,它由 JVM 维护。反射做的事情是“读这份元数据”和“基于这份元数据发起调用”,而不是“造出一个类”。
2.2 四条路径的代码与代价对比
先把四种写法摆在一起,注意每一种的返回值语义差别。
// 方式一:Class.forName —— 最常用,会触发类的静态初始化 Class<?> a = Class.forName("demo.reflect.Person"); // 方式二:类名.class —— 编译期就确定,不触发静态初始化,也不会抛 ClassNotFoundException Class<?> b = Person.class; // 方式三:对象.getClass() —— 已持有实例时最省事,拿到的是运行时实际类型 Person p = new Person(); Class<?> c = p.getClass(); // 方式四:包装类的 TYPE 字段 —— 拿到的是基本类型的 Class,不是包装类 Class<?> d = Integer.TYPE; // int Class<?> e = Integer.class; // java.lang.Integer System.out.println(d == e); // false,这两个不是同一个 Class四者的差异集中在一张表里,抄下来比背定义管用:
| 方式 | 触发静态初始化 | 编译期检查 | 典型场景 | 主要问题 |
|---|---|---|---|---|
Class.forName("全限定名") | 是 | 无,类名写错只在运行时暴露 | 配置驱动的动态加载、驱动注册 | 类名拼错抛ClassNotFoundException;静态块副作用提前执行 |
类名.class | 否 | 有,类不存在编译不过 | 代码里已知类型、反射工具内部 | 无法做到“类名来自配置文件” |
对象.getClass() | 否 | 有 | 已有实例,需要拿真实运行时类型 | 拿不到接口/父类的 Class,多态场景要注意 |
包装类.TYPE | 否 | 有 | 反射匹配基本类型参数 | 只覆盖八种基本类型,容易和包装类混用 |
表格里“触发静态初始化”这一列是重点。驱动类通常会在静态代码块里把自己注册进DriverManager,所以那种场景下必须用Class.forName让它初始化;反过来,如果你只是想读一读某个类的字段结构、并不希望它的静态块跑起来,就老老实实用类名.class或者Class.forName(name, false, loader)——那个false就是“不初始化”的开关,在写通用工具类的时候非常关键。
还有一个隐形成本:Class.forName(String)用的是调用方的类加载器。在容器、插件、多模块环境下,调用方加载器未必能看到目标类,这时必须换成三参数版本,显式传入业务侧的ClassLoader。这一点题库里没写,但它是“本地跑得好好的,一上环境就ClassNotFoundException”的头号原因。
2.3 把 PDF 里的示例跑起来:一个最小可运行骨架
光看不练,反射的坑一个都记不住。下面这份骨架把第 11 题的三段示例串成一个可执行程序,直接抄就能跑。
// Person.java —— 被反射操作的目标类 package demo.reflect; public class Person { private String name; private String gender; private int age; public Person() { } // 无参构造器,Class.newInstance() 依赖它 public Person(String name, String gender, int age) { this.name = name; this.gender = gender; this.age = age; } private String greet(String prefix) { // 私有方法,用来验证 setAccessible return prefix + ", " + name; } @Override public String toString() { return "Person{name='" + name + "', gender='" + gender + "', age=" + age + "}"; } }目标类准备好之后,主程序按“拿 Class → 看结构 → 造实例 → 调方法”的顺序走:
// ReflectDemo.java —— 反射三步走的完整示例 package demo.reflect; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; public class ReflectDemo { public static void main(String[] args) throws Exception { // 第一步:获取 Class 对象(这里用最常用的 forName) Class<?> clazz = Class.forName("demo.reflect.Person"); // 第二步:读结构。getDeclaredXxx 只取本类声明的成员,包含 private for (Method m : clazz.getDeclaredMethods()) { System.out.println("方法: " + m); } for (Field f : clazz.getDeclaredFields()) { System.out.println("字段: " + f); } for (Constructor<?> c : clazz.getDeclaredConstructors()) { System.out.println("构造器: " + c); } // 第三步:动态创建实例并访问成员 Constructor<?> ctor = clazz.getDeclaredConstructor(String.class, String.class, int.class); Object obj = ctor.newInstance("A同学", "男", 20); Field ageField = clazz.getDeclaredField("age"); ageField.setAccessible(true); // 关掉本次访问的权限检查 ageField.set(obj, 21); // 改一个私有字段 Method greet = clazz.getDeclaredMethod("greet", String.class); greet.setAccessible(true); System.out.println(greet.invoke(obj, "hello")); // hello, A同学 System.out.println(obj); } }编译运行两条命令,注意包路径要和生产目录结构对上:
# 编译到 out 目录,源码按包名放在 demo/reflect 下 javac -d out demo/reflect/Person.java demo/reflect/ReflectDemo.java # -cp 指定类路径,主类写全限定名 java -cp out demo.reflect.ReflectDemo这段代码里有三个参数细节值得盯住。第一,getDeclaredConstructor(String.class, String.class, int.class)的参数是精确类型,int.class不能写成Integer.class,两者是不同的Class,写错直接抛NoSuchMethodException。第二,setAccessible(true)是设在具体的Field/Method/Constructor实例上的,不是设在Class上,所以每拿到一个新成员对象都要单独设一次,这也是后面“缓存”技巧要解决的问题。第三,invoke的返回值统一是Object,原始类型会被装箱,需要时自己拆。
跑通之后,把getDeclaredMethods()换成getMethods()再跑一次,你会看到方法列表明显变长——前者只吐本类声明的成员,后者吐的是 public 成员且包含继承来的。这个对比比背十遍定义都直观。
3. 反射创建对象的两种写法:newInstance 与 Constructor 的分水岭
题库第 11 题把动态创建实例分成两条路:Class.newInstance()和Constructor.newInstance()。很多资料只说“后者可以选构造器”,没讲清前者为什么被废弃、什么时候只能用它、什么时候必须换成后者。这一章把这条分水岭讲透。
3.1 Class.newInstance() 的三个硬约束
clazz.newInstance()写起来最省事,一行就能出对象,但它有三条不写在方法签名上的约束,踩中任何一条就是运行时异常。
第一条,目标类必须有可访问的无参构造器。注意“可访问”三个字:它走的是和普通代码一样的访问权限检查,构造器是 private 的时候,即便你同一个包也未必能调,得先setAccessible——但Class.newInstance()本身没有给你设可访问性的机会,所以这条路直接堵死。
第二条,异常处理很难看。构造器里抛出的受检异常会被直接穿透出来,不包装。也就是说,你明明写的是clazz.newInstance(),编译器却要求你捕获一个跟你当前方法毫无关系的受检异常。这就是它在较新 JDK 里被标记为废弃的核心理由之一:异常语义不干净。
第三条,错误信息弱。构造器里的空指针、参数校验失败,抛出来后堆栈指向的是反射内部,不细看很难定位到业务构造函数那一行。
所以结论很干脆:Class.newInstance()只适合“目标类有无参构造器,且你不关心异常细节”的玩具级场景。一旦要传参、要访问私有构造器、要把异常包成自己的业务异常,就该换Constructor。题库里那句“需要默认空构造器”说得没错,但只说了三条约束里的一条,面试时能补上另外两条,档次立刻不一样。
3.2 getDeclaredConstructor 与 newInstance 的参数传递
Constructor这条路的核心是“先选中一个构造器,再调它”。选中靠参数类型列表,调用靠实参列表,两个列表必须严格对应。
Class<?> clazz = Class.forName("demo.reflect.Person"); // 选中「(String, String, int)」这个构造器,参数是类型,不是值 Constructor<?> ctor = clazz.getDeclaredConstructor(String.class, String.class, int.class); // 私有构造器同样要开权限,这一步和 Field/Method 一致 ctor.setAccessible(true); // 传入实参,数量与类型都要和上面选中的构造器对齐 Object obj = ctor.newInstance("A同学", "男", 20);参数说明上有几个高频误区。类型列表里千万不要图省事用getDeclaredConstructors()遍历再按个数猜,重载一多必然选错;getDeclaredConstructor()找不到匹配就抛NoSuchMethodException,这个异常信息里只有方法签名,不会告诉你“是不是少写了个 int”,得自己比对。基本类型必须用int.class、long.class这种写法,Integer.class是另一套。实参列表允许自动装箱,所以传20给int形参没问题,反过来形参是Integer时传int也会装箱。
如果构造器内部抛了异常,newInstance不会原样抛出,而是包一层InvocationTargetException。真实的业务异常在getCause()里,这一点在排错时太重要了——只盯着外层异常看,会以为反射本身出了问题。
3.3 反射与 new 的性能账:把「较低」量化
题库里第 2 题的答案是“new 效率更高”,这个结论对,但只答到这儿面试官不会满意。往下要能说清“低在哪、低多少、怎么补”。
低在三段开销:一是查找类资源,Class.forName要走类加载器、解析常量池;二是权限检查,每次访问成员都要比对调用方和目标成员的访问修饰符;三是参数装箱与调用链,invoke是可变参数,实参会被打包成Object[],返回值再装箱。这三段叠起来,量级上比直接new差一到两个数量级——注意是量级,不是“慢几倍”,具体数字受 JIT、预热、对象复杂度影响很大,报具体数字反而容易被打脸。
我一般这么测,只做量级对比,不当基准测试用:
int N = 100_000; for (int i = 0; i < 20_000; i++) { new Person(); } // 预热,让 JIT 先编译 long t1 = System.nanoTime(); for (int i = 0; i < N; i++) { new Person(); } long t2 = System.nanoTime(); Class<?> clazz = Person.class; Constructor<?> ctor = clazz.getDeclaredConstructor(); // 关键:提到循环外 long t3 = System.nanoTime(); for (int i = 0; i < N; i++) { ctor.newInstance(); } long t4 = System.nanoTime(); System.out.println("new : " + (t2 - t1) / N + " ns/op"); System.out.println("reflect : " + (t4 - t3) / N + " ns/op");这段代码的关键不在输出,而在那个“提到循环外”的注释。把getDeclaredConstructor()写进循环里,耗时会再翻上去一大截,因为每次都在做一次签名匹配;提出来之后,剩下的差距主要来自权限检查和调用链。所以题库第 7 题给的方案里,“多次创建同一类实例时用缓存会快很多”,说的就是把Class、Constructor、Method这些元数据对象缓存起来复用,而不是每次现查。setAccessible(true)解决的则是权限检查那一段,两者配合,在大部分业务场景下反射的开销可以压到能接受的范围。真到了十万级 QPS 的热点路径上,再考虑基于 ASM 字节码生成直接调用代码的方案,那是另一个层级的优化,日常 CRUD 用不上。
顺手记一个边界:缓存元数据对象要注意类加载器生命周期。把Method静态缓存在工具类里,如果目标类来自可热部署的加载器,加载器就回收不掉,时间一长就是元空间泄漏。稳妥做法是把缓存挂在业务侧的加载器范围内,或者用弱引用兜底。
4. Field 与 Method 的读写调用:setAccessible 到底关掉了什么
反射真正干活的部分在Field和Method上。题库第 8 题列了四个核心类,第 11 题给了遍历属性的示例,但没讲清setAccessible的语义和invoke的参数约定——这两处恰恰是写代码时最容易翻车的地方。
4.1 私有成员访问的三步走
访问一个私有字段,标准动作是三步,缺一步都不行:
Class<?> clazz = Class.forName("demo.reflect.Person"); Object obj = clazz.getDeclaredConstructor().newInstance(); // 第一步:取字段。私有字段必须用 getDeclaredField,getField 只认 public Field field = clazz.getDeclaredField("name"); // 第二步:关掉本次访问的权限检查 field.setAccessible(true); // 第三步:读写 field.set(obj, "A同学"); Object value = field.get(obj); System.out.println(value);setAccessible(true)关掉的是“每次读写这个成员时,JVM 去比对调用方是否有权限”的那段检查,它不会改变成员的修饰符,也不会让别的代码突然就能访问——这个开关只作用于你手上这个Field实例发起的访问。换句话说,它是“针对这次访问的通行证”,不是“把这个字段改成 public”。理解这一点,题库第 7 题那句“破坏了封装性”才有落脚点:破坏的不是编译期的封装,而是运行时的访问控制。
get的返回值有讲究。字段是int时返回的是装箱后的Integer,字段是 null 时返回 null,所以你写int age = (Integer) field.get(obj)是有拆箱风险的,字段没赋值就是空指针。基本类型字段在没显式赋值时也有默认值,但如果目标对象本身是反射造出来的、走的是Unsafe之类的路径,就别指望默认值了。
还有一个必须记的边界:从 JDK 9 开始有了模块系统,对 JDK 内部的包做setAccessible会抛InaccessibleObjectException,提示“module java.base does not open”。解决办法不是关掉模块系统,而是启动时加--add-opens java.base/目标包=ALL-UNNAMED。这件事在升级 JDK 时集中爆发,日志里那一行提示看着像反射坏了,其实是模块没开。
4.2 Method.invoke 的参数打包与返回值拆箱
Method.invoke(Object target, Object... args)这个签名藏着两个坑:可变参数和返回值类型。
Method greet = clazz.getDeclaredMethod("greet", String.class); // 参数是类型列表 greet.setAccessible(true); // 第一个参数是调用目标,静态方法传 null Object result = greet.invoke(obj, "hello"); System.out.println((String) result); // 无参方法:直接把第二个参数省掉,别传 null Method noArg = clazz.getDeclaredMethod("toStringNoArg"); noArg.invoke(obj);签名是Object...,所以传null会被编译器当成“整个数组是 null”还是“一个 null 元素”,取决于有没有做强制类型转换。想传一个值为 null 的实参,写new Object[]{null};想表示“没有实参”,直接省略。这一处歧义是空指针的高频来源,写工具类时必须统一约定。
调用目标那个参数同样要注意:invoke的第一个参数必须能赋给声明该方法的类的类型,传错类型抛IllegalArgumentException,而不是ClassCastException,看着很不直观。静态方法传null没问题,实例方法传null就是空指针,这个错在包装静态工具方法时特别常见。
返回值统一是Object,void方法返回 null。需要区分“真的返回了 null”和“方法没有返回值”的时候,去看method.getReturnType() == void.class,别用返回值猜。
4.3 缓存元数据对象:把反射开销压下去的做法
题库提到“多次创建用缓存会快很多”,落到代码上就是把Class、Constructor、Method、Field这些查一次就够的对象存起来复用。常见的错误写法是在循环里调getDeclaredMethod,那个方法内部要做签名比对,还会复制数组,开销比调用本身大得多。
// 方法元数据缓存,key 用「类 + 方法名 + 参数类型」拼,避免重载冲突 private static final Map<String, Method> METHOD_CACHE = new ConcurrentHashMap<>(); public static Object invoke(Object target, String methodName, Class<?>[] paramTypes, Object[] args) throws Exception { Class<?> clazz = target.getClass(); String key = clazz.getName() + "#" + methodName + Arrays.toString(paramTypes); // computeIfAbsent 保证并发下只创建一次,缓存里存的是已开权限的 Method Method m = METHOD_CACHE.computeIfAbsent(key, k -> { try { Method found = clazz.getDeclaredMethod(methodName, paramTypes); found.setAccessible(true); // 权限只开一次,后续调用直接受益 return found; } catch (NoSuchMethodException e) { throw new IllegalStateException("方法不存在: " + k, e); } }); return m.invoke(target, args); }这段代码的逻辑:先用类名加方法签名拼出一个稳定的 key,computeIfAbsent保证同一个 key 在并发下只走一次查找;查找成功时顺手把setAccessible(true)也做掉,因为权限标记挂在Method实例上,缓存复用的同时把这部分开销一起省掉了。参数说明方面,paramTypes必须和声明严格一致,args按装箱后的形式传,返回值强制转成业务类型时记得判空。
真要在生产环境用,还得给这个缓存加边界:key 里的类名来自动态加载的类时,缓存会阻止加载器被回收,要么限制缓存条目数,要么换成弱引用 key。这一点没有后悔药,泄漏是慢慢涨的,通常要压测或者跑几天才看得出来。
5. 避坑与排查:反射题库里最容易写错的五个地方
反射的坑有个共同特点——编译期全绿,运行时才炸,而且异常信息经常指向反射内部而不是业务代码。下面这五条是我在实际项目和面试复盘里反复见到的,每条按“现象、原因、解决”写,遇到时对照着查。
5.1 加载一个类,结果静态代码块被提前执行了
现象:只是想在启动时扫描一下某个类的字段结构,结果日志里出现了一段本该在业务初始化阶段才执行的注册逻辑,有时候还会因为依赖没就绪直接抛异常。
原因:Class.forName(String)默认initialize=true,会完成类的初始化并执行静态代码块。题库第 5 题给的第一种写法就是这个默认版本,很多人不知道它带副作用。
解决:只想读元数据、不想触发初始化时,用三参数版本Class.forName(name, false, classLoader),或者用类名.class直接引用。如果确实需要初始化(比如依赖静态块完成注册),就明确用默认版本,并在注释里写清楚为什么必须让它初始化。
5.2 构造器明明有,却报 NoSuchMethodException
现象:目标类里清清楚楚写着public Person(int age),调用getDeclaredConstructor(Integer.class)却抛NoSuchMethodException,翻来覆去检查类名也没写错。
原因:int和Integer是两个完全不同的Class,反射匹配签名是精确匹配,不做装箱也不做向上转型。同理,String和Object、数组的String[].class和Object[].class,都不能互相顶替。
解决:形参是基本类型就写int.class,或者用对应的Integer.TYPE;不确定时先把getDeclaredConstructors()全部打印出来,把toString()里的签名和你要找的逐一对照,比猜快得多。参数是可变参数时也要按数组类型匹配。
5.3 setAccessible(true) 抛 InaccessibleObjectException
现象:本地开发环境跑得好好的反射工具,换到新版本 JDK 上就抛InaccessibleObjectException,提示某个java.base下的包没有被 open。
原因:JDK 9 引入模块系统之后,对 JDK 内部包的深度反射默认关闭。setAccessible(true)不再是万能的,它要先过模块的开放检查。
解决:优先换掉对 JDK 内部 API 的依赖,改用公开 API;确实无法替换时,在启动参数里显式--add-opens java.base/具体包名=ALL-UNNAMED。别用关掉整个模块校验的思路绕,那是把一个问题换成十个问题。写工具库的人还要注意:这类参数必须写进文档,不然使用方升级 JDK 时会一脸茫然。
5.4 invoke 传 null 参数,参数个数对不上
现象:调用一个只有一个参数的方法,实参传null,结果要么报参数个数不匹配,要么目标方法收到的不是 null 而是空指针异常。
原因:invoke(Object obj, Object... args)是可变参数,直接写invoke(obj, null)时,编译器无法判断你是想传“一个 null 元素”还是“整个数组为 null”,实际行为取决于重载解析结果。
解决:约定两种写法——没有实参就省略第二个参数;要传一个 null 实参就显式写成new Object[]{null}。团队里最好把这条写进工具类的注释,比事后排查省事。
5.5 明明业务抛了异常,堆栈里只有 InvocationTargetException
现象:反射调用某个方法失败,异常堆栈的顶层是InvocationTargetException,往下翻全是反射框架的调用链,看不到业务代码到底哪一行出错。
原因:反射调用把目标方法抛出的任何异常都包了一层InvocationTargetException,原始异常保存在getCause()里。只看外层异常,等于只看到了包装盒。
解决:所有反射调用点统一在 catch 里判断类型,如果是InvocationTargetException就取出getCause()再抛或者再记日志。写通用工具方法时,最好把getCause()解出来重新包成自己的业务异常,把原始堆栈保留住,不然线上排查只能靠猜。
6. 把 11 题变成可验证的自测脚本:三个进阶技巧
题库看一遍容易忘,我习惯把它变成能跑的东西。做法是写一个二十行的通用字段打印器,用反射把任意对象的字段名和值打出来,顺便把题库里的Field、setAccessible、缓存三个点全串一遍:
// 调试用:打印对象所有字段(跳过静态字段和合成字段) public static String dump(Object obj) throws IllegalAccessException { Class<?> clazz = obj.getClass(); StringBuilder sb = new StringBuilder(clazz.getSimpleName()).append("{"); for (Field f : clazz.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers()) || f.isSynthetic()) continue; // 跳过静态与编译器合成字段 f.setAccessible(true); // 私有字段也能读 sb.append(f.getName()).append("=").append(f.get(obj)).append(", "); } return sb.append("}").toString(); }把它挂到日志里,任何对象丢进去都能出结构化内容,比手写一堆toString靠谱。写的过程中你会自然碰到三个进阶点,正好对应题库第 7 题给的提速方案。第一,setAccessible(true)要能省则省,把它和字段对象一起缓存起来,别在循环里反复设置。第二,getDeclaredFields()每次调用都会复制一份数组,高频路径上应该缓存字段列表,而不是每次现取。第三,泛型继承会带来桥接方法(bridge method),getDeclaredMethods()里会多出isSynthetic()为 true 的条目,遍历时记得过滤,否则统计方法数、做方法映射时会莫名其妙多出一截。
复习时可以用一张对照表把 11 题压到 API 层面,答不出来就回去翻对应章节:
| 题目考点 | 落到哪个 API | 验证方式 |
|---|---|---|
| 除 new 外的创建方式 | Class.newInstance、Constructor.newInstance | 两种写法各跑一次,观察异常差异 |
| 反射与 new 的效率 | System.nanoTime循环对比 | 把构造器查找移到循环外再测一次 |
| 反射的作用与场景 | Class元数据读取 | 打印任意类的成员列表 |
| 获取 Class 的四种方式 | forName、.class、getClass、TYPE | 用==比较,找出哪些不相等 |
| 四大反射类职责 | Class/Field/Method/Constructor | 各写一行获取代码 |
| 反射优缺点与提速 | setAccessible、缓存 | 去掉缓存跑一遍,看耗时变化 |
| 使用步骤 | 取 Class → 读结构 → 调用 | 串成一个 main 方法 |
| 动态创建实例 | Constructor选签名 | 换成int.class与Integer.class各试一次 |
最后说个习惯。早年我图快,反射代码从不缓存元数据,本地测都没事,压测一上来 CPU 全耗在getDeclaredMethod上,翻日志翻了半天才定位到。从那以后我每次写反射,都强制走一遍“缓存了没有、setAccessible开了没有、异常有没有解包”这三问,问完再提交。这套东西看着笨,但省下来的排查时间是真的。希望帮到你。
本文还有配套的精品资源,点击获取