『反射』就是指程序在运行时可以动态的获取到一个类的类型信息的一种操做。它是现代框架的灵魂,几尽全部的框架可以提供的一些自动化机制都是靠反射实现的,这也是为何各种框架都不容许你覆盖掉默认的无参构造器的缘由,由于框架须要以反射机制利用无参构造器建立实例。java
总的来讲,『反射』是很值得你们花时间学习的,尽管大部分人都不多有机会去手写框架,可是这将有助于你对于各种框架的理解。不奢求你经过本篇文章的学习对于『反射』可以有多么深层次的理解,但至少保证你了解『反射』的基本原理及使用。git
之间介绍过虚拟机的类加载机制,其中咱们提到过,每一种类型都会在初次使用时被加载进虚拟机内存的『方法区』中,包含类中定义的属性字段,方法字节码等信息。github
Java 中使用类 java.lang.Class 来指向一个类型信息,经过这个 Class 对象,咱们就能够获得该类的全部内部信息。而获取一个 Class 对象的方法主要有如下三种。数组
类名.class安全
这种方式就比较简单,只要使用类名点 class 便可获得方法区该类型的类型信息。例如:微信
Object.class; Integer.class; int.class; String.class; //等等
getClass 方法框架
Object 类有这么一个方法:学习
public final native Class<?> getClass();
这是一个本地方法,而且不容许子类重写,因此理论上全部类型的实例都具备同一个 getClass 方法。具体使用上也很简单:code
Integer integer = new Integer(12); integer.getClass();
forName 方法component
forName 算是获取 Class 类型的一个最经常使用的方法,它容许你传入一个全类名,该方法会返回方法区表明这个类型的 Class 对象,若是这个类尚未被加载进方法区,forName 会先进行类加载。
public static Class<?> forName(String className) { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
因为方法区 Class 类型信息由类加载器和类全限定名惟一肯定,因此想要去找这么一个 Class 就必须提供类加载器和类全限定名,这个 forName 方法默认使用调用者的类加载器。
固然,Class 类中也有一个 forName 重载,容许你传入类加载器和类全限定名来匹配方法区类型信息。
public static Class<?> forName(String name, boolean initialize, ClassLoader loader){ //..... }
至此,经过这些方法你能够获得任意类的类型信息,该类的全部字段属性,方法表等信息均可以经过这个 Class 对象进行获取。
Class 中有关获取字段属性的方法主要如下几个:
固然,一个 Field 实例包含某个类的一个属性的全部信息,包括字段名称,访问修饰符,字段类型。除此以外,Field 还提供了大量的操做该属性值的方法,经过传入一个类实例,就能够直接使用 Field 实例操做该实例的当前字段属性的值。
例如:
//定义一个待反射类 public class People { public String name; }
Class<People> cls = People.class; Field name = cls.getField("name"); People people = new People(); name.set(people,"hello"); System.out.println(people.name);
程序会输出:
hello
其实也很简单,set 方法会检索 People 对象是否具备一个 name 表明的字段,若是有将字符串 hello 赋值给该字段便可。
整个 Field 类主要由两大部分组成,第一部分就是有关该字段属性的描述信息,例如名称,类型,外围类 Class 对象等,第二部分就是大量的 get 和 set 方法用于间接操做任意的外围类实例的当前属性值。
一样的,Class 类也提供了四种方法来获取其中的方法属性:
Method 抽象地表明了一个方法,一样有描述这个方法基本信息的字段和方法,例如方法名,方法的参数集合,方法的返回值类型,异常类型集合,方法的注解等。
除此以外的还有一个 invoke 方法用于间接调用其余实例的该方法,例如:
public class People { public void sayHello(){ System.out.println("hello wrold "); } }
Class<People> cls = People.class; Method sayHello = cls.getMethod("sayHello"); People people = new People(); sayHello.invoke(people);
程序输出:
hello wrold
对于 Constructor 来讲,Class 类依然为它提供了四种获取实例的方法:
Constructor 本质上也是一个方法,只是没有返回值而已,因此内部的基本内容和 Method 是相似的,只不过 Constructor 类中有一个 newInstance 方法用于建立一个该 Class 类型的实例对象出来。
//最简单的一个反射建立实例的过程 Class<People> cls = People.class; Constructor c = cls.getConstructor(); People p = (People) c.newInstance();
以上,咱们简单的介绍了反射的基本使用状况,但都很基础,下面咱们看看反射和一些稍微复杂的类型结合使用的状况,例如:数组,泛型,注解等。
反射与数组
咱们都知道,数组是一种特殊的类型,它本质上由虚拟机在运行时动态生成,因此在反射这种类型的时候会稍有不一样。
public native Class<?> getComponentType();
Class 中有这么一个方法,该方法将返回数组 Class 实例元素的基本类型。只有当前的 Class 对象表明的是一个数组类型的时候,该方法才会返回数组的元素实际类型,其余的任什么时候候都会返回 null。
固然,有一点须要注意下,表明数组的这个由虚拟机动态建立的类型,它直接继承的 Object 类,而且全部有关数组类的操做,好比为某个元素赋值或是获取数组长度的操做都直接对应一个单独的虚拟机数组操做指令。
一样也由于数组类直接由虚拟机运行时动态建立,因此你不可能从一个数组类型的 Class 实例中获得构造方法,编译器根本没机会为类生成默认的构造器。因而你也不能以常规的方法经过 Constructor 来建立一个该类的实例对象。
若是你非要尝试使用 Constructor 来建立一个新的实例的话,那么运行时程序将告诉你没法匹配一个构造器。像这样:
Class<String[]> cls = String[].class; Constructor constructor = cls.getConstructor(); String[] strs = (String[]) constructor.newInstance();
控制台输出:
告诉你,Class 实例中根本找不到一个无参的构造器。那么难道咱们就没有办法来动态建立一个数组了吗?
固然不是,Java 中有一个类 java.lang.reflect.Array 提供了一些静态的方法用于动态的建立和获取一个数组类型。
//建立一个一维数组,componentType 为数组元素类型,length 数组长度 public static Object newInstance(Class<?> componentType, int length) //可变参数 dimensions,指定多个维度的单维度长度 public static Object newInstance(Class<?> componentType, int... dimensions)
这是我认为 Array 类中最重要的两个方法,固然了 Array 类中还有一些其它方法用于获取指定数组的指定位置元素,这里再也不赘述了。
彻底是由于数组这种类型并非由常规的编译器编译生成,而是由虚拟机动态建立的,因此想要经过反射的方式实例化一个数组类型是得依赖 Array 这个类型的相关 newInstance 方法的。
反射与泛型
泛型是 Java 编译器范围内的概念,它可以在程序运行以前提供必定的安全检查,而反射是运行时发生的,也就是说若是你反射调用一个泛型方法,实际上就绕过了编译器的泛型检查了。咱们看一段代码:
ArrayList<Integer> list = new ArrayList<>(); list.add(23); //list.add("fads");编译不经过 Class<?> cls = list.getClass(); Method add = cls.getMethod("add",Object.class); add.invoke(list,"hello"); System.out.println(list.get(1));
最终你会发现咱们从整型容器中取出一个字符串,由于虚拟机只管在运行时从方法区找到 ArrayList 这个类的类型信息并解析出它的 add 方法,接着执行这个方法。
它不像通常的方法调用,调用以前编译器会检测这个方法存在不存在,参数类型是否匹配等,因此没了编译器的这层安全检查,反射地调用方法更容易遇到问题。
除此以外,以前咱们说过的泛型在通过编译期以后会被类型擦除,但实际上表明该类型的 Class 类型信息中是保存有一些基本的泛型信息的,这一点咱们能够经过反射获得。
这里再也不带你们一块儿去看了,Class ,Field 和 Method 中都是有相关方法能够获取类或者方法在定义的时候所使用到的泛型类名名称。注意这里说的,只是名称,相似 E、V 这样的东西。
文章中的全部代码、图片、文件都云存储在个人 GitHub 上:
(https://github.com/SingleYam/overview_java)
欢迎关注微信公众号:OneJavaCoder,全部文章都将同步在公众号上。