文章目录
- 练习1
- 练习2
- 练习3
- 思考
- 封装原则与反射
- 合理使用反射“破坏”封装的场景
练习1
编写代码,通过反射获取String类的所有公共方法名称,并按字母顺序打印。
- 示例代码:
import java.lang.reflect.Method;
import java.util.Arrays;
public class StringMethodsReflection {
public static void main(String[] args) {
// 获取 String 类的所有公共方法(包含继承的方法)
Method[] methods = String.class.getMethods();
// 把方法名称提取到一个字符串数组中
String[] methodNames = new String[methods.length];
for (int i = 0; i < methods.length; i++) {
methodNames[i] = methods[i].getName();
}
// 对方法名称数组进行字母排序
Arrays.sort(methodNames);
// 为了防止重复打印同一方法名(由于重载会有相同名称),只输出不相同的名称
String lastPrinted = "";
for (String name : methodNames) {
if (!name.equals(lastPrinted)) {
System.out.println(name);
lastPrinted = name;
}
}
}
}
-
代码解析:
-
获取方法:
调用 String.class.getMethods() 能获取 String 类中所有的公共方法,包括从父类继承的方法。 -
提取方法名称:
将所有 Method 对象的名称依次存入 String 数组。 -
排序:
使用 Arrays.sort 对数组进行字母排序。 -
去重输出:
考虑到方法可能重载导致名称重复,通过比较上一个输出值过滤重复名称后依次打印。 -
输出结果:(部分)
练习2
创建一个包含私有字段private int value的类Secret,通过反射修改该字段的值并验证结果。
- 代码示例:
创建两个 Java 文件,一个用于定义类 Secret(中级练习题的目标类),另一个用于编写反射修改私有字段的代码。
// Secret.java
public class Secret {
private int value;
// 构造方法,用于初始化 value
public Secret(int value) {
this.value = value;
}
// 提供 getter 方法便于验证修改后的值
public int getValue() {
return value;
}
}
// ModifySecretReflection.java
import java.lang.reflect.Field;
public class ModifySecretReflection {
public static void main(String[] args) {
try {
// 创建 Secret 对象,初始值为 10
Secret secret = new Secret(10);
System.out.println("修改前的 value: " + secret.getValue());
// 通过反射获取私有字段 "value"
Field field = Secret.class.getDeclaredField("value");
// 解除封装限制,使私有字段可以被操作
field.setAccessible(true);
// 将字段的值修改为 42
field.setInt(secret, 42);
// 验证修改后的值
System.out.println("修改后的 value: " + secret.getValue());
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 输出结果:
- 代码解析:
- Secret 类:
定义了一个私有字段 value,同时提供构造方法和 getter 方法用于初始化和获取该字段的值。 - 反射修改字段:
在 ModifySecretReflection 类中:- 使用 Secret.class.getDeclaredField(“value”) 得到对应的 Field 对象。
- 调用 setAccessible(true) 来取消对私有字段的访问限制。
- 使用 field.setInt(secret, 42) 修改实例 secret 的 value 字段。
- 验证结果:
修改前后分别输出 value 字段的值,从而验证反射修改是否成功。
练习3
实现一个简易的“对象拷贝工具”,通过反射将源对象的所有字段值复制到目标对象的同名字段中(支持不同类的兼容字段)。
- 示例代码:
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class ObjectCopyUtil {
/**
* 将 source 对象中所有字段(包括私有字段)复制到 target 对象中同名且类型兼容的字段上。
*
* @param source 源对象
* @param target 目标对象
*/
public static void copyFields(Object source, Object target) {
Class<?> srcClass = source.getClass();
Class<?> targetClass = target.getClass();
// 获取源对象所有字段(包括继承的字段)
for (Field srcField : getAllFields(srcClass)) {
String fieldName = srcField.getName();
try {
// 从目标对象中查找同名字段
Field targetField = getField(targetClass, fieldName);
if (targetField != null) {
// 判断目标字段类型是否可以赋值源字段类型(兼容性检查)
if (targetField.getType().isAssignableFrom(srcField.getType())) {
srcField.setAccessible(true);
targetField.setAccessible(true);
Object value = srcField.get(source);
targetField.set(target, value);
}
}
} catch (Exception e) {
// 如果拷贝过程中发生异常,可以打印出来或继续处理
e.printStackTrace();
}
}
}
/**
* 从指定的类及其父类中获取所有字段。
*
* @param clazz 类对象
* @return 包含所有字段的数组
*/
private static Field[] getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> current = clazz;
while (current != null) {
Field[] declaredFields = current.getDeclaredFields();
for (Field f : declaredFields) {
fields.add(f);
}
current = current.getSuperclass();
}
return fields.toArray(new Field[0]);
}
/**
* 在目标类及其父类中查找指定名称的字段。
*
* @param clazz 类对象
* @param fieldName 字段名
* @return 找到的字段,若无则返回 null
*/
private static Field getField(Class<?> clazz, String fieldName) {
Class<?> current = clazz;
while (current != null) {
try {
return current.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
current = current.getSuperclass();
}
}
return null;
}
// 下面提供一个简单的测试示例
public static void main(String[] args) {
// 创建源对象和目标对象,这里两者的字段名称和类型保持兼容
SourceClass src = new SourceClass(100, "Hello", 3.14);
TargetClass trg = new TargetClass(0, "World", 0.0);
System.out.println("拷贝前:");
System.out.println("源对象: " + src);
System.out.println("目标对象: " + trg);
// 调用拷贝工具,将源对象的字段值复制到目标对象中
copyFields(src, trg);
System.out.println("拷贝后:");
System.out.println("源对象: " + src);
System.out.println("目标对象: " + trg);
}
}
// 示例:定义源对象类
class SourceClass {
private int num;
public String text;
protected double decimal;
public SourceClass(int num, String text, double decimal) {
this.num = num;
this.text = text;
this.decimal = decimal;
}
@Override
public String toString() {
return "SourceClass{num=" + num + ", text='" + text + "', decimal=" + decimal + "}";
}
}
// 示例:定义目标对象类
class TargetClass {
private int num;
public String text;
protected double decimal;
public TargetClass(int num, String text, double decimal) {
this.num = num;
this.text = text;
this.decimal = decimal;
}
@Override
public String toString() {
return "TargetClass{num=" + num + ", text='" + text + "', decimal=" + decimal + "}";
}
}
输出结果:
-
代码解析
-
copyFields 方法:
- 接受两个参数:源对象和目标对象。
- 调用 getAllFields 方法获得源对象中所有字段,包括私有和继承的字段。
- 对每个字段调用 getField,在目标对象中查找同名字段,如果找到则进行类型兼容检查(使用 isAssignableFrom 判断),确保源字段的值能赋给目标字段。
- 通过 setAccessible(true) 解除访问限制,然后从源对象中获取值,并设置到目标对象中对应字段上。
-
辅助方法 getAllFields:
- 通过循环遍历类及其父类,获取所有的声明字段,便于对象拷贝时不遗漏继承字段。
-
辅助方法 getField:
- 逐级向上查找目标类中的同名字段,确保可以操作可能在父类中声明的字段。
-
测试示例:
- 定义了两个示例类 SourceClass 和 TargetClass,它们具有相同名称和类型的字段。
- 在 main 方法中创建两个实例,并调用 copyFields 实现拷贝。
- 最后输出拷贝前后的对象状态,用于验证拷贝效果是否正确。
思考
封装原则与反射
- 封装的初衷:
封装主要目的是隐藏内部实现细节,对外只暴露必要的接口,从而降低系统复杂度以及增强代码的安全性和可维护性。通过私有成员和方法,类能更好地控制数据和行为的访问。 - 反射的特性:
反射作为一种强大的工具,允许在运行时动态检查类的结构,包括构造方法、字段和方法。通过反射,可以在运行时绕过常规的访问控制,从而对私有成员进行操作。这种能力确实打破了封装的“规则”,但它是为了提供动态性和灵活性,而不是作为日常开发中修改对象状态的一般手段。
合理使用反射“破坏”封装的场景
虽然反射破坏了封装原则,但在某些场景下,这种“破坏”是被认可且必要的:
1.框架与库设计:
很多 Java 框架(如依赖注入、ORM 框架、序列化工具)需要访问对象内部的状态来完成对象构造、初始化、映射和序列化操作。例如,Spring 框架在依赖注入和 AOP 方面就大量利用了反射。
2.单元测试:
在单元测试中,有时需要测试那些没有暴露接口的方法或状态。借助反射可以绕过正常访问限制,对对象进行细粒度的状态验证。不过,过度依赖这种方式可能提示设计上的问题,因此一般建议将测试重点放在公开接口上。
3.调试与诊断:
在调试或诊断复杂系统时,反射可以帮助开发者查看对象的真实内部状态,以便更迅速地定位问题。这种情况下,使用反射有助于快速恢复问题的本质。
4.对象映射和序列化:
对象与数据之间的映射(例如 JSON 序列化与反序列化)通常需要知道对象内部的字段信息。反射可以自动化这一过程,无需手动编写大量冗余代码。