反射的基本原理

综合编程 2018-06-24 阅读原文

『反射』就是指程序在运行时能够动态的获取到一个类的类型信息的一种操作。它是现代框架的灵魂,几尽所有的框架能够提供的一些自动化机制都是靠反射实现的,这也是为什么各类框架都不允许你覆盖掉默认的无参构造器的原因,因为框架需要以反射机制利用无参构造器创建实例。

总的来说,『反射』是很值得大家花时间学习的,尽管大部分人都很少有机会去手写框架,但是这将有助于你对于各类框架的理解。不奢求你通过本篇文章的学习对于『反射』能够有多么深层次的理解,但至少保证你了解『反射』的基本原理及使用。

Class 类型信息

之间介绍过虚拟机的类加载机制,其中我们提到过,每一种类型都会在初次使用时被加载进虚拟机内存的『方法区』中,包含类中定义的属性字段,方法字节码等信息。

Java 中使用类 java.lang.Class 来指向一个类型信息,通过这个 Class 对象,我们就可以得到该类的所有内部信息。而获取一个 Class 对象的方法主要有以下三种。

类名.class

这种方式就比较简单,只要使用类名点 class 即可得到方法区该类型的类型信息。例如:

Object.class;
Integer.class;
int.class;
String.class;
//等等

getClass 方法

Object 类有这么一个方法:

public final native Class getClass();

这是一个本地方法,并且不允许子类重写,所以理论上所有类型的实例都具有同一个 getClass 方法。具体使用上也很简单:

Integer integer = new Integer(12);
integer.getClass();

forName 方法

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 中有关获取字段属性的方法主要以下几个:

  • public Field[] getFields():返回该类型的所有 public 修饰的属性,包括父类的
  • public Field getField(String name):根据字段名称返回相应的字段
  • public Field[] getDeclaredFields():返回本类型中申明的所有字段,包含非 public 修饰的但不包含父类中的
  • public Field getDeclaredField(String name):同理

当然,一个 Field 实例包含某个类的一个属性的所有信息,包括字段名称,访问修饰符,字段类型。除此之外,Field 还提供了大量的操作该属性值的方法,通过传入一个类实例,就可以直接使用 Field 实例操作该实例的当前字段属性的值。

例如:

//定义一个待反射类
public class People {
    public String name;
}
Class 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 类也提供了四种方法来获取其中的方法属性:

  • public Method[] getMethods():返回所有的 public 方法,包括父类中的
  • public Method getMethod(String name, Class... parameterTypes):返回指定的方法
  • public Method[] getDeclaredMethods():返回本类申明的所有方法,包括非 public 修饰的,但不包括父类中的
  • public Method getDeclaredMethod(String name, Class... parameterTypes):同理

Method 抽象地代表了一个方法,同样有描述这个方法基本信息的字段和方法,例如方法名,方法的参数集合,方法的返回值类型,异常类型集合,方法的注解等。

除此之外的还有一个 invoke 方法用于间接调用其他实例的该方法,例如:

public class People {

    public void sayHello(){
        System.out.println("hello wrold ");
    }
}
Class cls = People.class;
Method sayHello = cls.getMethod("sayHello");
People people = new People();
sayHello.invoke(people);

程序输出:

hello wrold

反射构造器

对于 Constructor 来说,Class 类依然为它提供了四种获取实例的方法:

  • public Constructor[] getConstructors():返回所有 public 修饰的构造器
  • public Constructor[] getDeclaredConstructors():返回所有的构造器,无视访问修饰符
  • public Constructor
  • public Constructor

Constructor 本质上也是一个方法,只是没有返回值而已,所以内部的基本内容和 Method 是类似的,只不过 Constructor 类中有一个 newInstance 方法用于创建一个该 Class 类型的实例对象出来。

//最简单的一个反射创建实例的过程
Class 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 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 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,所有文章都将同步在公众号上。

博客园精华区

责编内容by:博客园精华区阅读原文】。感谢您的支持!

您可能感兴趣的

Starting / stopping a program with a JButt... So I have created a program which does the intended purpose of sending random keystrokes. Now I have implemented a very ...
浅谈安卓apk加固原理和实现 引言: 在安卓开发中,打包发布是开发的最后一个环节,apk是整个项目的源码和资源的结合体;对于懂点反编译原理的人可以轻松编译出apk的源码资源,并且可以修改资源代码、重新打包编译,轻轻松松变成自己的apk或者修改其中一部分窃取用户信息。...
命令行打APK实操全记录 介绍 本文基于ant命令创建工程使用android sdk提供的工具进行手动打包 打包基本流程 手动命令打包实操 后续学习gradle打包、python脚本打包及点击build As的工作过程等 图解...
Android8.0 Fragment变化之setTargetFragment 此问题源于一个FATAL异常: AndroidRuntime: FATAL EXCEPTION: main AndroidRuntime: Process: com.android.settings, PID: 3449 Android...
手把手教你建立一个Java游戏引擎 今天,让我们进入一个可以伸手触摸的世界吧。在这篇文章里,我们将从零开始快速完成一次第一人称探索。本文没有涉及复杂的数学计算,只用到了光线投射技术。你可能已经见识过这种技术了,比如《上古卷轴5 : 天际》、《毁灭公爵3D》。 ...