目录
一、反射基础
1. 反射的用途
2. 了解反射的底层运作
直接使用类
使用反射
总结
3. 反射的缺点
二、在Java中使用反射
1. 获取类型信息
1.1. Object.getClass()
1.2. XXX.class
1.3. Class.forName()
1.4. Integer.TYPE
1.5. 通过反射类ClassAPI获取类
2. 获取类的成员变量
2.1. 获取字段:
2.2. 获取方法:
2.3. 获取构造器:
3. 操作java.lang.reflect.Field类
3.1. 获取字段类型:
3.2. 获取字段修饰符:
3.3. 获取和设置字段值:
4. 反射修改final修饰的属性值
5. 操作java.lang.reflect.Method类
5.1. 获取方法类型的信息:
6. 操作java.lang.reflect.Constructor类
结语
参考:The Reflection API
深入理解Java虚拟机第三版
反射功能通常用于检查或修改Java虚拟机运行中(runtime)的应用程序的行为,这一句话就精准的描述了反射的全部功能,更详细来说可以分为以下几点:
1. 在运行中分析类的能力,可以通过完全限定类名创建类的对象实例。
2. 在运行中查看和操作对象,可以遍历类的成员变量。
3. 反射允许代码执行非反射代码中非法的操作,可以检索和访问类的私有成员变量,包括私有属性、方法等。
注意:要有选择的使用反射功能,如果可以直接执行操作,那么最好不要使用反射。
为了彻底理解反射的原理,可以先理解一下虚拟机的工作机制。
通常,java在编译之后,会将Java代码生成为class源文件,JVM启动时,将会载入所有的源文件,并将类型信息存放到方法区中;将所有对象实例存放在Java堆中,同时也会保存指向类型信息的指针。
以下分两种情况来分析,直接使用类和使用反射的区别,以此理解反射的实现原理。
正常流程下,我们要创建一个类的实例,是一定确定这个类的类型信息的,我们知道这个类的名字、方法、属性等等。我们可以很容易的创建实例,也可以通过实例很容易的获取属性、调用方法。
ArrayList list = new ArrayList<>();
list.add("A");
int size = list.size();
在一个方法中,如果我们不知道在实际运行(runtime)时,它将要处理的对象是谁,它的类型信息是怎么样的,那我们如何访问这个对象或为这个对象创建一个新的实例呢?
与直接使用类相反,我们需要先获取到对象在方法区的类型信息,获取到类型信息后,我们就知道这个类的构造器、属性、方法、注解、子类、父类等等信息了,这个时候,我们就可以通过这些类型信息来回调处理对象,来完成自己想要的操作了。
没错,这就是反射的原理了。反射在运行时,通过读取方法区中的字节码,来动态的找到其反射的类以及类的方法和属性等(实际上就是在运行时,根据全类型名在方法区找对应的类),用这些类型信息完成对该类实例的操作,其实就是直接使用类的一个逆向使用。
void reflectMethod(Object obj) {
// 处理这个无法明确类型的实例对象
// 获取类型信息
Class> aClass = obj.getClass();
Field[] fields = aClass.getFields();
Method[] methods = aClass.getMethods();
Annotation[] annotations = aClass.getAnnotations();
Constructor>[] constructors = aClass.getConstructors();
Class>[] interfaces = aClass.getInterfaces();
// ...
// 操作属性或方法
Field field = fields[0];
Object o = field.get(obj); // 获取obj的属性值
}
在实际开发过程会遇到很多这种情况,譬如常用到的Bean属性工具类org.springframework.beans.BeanUtils.copyProperties(Object source, Object target),在复制对象属性前,它是并不知道source、target这两个对象有什么属性的,那么这个工具类是如何完成属性复制呢?这里其实就用到了反射功能。可以简单了解下流程:
- 获取target的类型
- 获取target类中属性、getter和setter方法
- 遍历target中的属性,查询source中是否有属性名相同且支持getter和setter的属性
- 通过source.getter.invoke方法读取值
- 最后通过target.setter.invoke(source.getter.invoke) 设置刚刚从source读取的值
- 循环遍历target所有属性后,就完成了整个属性的复制
这里只是一个简单的反射运用,感兴趣的可以看看源码
直接使用是在运行前就明确类型信息,然后在运行时根据这个类来操作对象;
而反射是运行时先拿到对象,根据对象得到方法区中的类型信息后,再根据属性、方法来操作该对象。
1. 额外的性能开销(Performance Overhead):由于反射涉及动态类型的解析,它无法执行某些Java虚拟机优化,因此反射操作的性能通常要比非反射操作慢。
2. 安全限制(Security Restrictions):反射需要运行时操作权限,此操作可能在一些安全管理器下不被允许。
3. 内部泄露(Exposure of Internals):由于反射允许代码执行非反射代码中非法的操作(例如访问私有字段和方法),因此使用反射可能会导致意外的副作用,这可能会使代码无法正常工作并可能破坏可移植性。反射性代码破坏了抽象,因此可能会随着平台的升级而改变行为。
从一个实例对象中获取它的类。这仅适用于继承自Object的引用类型(当然Java的类默认继承于Object)。
Map hashMap = new HashMap<>();
Class extends Map> aClass = hashMap.getClass();
String text = "text";
Class extends String> aClass1 = text.getClass();
// Object类源码
public final native Class> getClass();
直接从未实例化的类获取类。
Class integerClass = int.class;
Class hashMapClass = HashMap.class;
通过完全限定类名获取类。即包名加类名(java.util.HashMap)。否则会报找不到类错误。
Class hashMapClass = Class.forName("java.util.HashMap");
// class类源码
public static Class> forName(String className)
throws ClassNotFoundException {
Class> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
基本类型的包装类通过TYPE获取类。都是Java早期版本的产物,已过时。
// Integer
@SuppressWarnings("unchecked")
public static final Class TYPE = (Class) Class.getPrimitiveClass("int");
// Double
@SuppressWarnings("unchecked")
public static final Class TYPE = (Class) Class.getPrimitiveClass("double");
注意,只有在已经直接或间接获得一个类的情况下,才可以访问这些API。
try {
Class> className = Class.forName("java.lang.String");
// 获取父类
Class> superclass = className.getSuperclass();
// 返回调用类的成员变量,包括所有公共的类、接口和枚举
Class>[] classes = className.getClasses();
// 返回调用类的依赖,包括所有类、接口和显式声明的枚举
Class>[] declaredClasses = className.getDeclaredClasses();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Class API |
是否是列表 |
是否获取父类属性 |
能否获取私有成员 |
getDeclaredField() |
no |
no |
yes |
getField() |
no |
yes |
no |
getDeclaredFields() |
yes |
no |
yes |
getFields() |
yes |
yes |
no |
Class API |
是否是列表 |
是否获取父类属性 |
能否获取私有成员 |
getDeclaredMethod() |
no |
no |
yes |
getMethod() |
no |
yes |
no |
getDeclaredMethods() |
yes |
no |
yes |
getMethods() |
yes |
yes |
no |
Class API |
是否是列表 |
是否获取父类属性 |
能否获取私有成员 |
getDeclaredConstructor() |
no |
N/A1 |
yes |
getConstructor() |
no |
N/A1 |
no |
getDeclaredConstructors() |
yes |
N/A1 |
yes |
getConstructors() |
yes |
N/A1 |
no |
说明:Field字段具有类型和值。Field提供访问属性对象类型信息的方法;以及获取和设置字段值的方法。
字段可以是原始类型或引用类型。
有八种基本类型:boolean,byte,short,int,long,char,float,和double。
引用类型是java.lang.Object类的直接或间接子类,包含接口,数组和枚举类型等 。
Class> className = Class.forName("java.util.HashMap");
Field table = className.getDeclaredField("table");
Class> type = table.getType();
Class> className = Class.forName("java.util.HashMap");
Field table = className.getDeclaredField("table");
// 获取属性的名字
String name = table.getName();
// 获取属性的类型
Class> type = table.getType();
// 获取修饰符
int modifiers = table.getModifiers();
System.out.println(Modifier.toString(modifiers));
// 获取注解
Override annotation = table.getDeclaredAnnotation(Override.class);
Annotation[] declaredAnnotations = table.getDeclaredAnnotations();
给定一个类的实例,可以使用反射来设置该类中字段的值。通常仅在特殊情况下无法以常规方式设置值时才执行此操作。因为这样的访问通常会违反该类的设计意图,所以应绝对谨慎地使用它。
HashMap map = new HashMap<>();
map.put("1", 2);
Class extends HashMap> mapClass = map.getClass();
Field capacity = mapClass.getDeclaredField("MAXIMUM_CAPACITY");
capacity.setAccessible(true); // 访问私有成员
Object o1 = capacity.get(map); // 获取属性值
capacity.set(map, 20); // 设置属性值
上面的设置属性值将会报错,因为hashmap中的MAXIMUM_CAPACITY参数是一个被static修饰的成员。
Exception in thread "main" java.lang.IllegalAccessException: Can not set static final int field java.util.HashMap.MAXIMUM_CAPACITY to java.lang.Integer
注意:通过反射设置字段的值会有一定的性能开销,因为必须进行各种操作,例如验证访问权限。从运行时的角度来看,效果是相同的,并且操作是原子的,就好像直接在类代码中更改了值一样。除此之外,反射会破坏Java原本的设定,列如可以重新设置final属性的值等。
反射功能强大,能修改private以及final修饰的变量。如下代码中,展示了JVM的优化以及反射的一些劣势。
@Data
public class FieldReflectDemo {
// 引用直接指向常量池中的常量值
private final String constantStr = "FinalConstantStringField";
// JVM优化了getter方法,直接将对constantStr引用全部替换成了常量
// public String getConstantStr() {return "FinalConstantStringField";}
// 在堆中新建了一个对象
private final String newStr = new String("FinalNewStringField");
public FieldReflectDemo(){}
public static void main(String[] args) {
FieldReflectDemo fieldReflectDemo = new FieldReflectDemo();
try {
Class> className = fieldReflectDemo.getClass();
Field constantStr = className.getDeclaredField("constantStr");
Field newStr = className.getDeclaredField("newStr");
// 获取实例对象的字段值
System.out.println("constantStr原:" + constantStr.get(fieldReflectDemo));
System.out.println("newStr原:" + newStr.get(fieldReflectDemo));
constantStr.setAccessible(true);
newStr.setAccessible(true);
constantStr.set(fieldReflectDemo, "New Filed Name");
newStr.set(fieldReflectDemo, "New Filed Name");
System.out.println("constantStr反射修改:" + constantStr.get(fieldReflectDemo));
System.out.println("newStr反射修改:" + newStr.get(fieldReflectDemo));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println("constantStr实例对象值:" + fieldReflectDemo.getConstantStr());
System.out.println("newStr实例对象值:" + fieldReflectDemo.getNewStr());
}
/**
* 输出
* constantStr原:FinalConstantStringField
* newStr原:FinalNewStringField
* constantStr反射修改:New Filed Name
* newStr反射修改:New Filed Name
* constantStr实例对象值:FinalConstantStringField
* newStr实例对象值:New Filed Name
*/
}
因为JVM在编译时期, 就把final类型的直接赋值的String进行了优化, 在编译时期就会把String处理成常量。反射成功将其值修改成功了,但是在它的get方法中,返回的不是当前变量,而是返回JVM优化好的一个常量值。
说明:
Method方法具有参数和返回值,并且方法可能抛出异常;
Method提供获取参数信息、返回值的方法;
它也可以调用(invoke)给定对象的方法。
方法声明包含了方法名、修饰符、参数、返回类型以及抛出的多个异常。
以及通过反射调用实例对象的方法。
public class MethodReflectDemo {
public MethodReflectDemo() {
private void getNothing(String name) {
public int getNumByName(String name) throws NullPointerException {
if (StringUtils.isEmpty(name))
throw new NullPointerException("名字为空");
return name.length();
}
public static void main(String[] args) {
MethodReflectDemo methodReflectDemo = new MethodReflectDemo();
try {
Class extends MethodReflectDemo> demoClass = methodReflectDemo.getClass();
Method method = demoClass.getDeclaredMethod("getNumByName", String.class);
String name = method.getName();
System.out.println("方法名:" + name);
// 修饰符
int modifiers = method.getModifiers();
System.out.println("所有修饰符:" + Modifier.toString(modifiers));
// 参数
Parameter[] parameters = method.getParameters();
// 返回类型
Class> returnType = method.getReturnType();
System.out.println("返回类型:" + returnType.getTypeName());
// 异常
Class>[] exceptionTypes = method.getExceptionTypes();
System.out.println("");
// 实例对象调用方法
Object invoke = method.invoke(methodReflectDemo, "名称");
System.out.println(invoke);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
Constructor与Method相似,但有几点不同:
public class ConstructorReflectDemo {
public ConstructorReflectDemo() {}
private void getNothing(String name) { }
public int getNumByName(String name) throws NullPointerException {
if (StringUtils.isEmpty(name))
throw new NullPointerException("名字为空");
return name.length();
}
public static void main(String[] args) {
ConstructorReflectDemo methodReflectDemo = new ConstructorReflectDemo();
try {
Class extends ConstructorReflectDemo> demoClass = methodReflectDemo.getClass();
Constructor extends ConstructorReflectDemo> constructor = demoClass.getConstructor();
String name = constructor.getName();
System.out.println("构造方法名:" + name);
// 修饰符
int modifiers = constructor.getModifiers();
System.out.println("所有修饰符:" + Modifier.toString(modifiers));
// 参数
Parameter[] parameters = constructor.getParameters();
// 异常
Class>[] exceptionTypes = constructor.getExceptionTypes();
System.out.println("");
// 构造方法无法被调用,只可以创建新实例
ConstructorReflectDemo constructorReflectDemo = constructor.newInstance();
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
自己也是玩心太大,很多时候都是抽空闲时间写的,所以写这一篇文章前前后后花了快一周吧。
此外,由于自己对内存模型那块还不是特别熟悉,所以错误在所难免。希望和各位大佬交流交流、