
简介针对Java面试中高频的反射机制专题这份PDF整理汇总了11道经典面试题及解答适合正在准备Java后端岗位面试、希望系统梳理反射知识点的开发者阅读。内容涵盖new与反射创建对象的效率差异、反射在JDBC/Hibernate/Spring等框架中的应用、获取Class对象的4种方式、Field/Method/Constructor等反射类用途以及反射优缺点与setAccessible(true)、缓存、字节码工具等优化方案并包含用Class和Constructor的newInstance方法创建实例的步骤示例。每个题目均给出简明答案部分辅以关键代码便于理解记忆和考前快速回顾。资源为独立PDF文件共1个文件压缩包约569KB已有228人学习适合作为Java面试突击清单之一。1. Java反射是什么给类照一次“X光”拿到它的完整体检报告Java反射Reflection可能是Java里最容易被当成“玄学”的特性之一。初学者常在框架源码里看到它却又说不清它到底做了什么。一句话先给个定义反射就是程序在运行时动态地获取某个类的所有信息属性、方法、构造器并操作它们的能力。打个比方new一个对象像是按图纸直接造零件而反射像是先给类拍一整套X光片拿到体检报告后再根据报告现场决定怎么操作它。这份“体检报告”就是Class对象。搞懂反射不只是为了应付面试题更是理解框架、连接数据库、写工具类的前提。这篇就把它的原理、用途和容易混淆的坑一次讲清楚。2. 为什么需要反射总有些类编译期根本不知道它的名字2.1 new 的局限代码写死 vs 运行期才知道平时写代码创建一个对象最常见的方式就是 new。new 的写法很直接但它有一个隐含的约束编译的时候类名必须是确定的。UserService userService new UserService();这一行代码在编译阶段编译器就要能找到 UserService 这个类。如果类名写错了编译直接报错。这在大多数业务代码里没问题因为你需要哪个类通常是早就定好的。但有一类场景 new 就无能为力了你写代码的时候根本不知道运行时具体要用哪个类类名字符串要到程序跑起来那一瞬间才出现。典型例子是加载数据库驱动。早期 JDBC 连接数据库时常见的写法是这样Class.forName(com.mysql.cj.jdbc.Driver);这段代码里的驱动类名是一个字符串存在配置文件里运行时才拼接出来。如果只靠 new你必须在代码里硬编码某一个数据库的驱动类换数据库就得改源码重新编译。而用反射只要传入不同的类名就能动态加载不同的驱动。这背后依赖的正式反射机制的第一个能力给定一个类名字符串在运行时找到并加载这个类。2.2 获取 Class 对象的四种方式反射的“入口”就靠它要用反射第一步永远是拿到某个类的 Class 对象。什么是 Class 对象通俗地说它记录了这个类本身的全部“元信息”类名叫什么、有哪些字段、有哪些方法、构造器长什么样。拿到 Class 对象就是拿到了反射操作的总入口。获取 Class 对象有四种方式区别和使用场景见下表获取方式写法适用场景注意点Class.forName(全限定名)Class.forName(com.example.User)类名字符串来自配置文件或用户输入运行时才知道是哪个类需要手写完整包名类名拼错会抱运行时异常类名.classUser.class编译期就知道要操作哪个类最直接不会触发类的初始化效率高写错编译期就报错对象名.getClass()user.getClass()已经有一个实例想知道它的实际类型只能在有对象实例后用多用于判断对象的真实运行时类型基本类型包装类的 TYPEInteger.TYPE获取 int、double 这类基本类型对应的 Class只能用来表示基本类型不能用它获取实例四种方式拿到的是同一个东西也就是同一个 Class 对象。同一个类在同一个类加载器下它的 Class 对象只会有一个。正因为如此比较两个对象是不是同一个类型时可以借助 Class 对象来判断。// 判断两个对象是不是同一个类型 boolean isSameType obj1.getClass() obj2.getClass();2.3 Class 对象里到底装了什么一个类的“元数据快照”Class 对象不是类的实例而是类的“户口本”。它装的是关于这个类本身的描述这类信息在计算机里叫元数据metadata。打个比方一个 User 类可以创建出成千上万个 User 实例每个实例有自己的姓名、年龄而 User 的 Class 对象只有一份它记录的是“User 这个类有哪些字段、哪些方法、构造器是什么样的”。反射能拿到哪些信息完全取决于 Class 对象里存了什么。一个典型的 Class 对象里包括类名、包名、父类、实现的接口、声明的字段Field、构造器Constructor、方法Method、注解等信息。后面要讲的 Field、Method、Constructor 这三个类本质上都是对 Class 对象中某类元数据的封装。值得注意的一点Class 对象对应的是类本身它没有“当前属性值”的概念。你不能问 Class 对象“这个 User 的年龄是多少”因为“年龄”是实例的私有数据Class 里只存着“有没有 age 这个字段、它是 int 类型”这样的结构信息。想要读某个具体对象的字段值必须同时持有 Class 对象和该实例后面的章节会展开讲。3. 反射的三大操作创建对象、调用方法、读写字段3.1 创建对象无参和有参构造器两条路反射最常用的操作之一是在运行时创建对象。拿到 Class 对象后有两种常见的创建方式。// 获取 Class 对象 Class? clazz Class.forName(com.example.User); // 方式一调用无参构造器 User user1 (User) clazz.newInstance();方式一代码量最少但有一个硬性限制目标类必须有一个公开的无参构造器。如果类里只有有参构造器newInstance()会直接抛异常。而且在新版 JDK 中这个方法已被标记为过时官方更推荐先获取 Construct 对象再创建实例。// 方式二获取指定构造器再创建对象 ConstructorUser constructor clazz.getDeclaredConstructor(String.class, String.class, int.class); User user2 constructor.newInstance(小李, 男, 20);方式二看起来代码多但它解决了一个关键问题可以通过参数类型精确定位某个构造器然后传入对应实参完成创建。注意getDeclaredConstructor的参数列表是 Class 类型的数组必须和构造器声明的参数类型严格匹配。这里踩坑的人很多构造器接收的是Integer类型你传int.class运行时会匹配不上抛NoSuchMethodException。3.2 调用方法拿到 Method再点“播放键”反射不只能创建对象还能调用任意方法包括私有方法。核心思路是通过 Class 对象找到目标方法的 Method 对象然后调用 Method 的invoke方法。Class? clazz Class.forName(com.example.User); Object user clazz.getDeclaredConstructor().newInstance(); // 拿到名为 sayHello、参数是 String 的方法 Method method clazz.getDeclaredMethod(sayHello, String.class); // 关闭访问检查允许调用私有方法 method.setAccessible(true); // invoke 的第一个参数是“谁来执行这个方法”后面是方法实参 Object result method.invoke(user, 反射);这段代码里有个细节值得记住invoke的第一参数必须是该类的实例因为实例方法要有一个“调用者”。如果调用的是静态方法这个位置传null即可。getDeclaredMethod的第二个参数是可变长表示方法签名里的参数类型列表顺序不能乱。方法有重载时正是靠这个参数列表来区分到底找哪一个。3.3 读写字段Field 能改 privatesetAccessible 是关键反射还能直接读和改对象的字段值包括那些用private修饰的私有字段。这在写通用框架时特别有用比如通用的对象序列化、表单对象赋值的工具类都能靠 Field 操作字段。Class? clazz Class.forName(com.example.User); Object user clazz.getDeclaredConstructor().newInstance(); // 获取名为 age 的字段 Field ageField clazz.getDeclaredField(age); // 因为 age 是 private默认没有访问权限必须调用 setAccessible(true) ageField.setAccessible(true); // 读取字段值get 返回 Object需要强转 int age (int) ageField.get(user); // 修改字段值 ageField.set(user, 30);setAccessible(true)是这里的关键。它做的是关闭 Java 语言的访问权限检查。注意这个方法不是 Field 独有的Method 和 Constructor 也都继承自公共的AccessibleObject父类所以说“关闭安全检查”这件事在访问字段、调用方法、创建对象时是通用的。使用它等于告诉虚拟器我知道这个成员是私有的但我仍然要操作它请你跳过权限校验。3.4 四个核心反射类的分工一张表看懂讲了三个操作背后的主角始终是四个类。它们的分工非常清晰反射类名字含义对应操作典型方法Class类的元数据快照反射的总入口获取类的一切信息forName、newInstance、getDeclaredMethodsField字段描述读取和修改字段值get、set、setAccessibleMethod方法描述调用目标方法invoke、setAccessibleConstructor构造器描述按指定参数创建实例newInstance一句话记住它们的关系Class 是蓝图Field、Method、Constructor 是蓝图上的三块局部图。拿到 Class 以后想要哪块局部图就调用对应的方法去取。下面这段代码展示了一次取全的过程Class? clazz Class.forName(com.example.User); // 获取声明的所有方法包括私有返回一个数组 Method[] methods clazz.getDeclaredMethods(); for (Method m : methods) { System.out.println(m.toString()); } // 获取声明的所有字段 Field[] fields clazz.getDeclaredFields(); for (Field f : fields) { System.out.println(f.toString()); } // 获取声明的所有构造器 Constructor?[] constructors clazz.getDeclaredConstructors(); for (Constructor? c : constructors) { System.out.println(c.toString()); }4. 反射的易混概念和常见误区5个坑踩过的人不少4.1 误区一反射创建对象等于 new效率完全不是一回事有人觉得反射就是“另一种写法的 new”没太大区别。其实两者的区别在深入使用时特别明显。为什么混从效果上看clazz.newInstance()和new User()都返回了一个 User 对象表面结果一样。怎么区分new 在编译期就能确定类型字节码里直接就有创建指令JVM 执行起来路径最短。反射则是在运行时先要通过字符串查找和加载类再解析类结构每一步都要做额外的临时对象和方法调用。整个过程繁琐不少所以性能天然比 new 低。官方资料和大量血泪经验都表明热点路径上频繁用反射性能会明显退化。后面会说怎么做优化但即使优化过也不如直接 new 快。4.2 误区二getMethods 能拿到私有方法别想太多有人顺手用getMethods()来拿类的全部方法然后发现私有方法不见了以为是反射失效了。为什么混两个名字看起来很像都是“取方法”容易混用。怎么区分getMethods()返回的是类的所有 public 方法包括从父类继承来的 public 方法getDeclaredMethods()返回的是当前这个类自己声明的所有方法不管 public 还是 private但不会包含继承来的方法。字段同理getFields()只能拿 public 字段而getDeclaredFields()能拿本类声明的私有字段。想拿到本类私有方法必须用带Declared的那个版本。方法名包含本类私有成员包含继承来的 public 成员适用场景getMethods() / getFields()否是只关心对外暴露的公共成员getDeclaredMethods() / getDeclaredFields()是否想全量查看本类的所有成员包括私有4.3 误区三Class 对象就是实例两个完全不同的东西Class 对象和类的实例对象是两种东西但初学者很容易搞混。为什么混user.getClass()这个方法是从对象上调用的返回的又像是“和 user 有关的东西”容易把它和 user 本身搅在一起。怎么区分对象是类的“作品”有实际的数据比如 user.getName() 能返回“小明”。Class 对象是类的“说明书”它回答的是“User 类有什么成员”而不是“这个 User 叫什么名字”。还有个重要的验证方式同一个类只会有一个 Class 对象但可以有很多个实例。user1.getClass() user2.getClass()返回 true但user1 user2几乎永远是 false除非两者是同一个对象。4.4 误区四forName 和 类名.class 没区别触发时机不一样获取 Class 对象有四种方式很多人觉得殊途同归随便用就行。为什么混结果看起来都一样——都得到了同一个 Class 对象于是被认为完全等价。怎么区分Class.forName(完整类名)传入的是字符串它会在运行时主动查找并加载类加载过程中执行类的静态初始化块也就是 static {} 里的代码。而类名.class是编译期就能确定的一个字面量它拿到 Class 对象时不会主动触发类的初始化逻辑。换句话说一个“动手加载”一个“被动引用”。这就是为什么加载数据库驱动时用 forName 更合适有些驱动的静态注册逻辑写在静态代码块里必须靠 forName 触发而反射某些类只是为了取方法用类名.class会更轻量。4.5 误区五反射“不安全” 漏洞是封装边界被绕过了反射可以获取私有字段和私有方法于是有人担心它是不是一个安全漏洞。为什么混字面上看“能访问私有成员”和“不安全”确实像一回事。怎么区分二者层次不同。反射的“不安全”准确说是破坏了封装性——类的作者用 private 声明“这个成员不对外公开”反射可以强行绕过这些限制访问它。但这本身不是一种系统入侵行为而是 JVM 在运行时提供的能力。真正该担心的混淆是如果你把反射的入口暴露给不可信用户对方确实可能利用它做更多操作。所以安全的做法是内部工具类可以用反射提升灵活性但对外部输入不做校验就拼进类名字符串冲动调用才是危险点。封装性和灵活性之间的权衡是每个反射使用者要自己把握的。5. 反射的性能与安全慢在哪怎么补救破坏封装怎么理解5.1 反射为什么慢一次调用背后有三层检查都知道反射慢但具体慢在哪看一个方法调用过程就知道Method method clazz.getDeclaredMethod(sayHello, String.class); method.setAccessible(true); method.invoke(user, 反射);第一层慢在查找getDeclaredMethod需要扫描类的方法列表按名字和参数类型做匹配这是一个遍历比较的过程。第二层慢在权限如果方法不是 publicinvoke 还会做访问检查。第三层慢在包装invoke 接收的是 Object 类型的参数和返回值所以基本类型会被自动装箱成包装类比如 int 变成 Integer多出对象的创建和拆装开销。这三层叠在一起反射调用的耗时自然比直接方法调用高出不少。5.2 提速三板斧setAccessible、缓存、字节码生成反射慢不代表不能用关键是有没有做优化。最常用的是三招。第一招显式调用setAccessible(true)。原理上面说过它跳过访问权限检查省掉那一道校验。第二招把反射找到的 Method、Field 对象缓存下来。因为“查找方法”这一步最费时间而同一个方法在一个类里只会有一份元数据拿到一次后存到 Map 里下次直接复用。// 用一个 Map 缓存 Method 对象避免每次反射都重新查找 private static final MapString, Method METHOD_CACHE new ConcurrentHashMap(); public static Object invokeCached(Object target, String methodName, Object... args) throws Exception { String key target.getClass().getName() # methodName; Method method METHOD_CACHE.get(key); if (method null) { method target.getClass().getDeclaredMethod(methodName); method.setAccessible(true); METHOD_CACHE.put(key, method); } return method.invoke(target, args); }缓存的效果立竿见影第一次反射时做扫描和解析之后每次调用直接使用已有的 Method 句柄省掉了最贵的查找过程。第三招更高级引入字节码生成工具。这类工具直接生成字节码来模拟方法调用相当于把反射的运行时分流变成了编译期生成的直接调用速度比普通反射快一个量级。很多高性能框架底层就是这么做的适合真正需要极致热点的场景。5.3 反射与“破坏封装”是绕过不是攻击反射能访问私有成员这一点常常让人心里没底。需要理清一个概念封装性是代码设计层面的约定不是系统结构层面的防御。private 修饰符的作用是让调用者在 IDE 里、在编译期一眼看出“这个成员不该从外面碰”。反射的setAccessible(true)可以在运行时越过这层约定但它遵守的是 JVM 的运行时权限规则。反射没有把数据加密层破坏掉也不是通过网络入侵代码内存它绕开的只是语言层面的可访问性约束。所以合理的态度是这样的日常业务代码里随手给私有方法加一个反射调用是不推荐的做法因为代码可读性被操作了维护的人看完会一头雾水。但在框架工具类里用反射去操作私有构造器、执行 setAccessible反而是常见且合理的技术选择。判断标准从来不是“能不能”而是“该不该、有没有更清晰的办法”。6. 什么时候该用反射什么时候别碰一条简单的判断线6.1 典型该用反射的场景框架类代码是最典型的使用场景。ORM 框架需要把数据库查出来的行映射成对象往往在运行时才知道实体类有哪些字段用 Field 批量赋值就非常自然。注解处理器也依赖反射在运行时扫描类上的注解再根据注解值去做对应的逻辑。动态代理、依赖注入容器、配置文件驱动的扩展点加载基本都是靠反射实现的。总结一条判断线代码里是否“在写代码时无法确定类型只能靠运行时的字符串或配置来决定操作目标”。是就该用反射。6.2 典型不该用反射的场景下面几种情况大概率是误用了反射。每天执行上千次的业务循环里反射调用带来的额外开销会被放大如果只是为了省几行代码不值得。已有清晰的接口和继承关系可以表达逻辑时不要用反射绕开接口。需要追求代码可读性的核心业务逻辑也尽量避免反射因为反射调用在 IDE 里无法跳转代码搜索也不友好后续维护成本高。记住口诀能用接口表达的不用反射能用静态类型表达的不做动态发现。6.3 一个具体例子迷你通用方法调用工具假设你在写一个框架接收外部配置的方法名调用任意对象的方法这个时候反射就非常合适。public static Object callByName(Object target, String methodName, Object... args) throws Exception { Class? clazz target.getClass(); // 根据参数运行时类型拼出方法的参数类型列表 Class?[] typeList new Class?[args.length]; for (int i 0; i args.length; i) { typeList[i] args[i].getClass(); } Method method clazz.getDeclaredMethod(methodName, typeList); method.setAccessible(true); return method.invoke(target, args); }如果外部调用者的方法签名固定、参数类型明确这个工具完全够用。但如果参数里有基本类型或接口实现类args[i].getClass()的类型可能和声明的参数类型不完全一致那时需要额外的类型匹配逻辑。这也是反射最常见的翻车点类型对不上运行时才发现错误。同样的功能如果改成在编译期能确定对象的类型和方法名优先直接调用。这个迷你工具只在真正“运行时才知道调用哪个方法”的场景里才有意义。从那以后我每次在项目里写反射相关工具类都会强制自己先走一遍那三问这真的需要运行时才知道类名吗这个调用路径热吗有没有不用反射就能表达的写法三问都通过才敢动手。希望这次的讲解能帮到你。本文还有配套的精品资源点击获取