本文参考:微信公众号Java团长,简书。
一,简介
反射可以帮助我们在运行程序时候加载、使用编译期间完全未知的class,简单来说就是Java可以加载一个运行时候才得知名称的class,获得其完整的构造,并生成实例化对象,对其成员变量赋值,调用方法等等。
在具体的研发中,通过反射获取类的实例,大大提高系统的灵活性和扩展性,同时由于反射的性能较低,而且它极大的破坏了类的封装性(通过反射获取类的私有方法和属性),在大部分场景下并不适合使用反射,但是在大型的一些框架中,会大范围使用反射来帮助架构完善一些功能。
二,常用类
类 | 说明 |
---|---|
Class | 在反射中表示内存中的一个Java类,Class可以代表的实例类型包括,类和接口、基本数据类型、数组 |
Object | Java中所有类的超类 |
Constructor | 封装了类的构造函数的属性信息,包括访问权限和动态调用信息 |
Field | 提供类或接口的成员变量属性信息,包括访问权限和动态修改 |
Method | 提供类或接口的方法属性信息,包括访问权限和动态调用信息 |
Modifier | 封装了修饰属性, public、protected、static、final、synchronized、abstract等 |
三,原理
对于每一种类,Java虚拟机都会初始化出一个Class类型的实例,每当我们编写并且编译一个新创建的类就会产生一个对应Class对象,并且这个Class对象会被保存在同名.class文件里。当我们new一个新对象或者引用静态成员变量时,Java虚拟机(JVM)中的类加载器系统会将对应Class对象加载到JVM中,然后JVM再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。(ClassLoader,类加载机制)
比如创建编译一个Student类,那么,JVM就会创建一个Student对应Class类的Class实例,该Class实例保存了Student类相关的类型信息,包括属性,方法,构造方法等等,通过这个Class实例可以在运行时访问Student对象的属性和方法等。另外通过Class类还可以创建出一个新的Student对象。这就是反射能够实现的原因,可以说Class是反射操作的基础。
需要特别注意的是,每个class(注意class是小写,代表普通类)类,无论创建多少个实例对象,在JVM中都对应同一个Class对象。
四,用处
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
五,流程
public class Man {
private String name;
private int age;
public Man(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "The Name is " + this.name + " The age is " + this.age;
}
}
public class Main {
public static void main(String[] args) {
//1.获取Class对象的三种方式
Man man = new Man("LiSi", 11);
Class class1 = man.getClass();
Class class2 = Man.class;
Class class3 = null;
try {
class3 = Class.forName("com.xupt.willscorpio.javareflect.Man");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(class1.getName());
System.out.println(class2.getName());
System.out.println(class3.getName());
//2.通过反射新建一个Man对象
Class class4 = Man.class;
Constructor constructor = null;
try {
constructor = class4.getConstructor(String.class, int.class);
Man newMan = (Man) constructor.newInstance("WangWu", 99);
System.out.println(newMan.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们有三种获取Class对象的方法:
1)Object.getClass()
通过对象实例获取对应Class对象
Man man = new Man("LiSi", 11);
Class class1 = man.getClass();
2)The .class Syntax
通过类的类型获取Class对象,基本类型同样可以使用这种方法
Class class2 = Man.class;
3)Class.forName()
通过类的全限定名获取Class对象, 基本类型无法使用此方法
class3 = Class.forName("com.xupt.willscorpio.javareflect.Man");
对于数组比较特殊
Class cDoubleArray = Class.forName("[D"); //相当于double[].class
Class cStringArray = Class.forName("[[Ljava.lang.String;"); //相当于String[][].class
基本类型和void 类型的Class对象也可以通过其包装类的TYPE字段获取
Class c = Double.TYPE; //等价于 double.class
Class c = Void.TYPE; //等价于 void.class
Class常用方法 | 含义 |
---|---|
getCanonicalName() | 获得类名 |
getModifiers() | 获得类限定符 |
getTypeParameters() | 获取类泛型信息 |
getGenericInterfaces() | 获取类实现的所有接口 |
getSuperclass() | 获取类继承树上的所有父类 |
getAnnotations() | 获取类的注解(只能获取到 RUNTIME 类型的注解) |
六,Field,Method,Constructor
1)Field :对应类变量
通过Field你可以访问给定对象的类变量,包括获取变量的类型、修饰符、注解、变量名、变量的值或者重新设置变量值,即使变量是private的。
Filed常用方法 | 含义 |
---|---|
getDeclaredField(String name) | 获取指定的变量(只要是声明的变量都能获得,包括private) |
getField(String name) | 获取指定的变量(只能获得public的) |
getDeclaredFields() | 获取所有声明的变量(包括private) |
getFields() | 获取所有的public变量 |
public class Man {
public String name;
private int age;
public Man(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "The Name is " + this.name + " The age is " + this.age;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Man man = new Man("LiSi", 11);
Class class1 = man.getClass();
//1.获取Man对象的所有变量,包括私有
Field[] fields = class1.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName() + " " + field.getType() + " " + Modifier.toString(field.getModifiers()));
}
//2.为Man对象赋予新值
Field fieldName = class1.getField("name");
Field fieldAge = class1.getDeclaredField("age");
System.out.println(man.toString());
fieldName.set(man, "LiYunLong");
fieldAge.setAccessible(true);
fieldAge.set(man, 38);
System.out.println(man.toString());
}
}
我们用到了setAccessible()方法,因为age是private,Java运行时会进行访问权限检查,private类型的变量无法进行直接访问,但是反射包的AccessibleObject类为我们提供了一个方法 setAccessible(boolean flag),该方法的作用就是可以取消 Java 语言访问权限检查。所以任何继承AccessibleObject的类的对象都可以使用该方法取消 Java 语言访问权限检查。(final类型变量也可以通过这种办法访问)
Field,Method和Constructor都是继承AccessibleObject。
2)Method :对应类方法
常用方法 | 含义 |
---|---|
getDeclaredMethod(String name, Class<?>… parameterTypes) | 根据方法名获得指定的方法, 参数name为方法名,参数parameterTypes为方法的参数类型,如 getDeclaredMethod(“eat”, String.class) |
getMethod(String name, Class<?>… parameterTypes) | 根据方法名获取指定的public方法,其它同上 |
getDeclaredMethods() | 获取所有声明的方法 |
getMethods() | 获取所有的public方法 |
获取方法返回类型
getReturnType() 获取目标方法返回类型对应的Class对象
getGenericReturnType() 获取目标方法返回类型对应的Type对象
这两个方法的区别
1)getReturnType()返回类型为Class,getGenericReturnType()返回类型为Type; Class实现Type。
返回值为普通简单类型如Object, int, String等,getGenericReturnType()返回值和getReturnType()一样
例如 public String function1()
那么各自返回值为:
getReturnType() : class java.lang.String
getGenericReturnType() : class java.lang.String
2)返回值为泛型
例如public T function2()
那么各自返回值为:
getReturnType() : class java.lang.Object
getGenericReturnType() : T
3)返回值为参数化类型
例如public Class function3()
那么各自返回值为:
getReturnType() : class java.lang.Class
getGenericReturnType() : java.lang.Class<java.lang.String>
反射中所有形如getGenericXXX()的方法规则都与上面所述类似。
反射通过Method的invoke()方法来调用目标方法。第一个参数为需要调用的目标类对象,如果方法为static的,则该参数为null。后面的参数都为目标方法的参数值,顺序与目标方法声明中的参数顺序一致。
public class Man {
public String name;
private int age;
public Man(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private int getAge() {
return age;
}
private void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "The Name is " + this.name + " The age is " + this.age;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Man man = new Man("LiSi", 11);
Class class1 = man.getClass();
//1.获取Man对象的所有方法,包括私有
Method[] methods = class1.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
//2.调用方法,改变字段
System.out.println(man.toString());
Method setName = class1.getMethod("setName", String.class);
setName.invoke(man, "ChuYunFei");
Method setAge = class1.getDeclaredMethod("setAge", int.class);
setAge.setAccessible(true); //因为是private,所以需要设置权限
setAge.invoke(man, 35);
System.out.println(man.toString());
}
}
我们通过调用setName()和setAge()方法改变了对象的值。
被调用的方法本身所抛出的异常在反射中都会以InvocationTargetException抛出。换句话说,反射调用过程中如果异常InvocationTargetException抛出,说明反射调用本身是成功的,因为这个异常是目标方法本身所抛出的异常。
3)Constructor :对应类构造函数
Constructor常用方法 | 含义 |
---|---|
getDeclaredConstructor(Class<?>… parameterTypes) | 获取指定构造函数,参数parameterTypes为构造方法的参数类型 |
getConstructor(Class<?>… parameterTypes) | 获取指定public构造函数,参数parameterTypes为构造方法的参数类型 |
getDeclaredConstructors() | 获取所有声明的构造方法 |
getConstructors() | 获取所有的public构造方法 |
在反射中有两种方法可以创建对象:
java.lang.reflect.Constructor.newInstance()
Class.newInstance()
一般来讲,我们优先使用第一种方法;那么这两种方法有什么区别?
1)Class.newInstance()仅可用来调用无参的构造方法;Constructor.newInstance()可以调用任意参数的构造方法。
2)Class.newInstance()会将构造方法中抛出的异常不作处理原样抛出;Constructor.newInstance()会将构造方法中抛出的异常都包装成InvocationTargetException抛出。
3)Class.newInstance()需要拥有构造方法的访问权限;Constructor.newInstance()可以通过setAccessible(true)方法绕过访问权限访问private构造方法。
Class class4 = Man.class;
Constructor constructor = null;
try {
constructor = class4.getConstructor(String.class, int.class);
Man newMan = (Man) constructor.newInstance("WangWu", 99);
System.out.println(newMan.toString());
} catch (Exception e) {
e.printStackTrace();
}
七,数组
数组类型:数组本质是一个对象,所以它也有自己的类型。
例如对于int[] intArray,数组类型为class [I。数组类型中的[个数代表数组的维度,例如[代表一维数组,[[代表二维数组;[后面的字母代表数组元素类型,I代表int,一般为类型的首字母大写(long类型例外,为J)。
class [B //byte类型一维数组
class [S //short类型一维数组
class [I //int类型一维数组
class [C //char类型一维数组
class [J //long类型一维数组,J代表long类型,因为L被引用对象类型占用了
class [F //float类型一维数组
class [D //double类型一维数组
class [Lcom.dada.Season //引用类型一维数组
class [[Ljava.lang.String //引用类型二维数组
数组常用方法 | 含义 |
---|---|
创建数组, 参数componentType为数组元素的类型,后面不定项参数的个数代表数组的维度,参数值为数组长度 | Array.newInstance(Class<?> componentType, int… dimensions) |
设置数组值,array为数组对象,index为数组的下标,value为需要设置的值 | Array.set(Object array, int index, int value) |
获取数组的值,array为数组对象,index为数组的下标 | Array.get(Object array, int index) |
public class Main {
public static void main(String[] args) throws Exception {
//1.通过反射生成一个二维数组
String[][] array = (String[][]) Array.newInstance(String.class, 2, 3);
array[0][0] = "a";
array[0][1] = "b";
array[0][2] = "c";
array[1][0] = "d";
array[1][1] = "e";
array[1][2] = "f";
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
System.out.println("i:" + i + " j:" + j + " " + array[i][j]);
}
}
//2.使用反射改变二维数组的值
Object row0 = Array.get(array, 0);
Object row1 = Array.get(array, 1);
Array.set(row0, 0, "One");
Array.set(row0, 1, "Two");
Array.set(row0, 2, "Three");
Array.set(row1, 0, "1");
Array.set(row1, 1, "2");
Array.set(row1, 2, "3");
for (int m = 0; m < array.length; m++) {
for (int n = 0; n < array[m].length; n++) {
System.out.println("m:" + m + " n:" + n + " " + array[m][n]);
}
}
System.out.println(array.getClass());
}
}
八,副作用
1)性能开销
反射涉及类型动态解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
2)安全限制
使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
3)内部曝光
由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用——代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
使用反射的一个原则:如果使用常规方法能够实现,那么就不要用反射。