简介面向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(); // 第一步取字段。私有字段必须用 getDeclaredFieldgetField 只认 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就是空指针这个错在包装静态工具方法时特别常见。返回值统一是Objectvoid方法返回 null。需要区分“真的返回了 null”和“方法没有返回值”的时候去看method.getReturnType() void.class别用返回值猜。4.3 缓存元数据对象把反射开销压下去的做法题库提到“多次创建用缓存会快很多”落到代码上就是把Class、Constructor、Method、Field这些查一次就够的对象存起来复用。常见的错误写法是在循环里调getDeclaredMethod那个方法内部要做签名比对还会复制数组开销比调用本身大得多。// 方法元数据缓存key 用「类 方法名 参数类型」拼避免重载冲突 private static final MapString, 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); }这段代码的逻辑先用类名加方法签名拼出一个稳定的 keycomputeIfAbsent保证同一个 key 在并发下只走一次查找查找成功时顺手把setAccessible(true)也做掉因为权限标记挂在Method实例上缓存复用的同时把这部分开销一起省掉了。参数说明方面paramTypes必须和声明严格一致args按装箱后的形式传返回值强制转成业务类型时记得判空。真要在生产环境用还得给这个缓存加边界key 里的类名来自动态加载的类时缓存会阻止加载器被回收要么限制缓存条目数要么换成弱引用 key。这一点没有后悔药泄漏是慢慢涨的通常要压测或者跑几天才看得出来。5. 避坑与排查反射题库里最容易写错的五个地方反射的坑有个共同特点——编译期全绿运行时才炸而且异常信息经常指向反射内部而不是业务代码。下面这五条是我在实际项目和面试复盘里反复见到的每条按“现象、原因、解决”写遇到时对照着查。5.1 加载一个类结果静态代码块被提前执行了现象只是想在启动时扫描一下某个类的字段结构结果日志里出现了一段本该在业务初始化阶段才执行的注册逻辑有时候还会因为依赖没就绪直接抛异常。原因Class.forName(String)默认initializetrue会完成类的初始化并执行静态代码块。题库第 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 methodgetDeclaredMethods()里会多出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开了没有、异常有没有解包”这三问问完再提交。这套东西看着笨但省下来的排查时间是真的。希望帮到你。本文还有配套的精品资源点击获取