反射
-
设计背景
反射是为了能够动态的加载一个类,动态的调用一个方法,动态的访问一个属性等动态要求而设计的。它的出发点就在于JVM会为每个类创建一个java.lang.Class类的实例,通过该对象可以获取这个类的信息,然后通过使用java.lang.reflect包下得API以达到各种动态需求。反射的初衷不是方便你去创建一个对象,而是让你在写代码的时候可以更加灵活,降低耦合,提高代码的自适应能力。
-
机制
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
-
功能
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有
- 在运行时调用任意一个对象的方法
- 生成动态代理
-
组成
由于反射最终也必须有类参与,因此反射的组成一般有下面几个方面组成:
- java.lang.Class.java:类对象
- java.lang.reflect.Constructor.java:类的构造器对象
- java.lang.reflect.Method.java:类的方法对象
- java.lang.reflect.Field.java:类的属性对象
-
加载过程
根据虚拟机的工作原理,一般情况下,类需要经过:加载->验证->准备->解析->初始化->使用->卸载这个过程,如果需要反射的类没有在内存中,那么首先会经过加载这个过程,并在在内存中生成一个class对象,有了这个class对象的引用,就可以发挥开发者的想象力,做自己想做的事情了
-
应用
- 在Java和Android开发中,一般情况下下面几种场景会用到反射机制
- 需要访问隐藏属性或者调用方法改变程序原来的逻辑,这个在开发中很常见的,由于一些原因,系统并没有开放一些接口出来,这个时候利用反射是一个有效的解决方法
- 自定义注解,注解就是在运行时利用反射机制来获取的。
- 在开发中动态加载类,比如在Android中的动态加载解决65k问题等等,模块化和插件化都离不开反射,离开了反射寸步难行。
- 应用实例
官方解释:反射被广泛地用于那些需要在运行时检测或修改程序行为的程序中。这是一个相对高级的特性,只有那些语言基础非常扎实的开发者才应该使用它。如果能把这句警示时刻放在心里,那么反射机制就会成为一项强大的技术,可以让应用程序做一些几乎不可能做到的事情。- 逆向代码(反编译)
- Retrofit(与注解相结合的框架)
- EventBus(单纯的反射机制应用框架)
- Gson (动态生成类框架)
- 在Java和Android开发中,一般情况下下面几种场景会用到反射机制
-
使用
正常创建类
Phone phone = new Phone(); //直接初始化,「正射」 phone.setPrice(4);
用反射创建类
Class clz = Class.forName("com.xxp.api.Phone"); Method setPriceMethod = clz.getMethod("setPrice", int.class); Constructor phoneConstructor = clz.getConstructor(); Object phoneObj = phoneConstructor.newInstance(); setPriceMethod.invoke(phoneObj, 4);
上面两段代码的执行结果,其实是完全一样的。但是其思路完全不一样,第一段代码在未运行时就已经确定了要运行的类(Phone),而第二段代码则是在运行时通过字符串值才得知要运行的类(com.xxp.reflect.Phone)。
- 基础方法
Person person = new Person(); Class<?> class1 = Person.getClass(); boolean isPrimitive = class1.isPrimitive();//判断是否是基础类型 boolean isArray = class1.isArray();//判断是否是集合类 boolean isAnnotation = class1.isAnnotation();//判断是否是注解类 boolean isInterface = class1.isInterface();//判断是否是接口类 boolean isEnum = class1.isEnum();//判断是否是枚举类 boolean isAnonymousClass = class1.isAnonymousClass();//判断是否是匿名内部类 boolean isAnnotationPresent = class1.isAnnotationPresent(Deprecated.class);//判断是否被某个注解类修饰 String className = class1.getName();//获取class名字 包含包名路径 Package aPackage = class1.getPackage();//获取class的包信息 String simpleName = class1.getSimpleName();//获取class类名 int modifiers = class1.getModifiers();//获取class访问权限 Class<?>[] declaredClasses = class1.getDeclaredClasses();//内部类 Class<?> declaringClass = class1.getDeclaringClass();//外部类
-
获取构造方法
//获取所有的构造方法 public Constructor<?>[] getConstructors() //获取public的 public Constructor<?>[] getDeclaredConstructors() //获取所有的 //获取单个构造方法 public Constructor getConstructor(Class<?>... parameterTypes) //获取指定声明的public构造函数 public Constructor getDeclaredConstructor(Class<?>... parameterTypes) //获取指定声明的所有构造函数
-
方法关键字
getDeclareMethods() 获取所有的方法 getMethods() 获取所有public方法,包括父类的方法 getReturnType() 获取方法的返回值类型 getParameterTypes() 获取方法的传入参数类型 getDeclareMethod("方法名,参数类型.class,....") 获得特定的方法 getMethod("方法名,参数类型.class,....") 返回该Class对象对应类的、带指定形参列表的public方法
-
成员变量
//获取所有成员变量 getDeclaredFields() getFields() //获取特定的成员变量 getDeclaredField(参数类型.class,....) getField(参数类型.class,....)
举个🌰:
Field[] allFields = class1.getDeclaredFields();//获取class对象的所有属性 Field[] publicFields = class1.getFields();//获取class对象的public属性 Field ageField = class1.getDeclaredField("age");//获取class指定属性 Field desField = class1.getField("des");//获取class指定的public属性
getDeclaredField()和getField()的区别:
- getDeclaredField() 获取的是 Class 中被 private 修饰的属性。
- getField() 方法获取的是非私有属性,并且 getField() 在当前 Class 获取不到时会向祖先类获取。
-
父类和父接口
getSuperclass() 获取某类的父类 getInterfaces() 获取某类实现的接口
- 基础方法
-
深入了解反射
-
setAccessible暴力访问
一般情况下,我们并不能对类的私有字段进行操作,利用反射也不例外,但有的时候,例如要序列化的时候,我们又必须有能力去处理这些字段,这时候,我们就需要调用AccessibleObject上的setAccessible()方法来允许这种访问,而由于反射类中的Field,Method和Constructor继承自AccessibleObject,因此,通过在这些类上调用setAccessible()方法,我们可以实现对这些字段的操作。Field gradeField = clazz.getDeclaredField("code"); // 如果是 private 或者 package 权限的,一定要赋予其访问权限 gradeField.setAccessible(true); Method goMethod = clazz.getDeclaredMethod("getMethod"); // 赋予访问权限 goMethod.setAccessible(true);
-
invoke()方法执行
Method 调用 invoke() 的时候,存在许多细节:- invoke() 方法中第一个参数 Object 实质上是 Method 所依附的 Class 对应的类的实例,如果这个方法是一个静态方法,那么 ojb 为 null,后面的可变参数 Object 对应的自然就是参数。
- invoke() 返回的对象是 Object,所以实际上执行的时候要进行强制转换
- 在对Method调用invoke()的时候,如果方法本身会抛出异常,那么这个异常就会经过包装,由Method统一抛InvocationTargetException。而通过InvocationTargetException.getCause() 可以获取真正的异常。
-
-
泛型和反射
public class GenericTest{ private Map<String , Integer> score; public static void main(String[] args) throws Exception{ Class<GenericTest> clazz = GenericTest.class; Field f = clazz.getDeclaredField("score"); // 直接使用getType()取出Field类型只对普通类型的Field有效 Class<?> a = f.getType(); // 下面将看到仅输出java.util.Map System.out.println("score的类型是:" + a); // 获得Field实例f的泛型类型 Type gType = f.getGenericType(); // 如果gType类型是ParameterizedType对象 if(gType instanceof ParameterizedType){ // 强制类型转换 ParameterizedType pType = (ParameterizedType)gType; // 获取原始类型 Type rType = pType.getRawType(); System.out.println("原始类型是:" + rType); // 取得泛型类型的泛型参数 Type[] tArgs = pType.getActualTypeArguments(); System.out.println("泛型类型是:"); for (int i = 0; i < tArgs.length; i++) { System.out.println("第" + i + "个泛型类型是:" + tArgs[i]); } } else{ System.out.println("获取泛型类型出错!"); } } }
- 使用反射来获取泛型信息
-
通过指定类对应的Class对象,可以获得该类里包含的所有Field,不管该Field是使用private修饰,还是使用public修饰。
Class<GenericTest> clazz = GenericTest.class; Field f = clazz.getDeclaredField("score");
-
获得了Field对象后,就可以很容易的获得该Field的数据类型,即使用如下代码即可获得指定Field的类型。
// 获取 Field 对象 f 的类型 Class<?> a = f.getType();
但上面这种方式只对普通类型的Field有效,如果该Field的类型是有泛型限制的类型,如Map<String,Integer>类型,则不能准确地获得该Field的泛型参数,我们可以使用如下方法来获取指定Field的类型。
// 获得 Field 实例的泛型类型 Type type = f.getGenericType();
-
然后将Type对象强制类型转换为ParameterizedType对象,ParameterizedType代表被参数化的类型,也就是增加了泛型限制的类型,ParameterizedType类提供了如下两个方法,但是
- getRawType():返回没有泛型信息的原始类型
- getActualTypeArguments():返回泛型参数的类型
-
完整代码如下
public class GenericTest{ private Map<String , Integer> score; public static void main(String[] args) throws Exception{ Class<GenericTest> clazz = GenericTest.class; Field f = clazz.getDeclaredField("score"); // 直接使用getType()取出Field类型只对普通类型的Field有效 Class<?> a = f.getType(); // 下面将看到仅输出java.util.Map System.out.println("score的类型是:" + a); // 获得Field实例f的泛型类型 Type gType = f.getGenericType(); // 如果gType类型是ParameterizedType对象 if(gType instanceof ParameterizedType){ // 强制类型转换 ParameterizedType pType = (ParameterizedType)gType; // 获取原始类型 Type rType = pType.getRawType(); System.out.println("原始类型是:" + rType); // 取得泛型类型的泛型参数 Type[] tArgs = pType.getActualTypeArguments(); System.out.println("泛型类型是:"); for (int i = 0; i < tArgs.length; i++) { System.out.println("第" + i + "个泛型类型是:" + tArgs[i]); } } else{ System.out.println("获取泛型类型出错!"); } } } //输出结果 //score 的类型是: interface java.util.Map //原始类型是: interface java.util.Map //泛型类型是: //第 0 个泛型类型是: class java.lang.String //第 1 个泛型类型是:class java.lang.Integer
-
- 使用反射来获取泛型信息
-
反射的适用场景
-
要理解反射的实用场景,我们需要先了解反射的利弊:
- 利:反射的好处很明显,概括起来就是:运行期类型的判断,动态类加载,写代码的时候可以更加灵活,降低耦合,提高代码的自适应能力
- 弊:
- 反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
- 使用反射技术要求程序必须在一个没有安全限制的环境中运行。
- 由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
-
了解了反射的利弊我们再来分析反射的适用场景:
- 基础框架
经典的就是在xml文件或者properties里面写好了配置,然后在Java类里面解析xml或properties里面的内容,得到一个字符串,然后用反射机制,根据这个字符串获得某个类的Class实例,这样就可以动态配置一些东西,不用每一次都要在代码里面去new或者做其他的事情,以后要改的话直接改配置文件,代码维护起来就很方便了,同时有时候要适应某些需求,Java类里面不一定能直接调用另外的方法,这时候也可以通过反射机制来实现。 - 当你做一个软件可以安装插件的功能,你连插件的类型名称都不知道,你怎么实例化这个对象呢?因为程序是支持插件的(第三方的),在开发的时候并不知道 。所以无法在代码中 New出来 ,但反射可以,通过反射,动态加载程序集,然后读出类,检查标记之后再实例化对象,就可以获得正确的类实例
- 在编码阶段不知道那个类名,要在运行期从配置文件读取类名, 这时候就没有办法硬编码new ClassName(),而必须用到反射才能创建这个对象.反射的目的就是为了扩展未知的应用。比如你写了一个程序,这个程序定义了一些接口,只要实现了这些接口的dll都可以作为插件来插入到这个程序中。那么怎么实现呢?就可以通过反射来实现。就是把dll加载进内存,然后通过反射的方式来调用dll中的方法。很多工厂模式就是使用的反射。
- 基础框架
-
尽量远离反射
在流行的库如Spring和Gson,Retrofit中,反射自然有其用武之地。不过在很多时候都不是一件好事,原因有很多,一般情况下建议不要使用反射。举个🌰:- 首先是代码可读性与工具支持。打开熟悉的IDE,寻找你的Java代码的内部依赖,很容易吧。现在,使用反射来替换掉你的代码然后再试一下,结果如何呢?如果通过反射来修改已经封装好的对象状态,那么结果将会变得更加不可控。比如下面Student为第三方库代码,这个时候如果别人更新setName方法名称,改为setStudentName,那么之前写的反射代码就会出现问题,而且不太好发现。
如果这样做就无法得到编译期的安全保证。就像上面这个示例一样,你会发现如果getDeclaredField()方法调用的参数输错了,那么只有在运行期才能发现。要知道的是,寻找运行期Bug的难度要远远超过编译期的Bug。public class Student { private String name; public Student() {} public void setName(String name) { this.name = name; } } Student student = new Student(); Method m = student.getClass().getMethod("setName", String.class); m.invoke(student, "张三");
- 首先是代码可读性与工具支持。打开熟悉的IDE,寻找你的Java代码的内部依赖,很容易吧。现在,使用反射来替换掉你的代码然后再试一下,结果如何呢?如果通过反射来修改已经封装好的对象状态,那么结果将会变得更加不可控。比如下面Student为第三方库代码,这个时候如果别人更新setName方法名称,改为setStudentName,那么之前写的反射代码就会出现问题,而且不太好发现。
-
-
延伸:Class与.class文档
- Java 在真正需要某个类时才会加载对应的.class文档,而非在程序启动时就加载所有类,因为大部分时候我们只需要用到应用程序部分资源,有选择地加载可以节省系统资源
- java.lang.Class 的实例代表 Java 应用程序运行时加载的 .class 文档,类、接口、Enum等编译过后,都会生成 .class 文档,所以 Class可以用来包含类、接口、Enum等信息
- Class 类没有公开的构造函数,实例是由 JVM 自动产生,每个 .class 文档加载时, JVM 会自动生成对应的 Class 对象
- Java 在真正需要类时才会加载.class文档,即在生成对象时才会加载.class文档。如果只是使用类声明了一个变量,此时并不会加载.class文档,而只是让编译程序检查对应的 .class 文档是否存在