第1课:反射入门 --- Class对象与构造方法
一、什么是反射?为什么要学?
反射 :在程序运行时,动态获取类的信息(构造方法、成员方法、成员字段等),并动态调用和操作它们的能力。
也就是说在程序运行的时候,我可以通过反射这门技术来获取到这个类中的所有信息。
这些所有的信息包括:类的构造器、类的成员变量、类的所有方法。
反射面前,人人平等------无论是什么修饰符(public/private/protected/default),我们都能获取到。
��面试考点:反射的核心价值
面试官问 :Java反射是什么?有什么实际应用场景?
标准话术 :
-
反射是 Java 在运行时动态获取类信息并操作的能力
-
框架层面 :MyBatis 的 resultMap 自动映射、Spring 的 Bean 实例化与依赖注入、SpringMVC 的参数绑定,都依赖反射实现
-
工具层面 :我们可以用反射封装通用工具类,如 BeanUtils 对象拷贝、通用 DAO 基类
-
配置驱动 :通过配置文件 + 反射实现对象创建,解耦硬编码
学了能在项目里做什么?
|--------|-----------------------------------------|
| 场景 | 说明 |
| 框架底层 | MyBatis、Spring 等框架都靠反射运行 |
| 通用 DAO | 写 BaseDao 时必须用反射把 ResultSet 映射成 Java 对象 |
| 配置驱动 | 通过配置文件动态创建对象,不用改代码就能切换实现类 |
二、获取Class对象的三种方式
Class 对象是反射的入口,JVM 为每个类只生成一个 Class 对象。获取它有三种方式:
// java
// ========== 方式1:通过类名.class(最常用,编译期就确定)==========
Class clazz1 = User.class;
// ========== 方式2:通过对象的getClass()方法(已有对象时用)==========
User user = new User();
Class clazz2 = user.getClass();
// ========== 方式3:通过Class.forName()全限定名(配置文件里写类名时用)==========
Class clazz3 = Class.forName("com.example.entity.User");
��三种方式怎么选?
|--------------|---------------------|------------|
| 场景 | 用哪种 | 原因 |
| 编译时就知道类名 | 类名.class | 最简洁,编译期就确定 |
| 已经有对象实例 | 对象.getClass() | 已有对象时的标准做法 |
| 运行时从配置文件读取类名 | Class.forName(全限定名) | 动态获取,灵活 |
��面试考点:Class.forName() vs 类名.class
面试官问 :Class.forName() 和 类名.class 有什么区别?
标准话术 :
-
核心区别 :Class.forName() 会触发类的静态代码块执行 (类加载),而 类名.class 不会
-
JDBC场景 :JDBC 老版本加载驱动用的就是 Class.forName("com.mysql.cj.jdbc.Driver"),它会触发 Driver 类中的静态注册代码完成驱动注册
-
Spring框架 :Spring 的 ClassPathXmlApplicationContext 底层也用到了 forName 来加载 Bean 定义
三、通过反射获取构造方法并创建对象
3.1 基础语法
// java
import java.lang.reflect.Constructor;
/**
* 用户实体类
*/
public class User {
private String name;
private int age;
// 无参构造
public User() {
}
// 有参构造
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
/**
* 反射创建对象演示
*/
public class ReflectDemo {
public static void main(String\[\] args) throws Exception {
// 获取Class对象
Class clazz = Class.forName("com.example.entity.User");
// 方式1:通过无参构造创建对象(最常用)
// 前提:类必须有public无参构造方法
User user1 = (User) clazz.newInstance();
System.out.println("无参创建:" + user1);
// 方式2:通过有参构造创建对象
Constructor constructor = clazz.getConstructor(String.class, int.class);
User user2 = (User) constructor.newInstance("张三", 25);
System.out.println("有参创建:" + user2);
}
}
3.2 获取构造方法的API总结
|----------------------------------|--------------------------------|
| 方法 | 说明 |
| getConstructor(Class...) | 获取public 指定参数类型的构造方法 |
| getConstructors() | 获取所有public 构造方法 |
| getDeclaredConstructor(Class...) | 获取所有 指定参数类型的构造方法(含private) |
| getDeclaredConstructors() | 获取所有 构造方法(含private) |
| newInstance(Object...) | 调用构造方法创建对象实例 |
��面试考点:newInstance() 的区别
面试官问 :Class.newInstance() 和 Constructor.newInstance() 有什么区别?
标准话术 :
-
Class.newInstance() 是 JDK 1.9 之前的方式,只能调用无参构造 ,且构造函数抛出异常会直接透出
-
Constructor.newInstance() 是更推荐的方式,可以调用任意构造方法 ,异常会被包装成 InvocationTargetException
-
实际项目 :推荐使用 getDeclaredConstructor().newInstance() 组合,更灵活
四、项目实战:通过配置动态创建DAO实例
4.1 场景说明
在电商项目中,我们经常需要根据配置决定用哪个 DAO 实现。传统方式是硬编码 new UserDaoImpl(),如果要切换实现类就需要改代码。用反射 + 配置文件可以优雅解决这个问题。
4.2 配置文件
// properties
dao.properties
键值对格式:key=类的全限定名
userDao=com.example.dao.impl.UserDaoImpl
productDao=com.example.dao.impl.ProductDaoImpl
orderDao=com.example.dao.impl.OrderDaoImpl
4.3 工厂类实现
// java
import java.io.InputStream;
import java.util.Properties;
/**
* DAO工厂类 - 通过反射 + 配置文件动态创建DAO实例
*
* 优势:切换实现类只需修改配置文件,无需改动Java代码
*/
public class DaoFactory {
private static final Properties props = new Properties();
/**
* 静态代码块:类加载时执行,只执行一次
* 读取classpath下的配置文件
*/
static {
try {
InputStream is = DaoFactory.class.getClassLoader()
.getResourceAsStream("dao.properties");
if (is == null) {
throw new RuntimeException("找不到配置文件 dao.properties");
}
props.load(is);
is.close();
} catch (Exception e) {
throw new RuntimeException("加载配置文件失败", e);
}
}
/**
* 根据key获取DAO实例
* @param key 配置文件中的键
* @return DAO实例对象
*/
@SuppressWarnings("unchecked")
public static <T> T getDao(String key) {
try {
// 1. 从配置读取类的全限定名
String className = props.getProperty(key);
if (className == null) {
throw new RuntimeException("配置项不存在: " + key);
}
// 2. 通过反射获取Class对象
Class clazz = Class.forName(className);
// 3. 调用无参构造创建实例
return (T) clazz.newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("类不存在: " + key, e);
} catch (InstantiationException e) {
throw new RuntimeException("无法实例化: " + key, e);
} catch (Exception e) {
throw new RuntimeException("创建DAO实例失败: " + key, e);
}
}
}
/**
* 测试类
*/
public class DaoFactoryTest {
public static void main(String\[\] args) {
// 不需要import具体实现类,通过配置文件动态创建
// 切换实现:只需修改dao.properties中的类名即可
Object userDao = DaoFactory.getDao("userDao");
Object productDao = DaoFactory.getDao("productDao");
Object orderDao = DaoFactory.getDao("orderDao");
System.out.println("userDao: " + userDao.getClass().getName());
System.out.println("productDao: " + productDao.getClass().getName());
System.out.println("orderDao: " + orderDao.getClass().getName());
}
}
4.4 运行结果
userDao: com.example.dao.impl.UserDaoImpl
productDao: com.example.dao.impl.ProductDaoImpl
orderDao: com.example.dao.impl.OrderDaoImpl
4.5 业务价值
|-----------------------|------------------------------|
| 传统方式 | 配置驱动方式 |
| 硬编码 new UserDaoImpl() | DaoFactory.getDao("userDao") |
| 切换实现需改Java代码 | 切换实现只需改配置文件 |
| 代码耦合严重 | 配置与代码解耦 |
| 测试困难 | 易于替换为Mock实现 |
�� 课堂练习(第1课)
练习1 :编写代码,通过反射获取 String 类的无参构造方法并创建实例。
练习2 :修改 DaoFactory,增加单例模式的支持,确保同一个 key 返回的是同一个实例。
练习3 :使用反射调用 Integer 类的有参构造方法,创建值为 100 的 Integer 对象。
第2课:反射操作方法与字段
一、获取和调用方法
1.1 基础语法
// java
import java.lang.reflect.Method;
/**
* 用户实体类 - 演示反射调用方法
*/
public class User {
private String name;
public User() {}
public User(String name) {
this.name = name;
}
// ========== 业务方法 ==========
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 私有方法(项目内部使用,不对外暴露)
private String formatName() {
return "" + name + "";
}
// 重载方法演示
public void setName(String name, boolean uppercase) {
this.name = uppercase ? name.toUpperCase() : name;
}
}
/**
* 反射调用方法演示
*/
public class MethodDemo {
public static void main(String\[\] args) throws Exception {
User user = new User("张三");
Class clazz = user.getClass();
// ========== 调用public无参方法:getName() ==========
Method getName = clazz.getMethod("getName");
String name = (String) getName.invoke(user);
System.out.println("getName结果: " + name); // 张三
// ========== 调用public有参方法:setName(String) ==========
Method setName = clazz.getMethod("setName", String.class);
setName.invoke(user, "李四");
System.out.println("调用setName后: " + user.getName()); // 李四
// ========== 调用重载方法:setName(String, boolean) ==========
Method setNameWithFlag = clazz.getMethod("setName", String.class, boolean.class);
setNameWithFlag.invoke(user, "王五", true);
System.out.println("大写设置后: " + user.getName()); // 王五
// ========== 调用private方法:formatName() ==========
// 步骤1:获取私有方法(用getDeclaredMethod,不是getMethod)
Method formatName = clazz.getDeclaredMethod("formatName");
// 步骤2:设置访问权限,暴力访问private方法
formatName.setAccessible(true);
// 步骤3:调用方法
String formatted = (String) formatName.invoke(user);
System.out.println("formatName结果: " + formatted); // 王五
}
}
1.2 getMethod vs getDeclaredMethod 对比
|--------------------------|---------------------------------|
| 方法 | 能获取到的范围 |
| getMethod("方法名") | 所有 public 方法(含继承的) |
| getDeclaredMethod("方法名") | 本类声明的 所有 方法(含 private,不含继承) |
��面试考点:setAccessible(true) 的作用
面试官问 :调用私有方法时为什么要用 setAccessible(true)?
标准话术 :
-
Java 的访问权限修饰符(public/protected/private)是编译期检查,运行时可以通过反射绕过
-
setAccessible(true) 关闭了安全检查,允许访问 private 成员
-
使用场景 :框架底层(如 Jackson 反序列化、MyBatis 映射)经常需要访问 private 字段/方法
-
注意事项 :生产环境中谨慎使用,可能破坏封装性
二、获取和操作字段
2.1 基础语法
// java
import java.lang.reflect.Field;
/**
* 商品实体类 - 演示反射操作字段
*/
public class Product {
private int id;
private String productName;
private double price;
public Product() {}
public Product(int id, String productName, double price) {
this.id = id;
this.productName = productName;
this.price = price;
}
@Override
public String toString() {
return "Product{id=" + id + ", name='" + productName + "', price=" + price + "}";
}
}
/**
* 反射操作字段演示
*/
public class FieldDemo {
public static void main(String\[\] args) throws Exception {
Product product = new Product(1, "iPhone", 5999.0);
Class clazz = product.getClass();
// ========== 获取所有声明字段(含private)==========
Field\[\] fields = clazz.getDeclaredFields();
System.out.println("=== Product 所有字段 ===");
for (Field f : fields) {
f.setAccessible(true); // 必须设置才能访问private字段
System.out.println(f.getName() + " = " + f.get(product));
}
// ========== 获取单个字段 ==========
Field priceField = clazz.getDeclaredField("price");
priceField.setAccessible(true);
// 读取字段值
double oldPrice = priceField.getDouble(product);
System.out.println("原价格: " + oldPrice); // 5999.0
// 修改字段值
priceField.setDouble(product, 4999.0);
System.out.println("修改后: " + product); // price变成了4999.0
}
}
2.2 Field 的常用方法
|-------------------------------------|-----------------|
| 方法 | 说明 |
| getName() | 获取字段名 |
| getType() | 获取字段类型(Class对象) |
| get(Object obj) | 获取字段值 |
| set(Object obj, Object value) | 设置字段值 |
| getInt(Object obj) | 获取int类型字段值 |
| setInt(Object obj, int value) | 设置int类型字段值 |
| getDouble(Object obj) | 获取double类型字段值 |
| setDouble(Object obj, double value) | 设置double类型字段值 |
��面试考点:getField vs getDeclaredField
面试官问 :getField() 和 getDeclaredField() 有什么区别?
标准话术 :
-
getField() 只能获取 public 字段,包括父类继承的 public 字段
-
getDeclaredField() 可以获取 本类声明的 所有字段,包括 private/protected/default,但不包括继承的字段
-
实际项目 :大多数实体类的字段都是 private,通常用 getDeclaredField() 配合 setAccessible(true) 使用
三、项目实战:通用对象属性拷贝工具
3.1 场景说明
在电商项目中,我们经常需要做对象之间的属性拷贝:
• DTO → Entity :前端传过来的数据对象转实体
• Entity → VO :数据库查出的实体转视图对象
• VO → JSON :视图对象序列化成JSON返回前端
传统方式需要手写大量 getter/setter,代码繁琐且容易出错。用反射封装一个通用工具,一行代码搞定!
3.2 工具类实现
// java
import java.lang.reflect.Field;
/**
* 通用对象属性拷贝工具
*
* 功能:将source中同名字段的值拷贝到target
* 场景:DTO转Entity、Entity转VO、VO转JSON等
*/
public class BeanUtils {
/**
* 将源对象的属性拷贝到目标对象(同名字段)
*
* @param source 源对象(数据来源)
* @param target 目标对象(数据写入目标)
* @throws IllegalArgumentException 参数为空
*/
public static void copyProperties(Object source, Object target) {
if (source == null || target == null) {
throw new IllegalArgumentException("source和target不能为空");
}
Class sourceClass = source.getClass();
Class targetClass = target.getClass();
// 获取目标类的所有声明字段
Field\[\] targetFields = targetClass.getDeclaredFields();
for (Field targetField : targetFields) {
try {
// 获取源类中同名字段
Field sourceField = sourceClass.getDeclaredField(targetField.getName());
// 设置访问权限(处理private字段)
sourceField.setAccessible(true);
targetField.setAccessible(true);
// 获取源字段值
Object value = sourceField.get(source);
// 拷贝到目标字段
targetField.set(target, value);
} catch (NoSuchFieldException e) {
// 目标有但源没有的字段,跳过(正常情况)
} catch (Exception e) {
// 其他异常打印日志,不影响主流程
e.printStackTrace();
}
}
}
/**
* 重载方法:创建新对象并拷贝属性
*
* @param source 源对象
* @param targetClass 目标类型
* @return 拷贝后的新对象
*/
public static <T> T copyProperties(Object source, Class<T> targetClass) {
if (source == null) {
return null;
}
try {
// 创建目标对象实例
T target = targetClass.newInstance();
// 拷贝属性
copyProperties(source, target);
return target;
} catch (Exception e) {
throw new RuntimeException("属性拷贝失败", e);
}
}
}
3.3 使用示例:电商场景
// java
// ========== 用户实体类 ==========
class UserEntity {
private int id;
private String name;
private String email;
public UserEntity() {}
// 省略getter/setter,实际项目必须有
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
@Override
public String toString() {
return "UserEntity{id=" + id + ", name='" + name + "', email='" + email + "'}";
}
}
// ========== 用户DTO(前端传过来的数据)==========
class UserDTO {
private int id;
private String name;
private String email;
public UserDTO() {}
public UserDTO(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public int getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
@Override
public String toString() {
return "UserDTO{id=" + id + ", name='" + name + "', email='" + email + "'}";
}
}
// ========== 测试:DTO转Entity ==========
public class BeanUtilsTest {
public static void main(String\[\] args) {
// 模拟前端传来的JSON数据(UserDTO)
UserDTO dto = new UserDTO(1, "张三", "zhangsan@qq.com");
System.out.println("DTO: " + dto);
// 方式1:拷贝到已存在的对象
UserEntity entity = new UserEntity();
BeanUtils.copyProperties(dto, entity);
System.out.println("Entity: " + entity);
// 方式2:创建新对象并拷贝
UserEntity entity2 = BeanUtils.copyProperties(dto, UserEntity.class);
System.out.println("Entity2: " + entity2);
}
}
3.4 运行结果
DTO: UserDTO{id=1, name='张三', email='zhangsan@qq.com'}
Entity: UserEntity{id=1, name='张三', email='zhangsan@qq.com'}
Entity2: UserEntity{id=1, name='张三', email='zhangsan@qq.com'}
3.5 业务价值
|----------------|-------------------------------|
| 传统方式 | BeanUtils方式 |
| 手写10+行setter代码 | 一行 BeanUtils.copyProperties() |
| 字段多有遗漏风险 | 自动匹配同名字段 |
| 维护成本高 | 改字段只需改一处 |
| 代码冗余 | 简洁优雅 |
��面试考点:手写BeanUtils的核心思路
面试官问 :如何手写一个BeanUtils属性拷贝工具?
标准话术 :
-
获取源对象和目标对象的 Class
-
遍历目标类的所有声明字段(getDeclaredFields)
-
在源类中查找同名字段(getDeclaredField)
-
设置访问权限(setAccessible)处理private
-
获取源字段值(field.get(source))
-
设置到目标字段(field.set(target, value))
-
注意处理异常:NoSuchFieldException表示字段不匹配,直接跳过
�� 课堂练习(第2课)
练习1 :扩展 BeanUtils,支持 类型转换 (如 String 转 int,String 转 Date)。
练习2 :使用反射实现一个方法,打印任意对象的所有字段名和值 (类似 toString,但字段名和值用等号连接)。
练习3 :编写代码,通过反射获取 System.out 的 println 方法并调用,打印 "Hello Reflection"。
�� 知识体系总结
反射核心知识体系树
Java反射 (Reflection)
│
├── 一、Class对象获取
│ ├── 1. 类名.class // 编译期确定,最常用
│ ├── 2. 对象.getClass() // 已有对象时使用
│ └── 3. Class.forName() // 运行时动态获取,触发静态块
│
├── 二、反射操作构造方法
│ ├── getConstructor() // 获取public构造
│ ├── getDeclaredConstructor()// 获取所有构造(含private)
│ ├── newInstance() // 创建对象实例
│ └── �� 面试重点:newInstance vs Constructor.newInstance
│
├── 三、反射操作成员方法
│ ├── getMethod() // 获取public方法(含继承)
│ ├── getDeclaredMethod() // 获取本类所有方法(含private)
│ ├── method.invoke() // 调用方法
│ ├── setAccessible(true) // �� 暴力访问private方法
│ └── �� 面试重点:getMethod vs getDeclaredMethod
│
├── 四、反射操作成员字段
│ ├── getField() // 获取public字段(含继承)
│ ├── getDeclaredField() // 获取本类所有字段(含private)
│ ├── field.get() // 读取字段值
│ ├── field.set() // 写入字段值
│ └── �� 面试重点:getField vs getDeclaredField
│
├── 五、项目实战应用
│ ├── 配置驱动工厂 // DaoFactory + properties
│ ├── 通用属性拷贝 // BeanUtils 实现
│ └── DTO/Entity/VO转换 // 电商项目核心场景
│
└── 六、面试高频考点
├── �� Class.forName() vs 类名.class 的区别
├── �� setAccessible(true) 的作用
├── �� getMethod vs getDeclaredMethod 对比
└── �� 手写BeanUtils的核心思路
�� 课后作业
作业1(基础) :编写一个类 ReflectionUtils,包含以下工具方法:
• getFieldValue(Object obj, String fieldName) - 获取指定字段的值
• setFieldValue(Object obj, String fieldName, Object value) - 设置指定字段的值
作业2(进阶) :实现一个 简单版 Spring IoC 容器 :
• 从配置文件读取 Bean 定义(class 属性)
• 提供 getBean(String id) 方法获取 Bean 实例
• 使用单例模式,每个 Bean 只创建一次
作业3(综合) :扩展 BeanUtils,实现 深度拷贝 功能:
• 支持嵌套对象的拷贝(如 User.address → new User().address)
• 支持集合类型的拷贝(List、Set)
• 处理循环引用(避免无限递归)
作业4(框架原理) :分析 MyBatis 为什么需要用反射?尝试手写一个简化版的 ResultSet → Entity 映射器。
作业5(面试突击) :准备一段 2 分钟的讲解,向面试官介绍你项目中如何使用反射提升代码通用性。