一、背景
要理解反射,首先要知道它产生的背景。
在 Java 中,正常情况下我们只需要 new 某个类来使用就行了,但是如果想在运行时灵活创建某个类怎么办?想要使用某个类但是并没有被 JVM 加载怎么办?
答案就是利用 反射,这个机制可以帮助我们在运行期需要的时候去加载创建某个类,从而使用该类的方法。
这里引用 知乎Kira 的回答可能更容易理解:
假如你写了一段代码:Object o=new Object();运行了起来!
首先JVM会启动,你的代码会编译成一个.class文件,然后被类加载器加载进jvm的内存中,你的类Object加载到方法区中,创建了Object类的class对象到堆中,注意这个不是new出来的对象,而是类的类型对象,每个类只有一个class对象,作为方法区类的数据结构的接口。
jvm创建对象前,会先检查类是否加载,寻找类对应的class对象,若加载好,则为你的对象分配内存,初始化也就是代码:new Object()。
上面的流程就是你自己写好的代码扔给jvm去跑,跑完就over了,jvm关闭,你的程序也停止了。
相当于这些代码都是写 "死" 的,但是在运行时要使用则需要利用反射通过包名灵活地加载某些类。
二、反射的组件
实现反射的一些主要类都处于 java.lang
包下:
- Class:代表一个类;
- Field:代表类的成员变量(属性);
- Method:代表类的方法;
- Constructor:代表构造器(构造函数、构造方法);
- Array:提供动态创建数组,以及访问数组元素的方法;
- Package:存放路径,包名;
接下来看一下怎么获取这些组件。
类对象 Class
- 通过类型获取;
Class c = String.class;
- 通过对象获取:
Class c = o.getClass();
名称 | 作用 |
---|---|
getName() | 返回String形式的该类的名称。 |
newInstance() | 根据某个Class对象产生其对应类的实例,它调用的是此类的默认构造方法(没有默认无参构造器会报错) |
getClassLoader() | 返回该Class对象对应的类的类加载器。 |
getSuperClass() | 返回某子类所对应的直接父类所对应的Class对象 |
isArray() | 判定此Class对象所对应的是否是一个数组对象 |
getComponentType() | 如果当前类表示一个数组,则返回表示该数组组件的 Class 对象,否则返回 null。 |
getConstructor(Class[]) | 返回当前 Class 对象表示的类的指定的公有构造子对象。 |
getConstructors() | 返回当前 Class 对象表示的类的所有公有构造子对象数组。 |
getDeclaredConstructor(Class[]) | 返回当前 Class 对象表示的类的指定已说明的一个构造子对象。 |
getDeclaredConstructors() | 返回当前 Class 对象表示的类的所有已说明的构造子对象数组。 |
getDeclaredField(String) | 返回当前 Class 对象表示的类或接口的指定已说明的一个域对象。 |
getDeclaredFields() | 返回当前 Class 对象表示的类或接口的所有已说明的域对象数组。 |
getDeclaredMethod(String, Class[]) | 返回当前 Class 对象表示的类或接口的指定已说明的一个方法对象。 |
getDeclaredMethods() | 返回 Class 对象表示的类或接口的所有已说明的方法数组。 |
getField(String) | 返回当前 Class 对象表示的类或接口的指定的公有成员域对象。 |
getFields() | 返回当前 Class 对象表示的类或接口的所有可访问的公有域对象数组。 |
getInterfaces() | 返回当前对象表示的类或接口实现的接口。 |
getMethod(String, Class[]) | 返回当前 Class 对象表示的类或接口的指定的公有成员方法对象。 |
getMethods() | 返回当前 Class 对象表示的类或接口的所有公有成员方法对象数组,包括已声明的和从父类继承的方法。 |
isInstance(Object) | 此方法是 Java 语言 instanceof 操作的动态等价方法。 |
isInterface() | 判定指定的 Class 对象是否表示一个接口类型 |
isPrimitive() | 判定指定的 Class 对象是否表示一个 Java 的基类型。 |
newInstance() | 创建类的新实例 |
构造器 Constructor
首先使用 Class
对象获取 Constructor
:
- getConstructors()
- getConstructor(Class<?>…parameterTypes)
- getDeclaredConstructors()
- getDeclaredConstructor(Class<?>...parameterTypes)
然后就可以使用 Constructor
的下列方法:
名称 | 作用 |
---|---|
isVarArgs() | 查看该构造方法是否允许带可变数量的参数,如果允许,返回 true,否则返回false |
getParameterTypes() | 按照声明顺序以 Class 数组的形式获取该构造方法各个参数的类型 |
getExceptionTypes() | 以 Class 数组的形式获取该构造方法可能抛出的异常类型 |
newInstance(Object … initargs) | 通过该构造方法利用指定参数创建一个该类型的对象,如果未设置参数则表示采用默认无参的构造方法 |
setAccessiable(boolean flag) | 如果该构造方法的权限为 private,默认为不允许通过反射利用 netlnstance()方法创建对象。如果先执行该方法,并将入口参数设置为 true,则允许创建对象 |
getModifiers() | 获得可以解析出该构造方法所采用修饰符的整数 |
Modifier 用来获取修饰符信息,比如 isStatic(int mod)
返回是否静态、isPublic(int mod)
是否公共。不只是构造器类型含有此方法,函数、属性都有此方法。
方法 Method
名称 | 作用 |
---|---|
getName() | 获取该方法的名称 |
getParameterType() | 按照声明顺序以 Class 数组的形式返回该方法各个参数的类型 |
getReturnType() | 以 Class 对象的形式获得该方法的返回值类型 |
getExceptionTypes() | 以 Class 数组的形式获得该方法可能抛出的异常类型 |
invoke(Object obj,Object...args) | 利用 args 参数执行指定对象 obj 中的该方法,返回值为 Object 类型 |
isVarArgs() | 查看该方法是否允许带有可变数量的参数,如果允许返回 true,否则返回 false |
getModifiers() | 获得可以解析出该方法所采用修饰符的整数 |
属性、字段 Field
名称 | 作用 |
---|---|
getName() | 获得该成员变量的名称 |
getType() | 获取表示该成员变量的 Class 对象 |
get(Object obj) | 获得指定对象 obj 中成员变量的值,返回值为 Object 类型 |
set(Object obj, Object value) | 将指定对象 obj 中成员变量的值设置为 value |
getlnt(0bject obj) | 获得指定对象 obj 中成员类型为 int 的成员变量的值 |
setlnt(0bject obj, int i) | 将指定对象 obj 中成员变量的值设置为 i |
setFloat(Object obj, float f) | 将指定对象 obj 中成员变量的值设置为 f |
getBoolean(Object obj) | 获得指定对象 obj 中成员类型为 boolean 的成员变量的值 |
setBoolean(Object obj, boolean b) | 将指定对象 obj 中成员变量的值设置为 b |
getFloat(Object obj) | 获得指定对象 obj 中成员类型为 float 的成员变量的值 |
setAccessible(boolean flag) | 此方法可以设置是否忽略权限直接访问 private 等私有权限的成员变量 |
getModifiers() | 获得可以解析出该方法所采用修饰符的整数 |
数组相关 Array
名称 | 作用 |
---|---|
newInstance(Class<?> componentType, int length) | 创建一个 componentType 类型的数组,设定长度 |
getLength(Object array) | 获取数组长度 |
get(Object array, int index) | 获取数组指定下标位置的数据 |
set(Object array, int index, Object value) | 设置数组指定下标位置的数据 |
三、使用
获取组件
接下来写一个简单的小例子来看下反射的使用,首先准备一个 Student
类:
package com.sky.test.ref;
public class Student {
private int privateAge;
public int publicAge;
public void publicStudy(){
}
private void privateStudy(){
}
}
很简单的类,拥有一个私有、一个公共属性,一个私有、一个公共方法,接下来使用反射获取这些信息。
public static void main(String[] args) {
try {
// 根据包名找到类型
Class<?> classType = Class.forName("com.sky.test.ref.Student");
// 获取所有属性
Field[] fields = classType.getFields();
Field[] fieldsD = classType.getDeclaredFields();
for (Field f : fields) {
System.out.println("getFields 获取到的属性:" + f.getName());
// getFields 获取到的属性:publicAge
}
for (Field f : fieldsD) {
System.out.println("getDeclaredFields 获取到的属性:" + f.getName());
// getDeclaredFields 获取到的属性:privateAge
// getDeclaredFields 获取到的属性:publicAge
}
// 获取该类的所有方法
Method[] methods = classType.getMethods();
Method[] methodsD = classType.getDeclaredMethods();
for (Method m : methods) {
System.out.println("getMethods 获取到的方法:" + m.getName());
// getMethods 获取到的方法:publicStudy
// getMethods 获取到的方法:wait
// getMethods 获取到的方法:wait
// getMethods 获取到的方法:wait
// getMethods 获取到的方法:equals
// getMethods 获取到的方法:toString
// getMethods 获取到的方法:hashCode
// getMethods 获取到的方法:getClass
// getMethods 获取到的方法:notify
// getMethods 获取到的方法:notifyAll
}
for (Method m : methodsD) {
System.out.println("getDeclaredMethods 获取到的方法:" + m.getName());
// getDeclaredMethods 获取到的方法:publicStudy
// getDeclaredMethods 获取到的方法:privateStudy
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
-
Class.forName()
方法传入包名,以找到想要使用的类,找不到会抛ClassNotFoundException
异常; -
Class
的getFields()
方法:获取所有 公共的(public) 的属性,包含父类的 public 属性(但不包含父类非 public 属性); -
Class
的getDeclaredFields()
方法:获取本类所有的属性,但不包含父类的属性; -
Class
的getMethods()
方法:获取所有 公共的(public) 的方法,包含父类的 public 方法(但不包含父类非 public 方法); -
Class
的gettDeclaredMethods()
方法:获取本类所有的方法,但不包含父类的方法;
简单使用
已经成功获取到这些方法和属性了,接下来就是使用通过反射获取到的属性和方法了。但是首先要做的是能把修改的信息展示出来,把 Student
类简单修改下:
public class Student extends SuperStudent {
private int privateAge;
public int publicAge;
public void publicStudy(int hour) {
System.out.println("设置年龄为" + publicAge);
System.out.println("学习了" + hour + "小时");
}
private void privateStudy() {
}
}
只是给 publicStudy()
方法增加了 int 类型的参数,并打印了两个字段:publicAge、hour
。接下来就可以使用反射来修改里面的信息了。
public static void main(String[] args) {
try {
// 1. 根据包名找到类型
Class<?> classType = Class.forName("com.sky.test.ref.Student");
// 创建实例,修改该对象的信息
Object o = classType.newInstance();
// 2. 获取 年龄 字段
Field field = classType.getDeclaredField("publicAge");
// 设置年龄 publicAge 属性:第一个参数为要修改的对象,第二个参数为数值
field.set(o, 1);
// 3. 获取 publicStudy 方法,第二个参数传入参数类型
Method method = classType.getDeclaredMethod("publicStudy", int.class);
// 调用 publicStudy 方法
method.invoke(o, 3);
} catch (ClassNotFoundException
| NoSuchFieldException
| NoSuchMethodException | IllegalAccessException
| InstantiationException | InvocationTargetException
e) {
e.printStackTrace();
}
}
- 首先要做的是找到该类型,并创建该类型的对象。修改数据首先得有对象吧,然后内存空间有它的地方吧,不然修改啥。
- 然后就是获取字段了,我们要做的是修改年龄
publicAge
字段:获取该字段的Field
对象,使用set()
传入 对象 和要修改的值,这样就 ok 了。 - 类似的方法获取
publicStudy()
方法,调用invoke()
方法传参激活打印。特别注意: 第二个参数是当前方法的参数类型,如果传的不对直接给你报NoSuchMethodException
。而且注意int.class
和Integer.class
竟然不是一个东西,传Integer
还是会报错...
这样就完成了普通的使用。
特别注意:权限问题
反射使用引入了权限机制,私有的属性已经方法直接反射调用的话会抛出异常。
Field fieldPrivate = classType.getDeclaredField("privateAge");
fieldPrivate.set(o,2); // 会抛出 java.lang.IllegalAccessException 异常
如果想要使用的话,需要设置属性或方法等的访问权限:
Field fieldPrivate = classType.getDeclaredField("privateAge");
fieldPrivate.setAccessible(true);
fieldPrivate.set(o,2);
注解使用
注解一般也是利用反射来使用的,大概过程就是利用反射获取 Class
类型上面的注解,然后执行相应逻辑。
之前写过一篇文章有记录,在此不再赘述。
远程方法运用反射
- 服务端:HelloService
- 客户端:SimpleClient
- 通信信息:Call
客户端生成 Call 对象,指定要调用的类、对象以及方法参数等信息,通过流的形式发生给服务端。
public class Call implements Serializable {
private static final long serialVersionUID = 6659953547331194808L;
private String className; // 表示类名或接口名
private String methodName; // 表示方法名
private Class[] paramTypes; // 表示方法参数类型
private Object[] params; // 表示方法参数值
// 表示方法的执行结果
// 如果方法正常执行,则result为方法返回值,如果方法抛出异常,那么result为该异常。
private Object result;
...
}
服务端处理过将结果再以流的形式返回给客户端。
反射操作数组
需求: 创建一个数组,通过反射获取数组的类型,然后创建一个新长度的新数组,拷贝旧数组的内容。
为了看出效果,首先为 Student
类增加一个静态的创建方法:
public class Student extends SuperStudent {
public int publicAge;
...
public static Student newInstance(){
Student student = new Student();
student.publicAge = 8;
return student;
}
}
使用此方法创建实例,给内部的年龄字段一个初始值 8。接下来就创建一个 Student 的数组,并利用反射进行创建和复制。
public static void main(String[] args) {
// 1. 创建数组,里面存放一个 Student 对象
Student[] students = new Student[]{Student.newInstance()};
// 2. 获取元素类型
Class<?> classType = students.getClass().getComponentType();
// 3. 创建新数组
Object[] objects = (Object[]) Array.newInstance(classType, Array.getLength(students));
// 4. 进行 copy:原数组,拷贝起始,新数组,拷贝起始,拷贝长度
System.arraycopy(students, 0, objects, 0, Array.getLength(students));
// 5. 从新数组获取第一个元素
Student s = (Student)Array.get(objects, 0);
System.out.println("拷贝完成,第一个数据年龄: " + s.publicAge);
// 拷贝完成,第一个数据年龄: 8
}
需要注意的是一些方法的使用:
getClass()
和getComponentType()
的区别:
getClass()
用于获取对象的类型,比如这里获取数组的是Student[]
类型;
getComponentType()
获取数组元素类型,比如这里获取到的是Student
类型。Array.newInstance()
可以帮助我们动态创建数组,可以从别的地方传递来类型和长度,即可完成创建。
反射操作泛型
知识储备:
-
Type
:所有类型的公共父接口,Class
就是Type
的子类之一,以下是该接口的子类型:-
ParameterizedType
:参数化类型,可以理解为泛型; -
TypeVariable
:类型变量,也就是平时定义的T t、K k
等类似的变量; -
GenericArrayType
:泛型数组类型,T[]
这种类型; -
WildcardType
:通配符类型,<?>, <? Extends String>
这种。
-
小栗子:
依旧是使用上面的 Student
类,给它增加一个包含泛型的方法:
public void setStudents(List<Student> list){
}
接下来就是获取泛型:
public static void main(String[] args) {
try {
Method method = Student.class.getMethod("setStudents", List.class);
// 获取方法参数类型
Type[] t = method.getGenericParameterTypes();
for (Type type : t) {
System.out.println("参数类型:" + type);// 泛型类型:java.util.List<com.sky.test.ref.Student>
if (type instanceof ParameterizedType) {
Type[] real = ((ParameterizedType) type).getActualTypeArguments();
for (Type genericType : real) {
System.out.println("泛型类型:" + genericType);
// 泛型类型:class com.sky.test.ref.Student
}
}
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
这只是利用反射获取泛型的一个很小的例子,可以使用不同的方法获取 接口、类等的参数、返回值中的泛型。
说起来为什么会把泛型擦除掉,是因为泛型本来不是 Java 中的东西(可能是抄来的)。如果要把泛型加入字节码,就需要修改字节码指令集。我们都知道新增简单修改难,这字节码指令用了多少年了,要改基本不肯能了。
所以把泛型擦除之后呢,又需要一些类型来表示到底是哪种泛型,于是就有了上面那些 Type 的子接口。
四、反射影响性能
这里引用文章 大家都说 Java 反射效率低,你知道原因在哪里么 的结论:
- Method#invoke 方法会对参数做封装和解封操作
- 需要检查方法可见性
- 需要校验参数
- 反射方法难以内联
- JIT 无法优化
具体可以参考该文章,自此本文结束。
参考: