1. 何为反射?
反射(Reflection)机制指的是程序在运行的时候能够获取自身的信息。具体来说,反射允许程序在运行时获取关于自己代码的各种信息。如果知道一个类的名称或者它的一个实例对象, 就能把这个类的所有方法和变量的信息(方法名,变量名,方法,修饰符,类型,方法参数等等所有信息)找出来。这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。简单来说,就是面向对象看待一个类,即以面向对象的思想来抽象一个类。
反射是一种强大的功能,广泛应用于框架设计、依赖注入、单元测试和动态代理等领域。然而,由于反射会绕过编译时的类型安全检查,所以使用不当可能会导致更高的运行时错误几率和性能开销。因此,在使用反射时需要格外谨慎,以避免潜在的风险。
2. 反射的意义所在?
- 通过反射机制可以让程序创建和控制任何类的对象,无需提前硬编码目标类。
- 使用反射机制能够在运行时构造一个类的对象、判断一个类所具有的成员变量和方法、调用一个对象的方法。
- 反射机制是构建框架技术的基础所在,使用反射可以避免将代码写死在框架中。
不过反射也存在明显的缺点:使用反射性能较低,需要解析字节码,且如果将内存中的对象进行解析,相对不安全,会破坏封装性。
3. 通过反射实例化对象
在Java中,可以通过反射获取类的构造器、方法、字段等信息:
class Test {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// 下面这段代码是以反射机制的方式创建对象。
// 通过反射机制,获取Class,通过Class来实例化对象
Class c = Class.forName("com.reflectBean.User");
// newInstance() 这个方法会调用User这个类的无参数构造方法,完成对象的创建。
// 重点是:newInstance()调用的是无参构造,必须保证无参构造是存在的!
Object obj = c.newInstance();
Method method = c.getMethod("myMethod");
method.invoke(obj);
}
}
4. 获取Class类的三种方式
在Java编程中,获取 Class 对象的三种常用方式如下:
1. 通过类名的静态属性 class: 这是获取 Class 对象最简单的方法,适用于已知编译时类型的情况。
Class<MyClass> clazz = MyClass.class;
2. 通过对象的实例方法 getClass: 这是在运行时获取一个对象的 Class 对象的方法,适用于已知一个实例化对象的情况。
MyClass obj = new MyClass();
Class<? extends MyClass> clazz = obj.getClass();
3. 通过类名的字符串 forName: 这是动态加载类的一种方式,特别有用在类名只有在运行时才能确定的情况下。
try {
Class<?> clazz = Class.forName("com.reflectBean.User");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
5. 常用的Class类的方法
Class 类在 Java 中定义了大量的方法,可以用来反射性地获取类的各种信息或进行操作。不过需要注意的是,在使用反射进行私有成员访问时,需要设置相应的访问权限,如 setAccessible(true)。以下是一些常用的方法:
1. 类信息获取方法
getName()
:返回完整类名带包名。
getSimpleName()
:获取类的简单名称。
getCanonicalName()
:获取类的规范名称。
getPackage()
:获取类所在的包。
getModifiers()
:获取类的修饰符(如 public, abstract, final 等)。
2. 构造函数相关方法
getConstructor(Class<?>... parameterTypes):获取指定参数类型的公共构造函数。
getConstructors():获取所有公共构造函数。
getDeclaredConstructor(Class<?>... parameterTypes):获取指定参数类型的任意(包括私有)构造函数。
getDeclaredConstructors():获取所有任意(包括私有)构造函数。
3. 方法相关方法
- getMethod(String name, Class<?>... parameterTypes):获取指定名称和参数类型的公共方法。
- getMethods():获取所有公共方法,包括从父类继承的方法。
- getDeclaredMethod(String name, Class<?>... parameterTypes):获取指定名称和参数类型的任意方法。
- getDeclaredMethods():获取所有任意方法(包括私有)。
4. 字段(成员变量)相关方法
getField(String name)
:获取指定名称的公共字段。getFields()
:获取所有公共字段。getDeclaredField(String name)
:获取指定名称的任意字段。getDeclaredFields()
:获取所有任意字段(包括私有)。
5. 类层次结构相关方法
getSuperclass()
:获取父类。getInterfaces()
:获取实现的接口。isAssignableFrom(Class<?> cls)
:判断当前类或接口是否可以从指定类型的对象赋值(即能否进行类型转换)。
6. 实例创建方法
newInstance()
:创建类的实例,需要无参构造函数。
7. 其他方法
getAnnotations()
:获取类上的注解。isAnnotation()
:判断类是否是注解类型。isInterface()
:判断是否为接口。isArray()
:判断是否为数组。isPrimitive()
:判断是否为基本数据类型。isInstance(Object obj)
:判断给定对象是否为该Class
的实例。
6. 获取Field的四种方式
在Java中,可以通过Class
对象获取某个类的字段(Field),Field
类代表类的成员变量。获取 Field的几种常用方式如下:
1. 通过字段名获取公共字段 (public 属性):
try {
Class<?> clazz = Class.forName("com.reflectBean.User");
Field field = clazz.getField("userName");
// 对 field 进行操作
// ...
} catch (NoSuchFieldException | ClassNotFoundException e) {
e.printStackTrace();
}
2. 获取所有公共字段 (public 属性):
try {
Class<?> clazz = Class.forName("com.reflectBean.User");
Field[] fields = clazz.getFields();
for (Field field : fields) {
// 对每个 field 进行操作
// ...
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
3. 通过字段名获取任何字段(包括私有、保护和包访问属性):
try {
Class<?> clazz = Class.forName("com.reflectBean.User");
Field field = clazz.getDeclaredField("password");
// 允许访问私有字段
field.setAccessible(true);
// 对 field 进行操作,比如设置可访问性
field.setAccessible(true);
} catch (NoSuchFieldException | ClassNotFoundException e) {
e.printStackTrace();
}
4. 获取所有字段(包括私有、保护和包访问属性):
try {
Class<?> clazz = Class.forName("com.reflectBean.User");
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 对每个 field 进行操作,比如设置可访问性
field.setAccessible(true);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
7. 常用的Field类的方法
在Java中,Field类提供了许多方法,用于获取和操作类的字段(成员变量)。以下是一些常用的方法:
1. 获取字段信息的方法
- getName():返回字段的名称。
- getType():返回表示字段类型的 Class 对象。
- getGenericType():返回表示字段的 Type 对象,包括泛型类型信息。
- getModifiers():返回字段的修饰符整数编码(使用 Modifier 类中的方法可以解码)。
2. 访问和修改字段值的方法
- get(Object obj):从指定对象中获取该字段的值。
- getByte(Object obj):从指定对象中获取该字段的值(byte类型)。
- set(Object obj, Object value):将指定对象的该字段设置为新的值。
3. 访问控制
- setAccessible(boolean flag):设置字段的可访问性。即使字段是私有的,通过设置可访问性也可以访问它。
8. 获取Method的四种方式
在Java中,可以通过反射机制获取类的方法(Method
)对象。以下是几种常用的方法获取方式:
1. 通过方法名和参数类型获取公共方法
try {
// 获取类的Class对象
Class<?> clazz = Class.forName("com.reflectBean.User");
// 通过方法名和参数类型获取公共方法
Method method = clazz.getMethod("publicMethod", String.class);
// 调用方法
method.invoke(null, "参数");
} catch (Exception e) {
e.printStackTrace();
}
2. 获取所有公共方法
try {
// 获取类的Class对象
Class<?> clazz = Class.forName("com.reflectBean.User");
// 获取所有公共方法
Method[] methods = clazz.getMethods();
// 遍历所有方法
for (Method method : methods) {
// 获取方法名
String methodName = method.getName();
// 判断方法名是否为getName
if (methodName.equals("getName")) {
// 执行getName方法
Object result = method.invoke(user);
System.out.println(result);
}
}
} catch (Exception e) {
e.printStackTrace();
}
3. 通过方法名和参数类型获取任意方法(包括私有、保护和默认访问权限的方法)
try {
// 获取类的Class对象
Class<?> clazz = Class.forName("com.reflectBean.User");
// 通过方法名和参数类型获取任意方法
Method method = clazz.getDeclaredMethod("privateMethod", int.class);
// 调用方法
method.invoke(new com.reflectBean.User(), 100);
// 允许访问私有方法
method.setAccessible(true);
} catch (Exception e) {
e.printStackTrace();
}
4. 获取所有的任意方法(包括私有、保护和默认访问权限的方法)
try {
// 获取类的Class对象
Class<?> clazz = Class.forName("com.example.MyClass");
// 获取所有任意方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
// 对每个 method 进行操作
// ...
}
} catch (Exception e) {
e.printStackTrace();
}
9. 常用的Method的方法
Method类在Java反射中提供了许多方法,这些方法可以用于获取方法的各类信息、调用方法以及操作注解等。以下是一些常用的Method类方法:
1. 获取方法信息的方法
- String getName():获取方法的名称。
- Class<?> getDeclaringClass():获取声明此方法的类或接口的Class对象。
- Class<?> getReturnType():获取方法的返回类型。
- Type getGenericReturnType():获取方法的泛型返回类型。
- Class<?>[] getParameterTypes():获取方法的参数类型数组。
- Type[] getGenericParameterTypes():获取方法的泛型参数类型数组。
- int getParameterCount():获取方法的参数数量。
- Class<?>[] getExceptionTypes():获取方法声明的异常类型数组。
- Type[] getGenericExceptionTypes():获取方法声明的泛型异常类型数组。
- int getModifiers():获取方法的修饰符,用于确定方法是public、private、protected、static等。
- Annotation[][] getParameterAnnotations():获取方法的参数上的注解。
- String toGenericString():返回描述方法的字符串,包括泛型信息。
2. 调用方法
- Object invoke(Object obj, Object... args):调用此Method对象表示的底层方法。
3. 注解相关的方法
- <T extends Annotation> T getAnnotation(Class<T> annotationClass):获取指定类型的注解。
- Annotation[] getAnnotations():获取此方法上存在的所有注解。
- Annotation[] getDeclaredAnnotations():获取直接存在于此方法上的所有注解。
- <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass):获取直接存在于此方法上的指定类型的注解。
- <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass):获取直接存在于此方法上的指定类型的所有注解。
- <T extends Annotation> T getAnnotationsByType(Class<T> annotationClass):获取此方法上存在的指定类型的所有注解。
- boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判断此方法上是否存在指定类型的注解。
4. 设置和获取访问控制
- void setAccessible(boolean flag):设置方法的可访问性。如果是 true,则强制使能此方法,即使它是私有的。
- boolean isAccessible():判断此方法是否可以通过反射执行。
5. 其他常用方法
- boolean isSynthetic():判断此方法是否是编译器生成的合成方法。
- boolean isVarArgs():判断此方法是否接受可变数量的参数。
10. 实践出真知
好了,扯了那么久反射的概念和基础使用,接下来,结合反射的内容,我们来简单演示通过反射去实现一些基本业务。
10.1 如何通过反射获取和设置对象私有字段的值?
先捋清楚思路,获取和设置对象私有字段的值,大致可以分为四个步骤实现:
1. 获取对象的 Class 对象。
2. 通过反射获取私有字段 Field 对象。
3. 设置字段的可访问性为 true,以绕过 Java 语言访问检查。
4. 通过反射获取和设置字段的值。
那么接下来演示下代码的实现流程:
假设有一个包含私有字段的类:AccountBalance.class
public class AccountBalance {
// 设置初始值为100
private BigDecimal balance = new BigDecimal(100);
// 默认构造函数
public AccountBalance() {
}
// 打印字段值的方法
public void printField() {
System.out.println("当前的账户余额为: " + balance);
}
}
接下来,我们来演示下怎么通过反射获取账户余额,然后变更私有字段:
public class Test {
public static void main(String[] args) {
try {
// 创建 AccountBalance 对象实例
AccountBalance account = new AccountBalance();
// 获取 AccountBalance 的 Class 对象
Class<?> clazz = account.getClass();
// 获取私有字段
Field privateField = clazz.getDeclaredField("balance");
// 设置字段的可访问性为 true
privateField.setAccessible(true);
// 获取私有字段的值
BigDecimal initBlance = (BigDecimal) privateField.get(account);
System.out.println("最初的账号余额为: " + initBlance);
// 设置私有字段的值
privateField.set(account, new BigDecimal(50));
// 验证字段值已经更新
initBlance = (BigDecimal) privateField.get(account);
System.out.println("本次扣除的账户余额为: " + initBlance);
// 使用类的方法验证字段被正确修改
account.printField();
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
控制台输出:
10.2 如何用反射机制创建对象呢?
依旧是先整理下步骤,使用反射机制创建对象,大致可分为3个步骤:
1. 获取类的Class对象。
2. 通过Class对象获取构造方法(构造器)。注意,这里虽然可以通过Class对象的newInstance方法简单地创建一个类的实例,但是这种方式只能调用无参构造函数,故该实例不给予演示。
3. 使用构造方法创建类的实例。
还是假设我们有一个包含无参和有参数构造函数的类AccountBalance.class
public class AccountBalance {
// 设置初始值为100
private BigDecimal balance = new BigDecimal(100);
// 默认构造函数
public AccountBalance() {
System.out.println("调用无参构造函数");
}
// 有参构造函数
public AccountBalance(BigDecimal initialBalance) {
this.balance = initialBalance;
System.out.println("调用带参数的构造函数:初始化余额为 " + initialBalance);
}
// 打印字段值的方法
public void printField() {
System.out.println("当前的账户余额为: " + balance);
}
}
使用Constructor.newInstance()创建对象并访问其方法
public void Test {
public static void main(String[] args) {
try {
// 获取 AccountBalance 的 Class 对象
Class<?> clazz = Class.forName("com.example.AccountBalance");
// 获取有参构造函数
Constructor<?> constructor = clazz.getConstructor(BigDecimal.class);
// 使用构造函数创建实例并传递参数
Object obj = constructor.newInstance(new BigDecimal(200));
// 验证对象创建成功,通过调用方法
Method printMethod = clazz.getMethod("printField");
printMethod.invoke(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
10.3 如何反编译一个类的构造方法?
反编译一个类的构造方法,大致可以分为四个步骤实现:
1. 获取需要被反编译的类的
Class
对象2. 获取类所有声明的构造方法
3. 循环遍历所有的构造方法
4. 获取每个构造方法的修饰符、参数类型并格式化输出
我们仍然以AccountBalance
类为例进行说明。
public class AccountBalance {
// 设置初始值为100
private BigDecimal balance = new BigDecimal(100);
// 默认构造函数
public AccountBalance() {
System.out.println("调用无参构造函数");
}
// 有参构造函数
public AccountBalance(BigDecimal initialBalance) {
this.balance = initialBalance;
System.out.println("调用带参数的构造函数:初始化余额为 " + initialBalance);
}
// 打印字段值的方法
public void printField() {
System.out.println("当前的账户余额为: " + balance);
}
}
进行反编译构造方法流程:
public void Test {
public static void main(String[] args) {
try {
// 获取 AccountBalance 的 Class 对象
Class<?> clazz = Class.forName("com.example.AccountBalance");
// 获取所有声明的构造方法(包括私有构造方法)
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
// 获取构造方法的修饰符
String modifiers = Modifier.toString(constructor.getModifiers());
// 获取构造方法的参数类型
Class<?>[] parameterTypes = constructor.getParameterTypes();
StringBuilder params = new StringBuilder();
for (int i = 0; i < parameterTypes.length; i++) {
params.append(parameterTypes[i].getSimpleName());
if (i < parameterTypes.length - 1) {
params.append(", ");
}
}
// 打印构造方法的详细信息
System.out.println(modifiers + " " + clazz.getSimpleName() + "(" + params + ") { }");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
10.4 给定一个类,如何获取这个类的基类以及实现的接口呢?
这种问题的思路,需要考虑两点:
1. 怎么获取该类的父类:这点可以使用Class提供的getSuperClass()方法来获取。2. 怎么获取类的接口:这点可以使用Class提供的getInterfaces()方法获取实现的接口。
假设定义了以下类和接口:ChildClass.class 和 Parent.class、Car.java。
public class ChildClass extends ParentClass implements Car {
}
public class ParentClass {
}
public interface Car {
}
获取一个类的父类和实现的接口的具体示例:
public static void main(String[] args) throws ClassNotFoundException {
try {
// 获取 ChildClass 的 Class 对象
Class<?> clazz = Class.forName("com.example.ChildClass");
// 获取父类
Class<?> superClass = clazz.getSuperclass();
System.out.println("父类: " + superClass.getName());
// 获取实现的接口
Class<?>[] interfaces = clazz.getInterfaces();
System.out.println("实现的接口:");
for (Class<?> iface : interfaces) {
System.out.println(iface.getName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
11. 总结
反射机制是Java语言中一种强大的功能,它让我们能够在运行时进行一些本应只能在编译时完成的操作。这个功能的特点就是使得程序可以动态地检查和操作类、方法、字段等,从而带来了极大的灵活性。简单来说,反射机制使得开发人员能够编写更加灵活和动态的代码。举个例子,我们可以根据配置文件来动态创建对象、实现依赖注入、在运行时调用方法而不需要提前知道方法的名称。
然而,反射机制虽然给我们带来了好处,但也有一些弊端需要注意。首先,反射通常会比直接调用要慢,因为它会绕过一些编译时的优化。其次,由于反射机制允许操作私有成员,可能会破坏封装性和安全性。最后,使用反射的代码可读性降低,同时也更难以维护和调试。因此,在使用反射时需要谨慎考虑其影响。