反射是什么?官方文档上有这么一段介绍:
我来翻译一下:反射技术通常被用来检测和改变应用程序在 Java 虚拟机中的行为表现。它是一个相对而言比较高级的技术,通常它应用的前提是开发者本身对于 Java 语言特性有很强的理解的基础上。值得说明的是,反射是一种强有力的技术特性,因此可以使得应用程序突破一些藩篱,执行一些常规手段无法企及的目的。 我再通俗概括一下:反射是个很牛逼的功能,能够在程序运行时修改程序的行为。但反射是非常规手段,反射有风险,应用需谨慎。 相信,大部分同学会有稍微清晰一点的概念了。但这还不是我的目的所在。 我的目的是想,我如何向一个刚有一点点 Java 基础的初学者,或者是说毫无 Java 基础的门外汉解释清楚反射这样一种东西? 直接翻译官方文档,显然是不太行。因为那仍然是抽象的,所以,最好的方法仍然是通过类比或者是拟人,用生活场景中具体的事物与抽象的概念建立相关性。 把程序代码比作一辆车,因为 Java 是面向对象的语言,所以这样很容易理解,正常流程中,车子有自己的颜色、车型号、品牌这些属性,也有正常行驶、倒车、停泊这些功能操作。 正常情况下,我们需要为车子配备一个司机,然后按照行为准则规范行驶。 那么反射是什么呢?反射是非常规手段,正常行驶的时候,车子需要司机的驾驶,但是,反射却不需要,因为它就是车子的——自动驾驶。 因为,反射牛逼,又因为反射非常规,所以,它风险未知,需要开发者极强的把控力。而汽车中的自动驾驶技术现在是热门,但是特斯拉都出过故障,所以同样在汽车领域,自动驾驶技术也需要车厂家有极牛逼的风险把控能力,这个基础就是要遵从汽车本身的结构与交通规则,不能因为运用了自动驾驶技术的汽车就不叫做汽车了,应用了反射技术的代码就不叫做代码了。 自动驾驶需要遵守基础规则,同样反射也需要,下面的文章就是介绍反射技术应该遵守的规格与限制。 反射入口我们试想一下,如果自动驾驶要运用到一辆汽车之上,研发人员首先要拿到的是什么? 肯定是汽车的规格说明书。同样,反射如果要作用于一段 Java 代码上,那么它也需要拿到一本规格说明书,那么对于反射而言,这本规格说明书是什么呢? Class因为 Java 是面向对象的语言,基本上是以类为基础构造了整个程序系统,反射中要求提供的规格说明书其实就是一个类的规格说明书,它就是 Class。 注意的是 Class 是首字母大写,不同于 class 小写,class 是定义类的关键字,而 Class 的本质也是一个类,因为在 Java 中一切都是对象。 Class 就是一个对象,它用来代表运行在 Java 虚拟机中的类和接口。 把 Java 虚拟机类似于高速公路,那么 Class 就是用来描述公路上飞驰的汽车,也就是我前面提到的规格说明书。 Class 的获取反射的入口是 Class,但是反射中 Class 是没有公开的构造方法的,所以就没有办法像创建一个类一样通过 new 关键字来获取一个 Class 对象。 不过,不用担心,Java 反射中 Class 的获取可以通过下面 3 种方式。 1. 通过 Object.getClass()对于一个对象而言,如果这个对象可以访问,那么调用 值得注意的是,这种方法不适合基本类型如 int、float 等等。 2. 通过 .class 标识上面的例子中,Car 是一个类,car 是它的对象,通过 car.getClass() 就获取到了 Car 这个类的 Class 对象,也就是说通过一个类的实例的 getClass() 方法就能获取到它的 Class。如果不想创建这个类的实例的话,就需要通过 3. 通过 Class.forName() 方法有时候,我们没有办法创建一个类的实例,甚至没有办法用 Car.class 这样的方式去获取一个类的 Class 对象。 这在 Android 开发领域很常见,因为某种目的,Android 工程师把一些类加上了 @hide 注解,所示这些类就没有出现在 SDK 当中,那么,我们要获取这个并不存在于当前开发环境中的类的 Class 对象时就没有辙了吗?答案是否定的,Java 给我们提供了 Class.forName() 这个方法。 只要给这个方法中传入一个类的全限定名称就好了,那么它就会到 Java 虚拟机中去寻找这个类有没有被加载。 “com.frank.test.Car” 就是 Car 这个类的全限定名称,它包括包名+类名。 如果找不到时,它会抛出 ClassNotFoundException 这个异常,这个很好理解,因为如果查找的类没有在 JVM 中加载的话,自然要告诉开发者。 所以,上面 3 节讲述了如何拿到一个类的 Class 对象。 Class 内容清单仅仅拿到 Class 对象还不够,我们感兴趣的是它的内容。 在正常的代码编写中,我们如果要编写一个类,一般会定义它的属性和方法,如: 现在我们来一一分解它。 Class 的名字Class 对象也有名字,涉及到的 API 有: 现在,说说它们的区别。 因为 Class 是一个入口,它代表引用、基本数据类型甚至是数组对象,所以获取它们的方式又有一点不同。 先从 getName() 说起。 当 Class 代表一个引用时getName() 方法返回的是一个二进制形式的字符串,比如“com.frank.test.Car”。 当 Class 代表一个基本数据类型,比如 int.class 的时候getName() 方法返回的是它们的关键字,比如 int.class 的名字是 int。 当 Class 代表的是基础数据类型的数组时 比如 int[][][] 这样的 3 维数组时getName() 返回 [[[I 这样的字符串。 为什么会这样呢?这是因为,Java 本身对于这一块制定了相应规则,在元素的类型前面添加相应数量的 [ 符号,用 [ 的个数来提示数组的维度,并且值得注意的是,对于基本类型或者是类,都有相应的编码,所谓的编码大多数是用一个大写字母来指示某种类型,规则如下: 需要注意的是类或者是接口的类型编码是 L类名; 的形式,后面有一个分号。 比如 String[].getClass().getName() 结果是 [Ljava.lang.String;。 我们来测试一下代码: 上面代码的打印结果如下: 刚刚介绍的都是 getName() 的情况,那么 getSimpleName() 和 getCaninolName() 呢? getSimpleName() 自然是要去获取 simplename 的,那么对于一个 Class 而言什么是 SimpleName 呢?我们先要从嵌套类说起 Outter 这个类中有一个静态的内部类。 我们分别打印 Inner 这个类的 Class 对象的 name 和 simplename。 可以看到,因为是内部类,所以通过 getName() 方法获取到的是二进制形式的全限定类名,并且类名前面还有个 $ 符号。 打个比方,我的全名叫做 Frank Zhao,而我的 simplename 就叫做 frank,simplename 之于 name 也是如此。 simplename 的不同需要注意的是,当获取一个数组的 Class 中的 simplename 时,不同于 getName() 方法,simplename 不是在前面加 [,而是在后面添加对应数量的 [] 。 上面代码打印结果是: 还需要注意的是,对于匿名内部类,getSimpleName() 返回的是一个空的字符串。 打印结果是: 最后再来看 getCanonicalName()。 Canonical 是官方、标准的意思,那么 getCanonicalName() 自然就是返回一个 Class 对象的官方名字,这个官方名字 canonicalName 是 Java 语言规范制定的,如果 Class 对象没有 canonicalName 的话就返回 null。 getCanonicalName() 是 getName() 和 getSimpleName() 的结合。
打印结果如下: Class 去获取相应名字的知识内容就讲完了,仔细想一下,小小的一个细节,其实蛮有学问的。 好了,我们继续往下。 Class 获取修饰符通常,Java 开发中定义一个类,往往是要通过许多修饰符来配合使用的。它们大致分为 4 类。
Java 反射提供了 API 去获取这些修饰符。 我们定义了一个类,名字为 TestModifier,被 public 和 abstract 修饰,现在我们要提取这些修饰符。我们只需要调用 Class.getModifiers() 方法就是了,它返回的是一个 int 数值。 打印结果是: 大家肯定会有疑问,为什么会返回一个整型数值呢? 这是因为一个类定义的时候可能会被多个修饰符修饰,为了一并获取,所以 Java 工程师考虑到了位运算,用一个 int 数值来记录所有的修饰符,然后不同的位对应不同的修饰符,这些修饰符对应的位都定义在 Modifier 这个类当中。 调用 Modifier.toString() 方法就可以打印出一个类的所有修饰符。 当然,Modifier 还提供了一系列的静态工具方法用来对修饰符进行操作。 这些代码的作用,一看就懂,所以不再多说。 获取 Class 的成员一个类的成员包括属性(有人翻译为字段或者域)、方法。对应到 Class 中就是 Field、Method、Constructor。 获取 Filed获取指定名字的属性有 2 个 API 两者的区别就是 getDeclaredField() 获取的是 Class 中被 private 修饰的属性。 getField() 方法获取的是非私有属性,并且 getField() 在当前 Class 获取不到时会向祖先类获取。 获取所有的属性。 可以用一个例子,给大家加深一下理解。 代码打印结果: 大家细细体会一下,不过需要注意的是 getDeclaredFileds() 方法可以获取 private、protected、public 和 default 属性,但是它获取不到从父类继承下来的属性。 获取 Method类或者接口中的方法对应到 Class 就是 Method。 因为跟 Field 类似,所以不做过多的讲解。parameterTypes 是方法对应的参数。 获取 ConstructorJava 反射把构造器从方法中单独拎出来了,用 Constructor 表示。 仍然以前面的 Father 和 Son 两个类为例。 测试程序代码的打印结果如下: 因为,Constructor 不能从父类继承,所以就没有办法通过 getConstructor() 获取到父类的 Constructor。 我们获取到了 Field、Method、Constructor,但这一是终点,相反,这正是反射机制中开始的地方,我们运用反射的目的就是为了获取和操控 Class 对象中的这些成员。 Field 的操控我们在一个类中定义字段时,通常是这样。 像 c、d、e、car 这些变量都是属性,在反射机制中映射到 Class 对象中都是 Field,很显然,它们也有对应的类别。 它们要么是 8 种基础类型 int、long、float、double、boolean、char、byte 和 short。或者是引用,所有的引用都是 Object 的后代。 Field 类型的获取获取 Field 的类型,通过 2 个方法: 注意,两者返回的类型不一样,getGenericType() 方法能够获取到泛型类型。大家可以看下面的代码进行理解: 打印结果: 可以看到 getGenericType() 确实把泛型都打印出来了,它比 getType() 返回的内容更详细。 Field 修饰符的获取同 Class 一样,Field 也有很多修饰符。通过 getModifiers() 方法就可以轻松获取。 这个与前面 Class 获取修饰符一致,所以不需要再讲,不清楚的同学翻看前面的内容就好了。 Field 内容的读取与赋值这个应该是反射机制中对于 Field 最主要的目的了。 Field 这个类定义了一系列的 get 方法来获取不同类型的值。 Field 又定义了一系列的 set 方法用来对其自身进行赋值。 可能有同学会对方法中出现的 Object 参数有疑问,它其实是类的实例引用,这里涉及一个细节。 Class 本身不对成员进行储存,它只提供检索,所以需要用 Field、Method、Constructor 对象来承载这些成员,所以,针对成员的操作时,一般需要为成员指定类的实例引用。如果难于理解的话,可以这样理解,班级这个概念是一个类,一个班级有几十名学生,现在有A、B、C 3 个班级,将所有班级的学生抽出来集合到一个场地来考试,但是学生在试卷上写上自己名字的时候,还要指定自己的班级,这里涉及到的 Object 其实就是类似的作用,表示这个成员是具体属于哪个 Object。这个是为了精确定位。 下面用代码来说明: 打印结果如下: 我们再来看看 Field 被 private 修饰的情况 再编写测试代码 打印的结果如下: 抛异常了。这是因为在反射中访问了 private 修饰的成员,如果要消除异常的话,需要添加一句代码。 再看打印结果 Method 的操控Method 对应普通类的方法。 方法由下面几个要素构成: 很显然,反射中 Method 提供了相应的 API 来提取这些元素。 Method 获取方法名通过 getName() 这个方法就好了。 以前面的 Car 类作为测试对象。 打印结果如下: Method 获取方法参数涉及到的 API 如下: 返回的是一个 Parameter 数组,在反射中 Parameter 对象就是用来映射方法中的参数。经常使用的方法有: Parameter.java 当然,有时候我们不需要参数的名字,只要参数的类型就好了,通过 Method 中下面的方法获取。 下面,同样进行测试。 打印结果如下: Method 获取返回值类型Method 获取修饰符这部分内容前面已经讲过。 Method 获取异常类型Method 方法的执行这个应该是整个反射机制的核心内容了,很多时候运用反射目的其实就是为了以常规手段执行 Method。 Method 调用 invoke() 的时候,存在许多细节:
下面同样通过例子来说明,我们新建立一个类,要添加一个 static 修饰的静态方法,一个普通的方法和一个会抛出异常的方法。 我们编写测试代码: 打印结果如下: Constructor 的操控在平常开发的时候,构造器也称构造方法,但是在反射机制中却把它与 Method 分离开来,单独用 Constructor 这个类表示。 Constructor 同 Method 差不多,但是它特别的地方在于,它能够创建一个对象。 在 Java 反射机制中有两种方法可以用来创建类的对象实例:Class.newInstance() 和 Constructor.newInstance()。官方文档建议开发者使用后面这种方法,下面是原因。
还是通过代码来验证。 上面的类中有 2 个构造方法,一个无参,一个有参数。编写测试代码: 分别用 Class.newInstance() 和 Constructor.newInstance() 方法来创建类的实例,打印结果如下: 可以看到通过 Class.newInstance() 方法调用的构造方法确实是无参的那个。 现在,我们学习了 Class 对象的获取,也能够获取它内部成员 Filed、Method 和 Constructor 并且能够操作它们。在这个基础上,我们已经能够应付普通的反射开发了。 但是,Java 反射机制还另外细分了两个概念:数组和枚举。 反射中的数组数组本质上是一个 Class,而在 Class 中存在一个方法用来识别它是否为一个数组。 为了便于测试,我们创建一个新的类 其中有一个 int 型的数组属性,它的名字叫做 array。还有一个 cars 数组,它的类型是 Car,是之前定义好的类。 当然,array 和 cars 是 Shuzu 这个类的 Field,对于 Field 的角度来说,它是数组类型,我们可以这样理解数组可以同 int、char 这些基本类型一样成为一个 Field 的类别。 我们可能通过一系列的 API 来获取它的具体信息,刚刚有提到它本质上还是一个 Class 而已。 第二个方法是获取数组的里面的元素的类型,比如 int[] 数组的 componentType 自然就是 int。 按照惯例,写代码验证。 打印结果如下: 反射中动态创建数组反射创建数组是通过 Array.newInstance() 这个方法。 第一个参数指定的是数组内的元素类型,后面的是可变参数,表示的是相应维度的数组长度限制。 比如,我要创建一个 int[2][3] 的数组。 Array 的读取与赋值首先,对于 Array 整体的读取与赋值,把它作为一个普通的 Field,根据 Class 中相应获取和设置就好了。调用的是 Field 中对应的方法。 还需要处理的情况是对于数组中指定位置的元素进行读取与赋值,这要涉及到 Array 提供的一系列 setXXX() 和 getXXX() 方法。因为和之前 Field 相应的 set 、get 方法类似,所以我在下面只摘抄典型的几种,大家很容易知晓其它类型的怎么操作。 进行代码测试: 打印结果如下: 反射中的枚举 Enum同数组一样,枚举本质上也是一个 Class 而已,但反射中还是把它单独提出来了。 我们来看一般程序开发中枚举的表现形式。 枚举真的跟类很相似,有修饰符、有方法、有属性字段甚至可以有构造方法。 在 Java 反射中,可以把枚举看成一般的 Class,但是反射机制也提供了 3 个特别的的 API 用于操控枚举。 枚举的获取与设定因为等同于 Class,所以枚举的获取与设定就可以通过 Field 中的 get() 和 set() 方法。 需要注意的是,如果要获取枚举里面的 Field、Method、Constructor 可以调用 Class 的通用 API。 用例子来加深理解吧。 打印结果如下: 到这里,反射的所有知识基本上讲完了。下面进行模拟实战。 反射与自动驾驶文章开头,我用自动驾驶的技术来比喻反射,实际上的目的是为了给初学者一个大体的印象和一个模糊的轮廓,实际上反射不是自动驾驶,它是什么取决于你自己对它的理解。 下段代码的目标是为了对比,先定义一个类 AutoDrive,这个类有一系列的属性,然后有一系列的方法,先用普通编码的方式来创建这个类的对象,调用它的方法。然后用反射的机制模拟自动驾驶。 汽车开动的步骤,以手动档为例。 现在代码模拟 我们只要创建一个 AutoDrive 的对象,调用它的 drive() 方法就好了。 结果如下: 我们现在要使用自动驾驶技术,具体到代码就是反射,因为非常规嘛。 最后,打印结果: 总结
最后,需要注意的是。 反射是非常规开发手段,它会抛弃 Java 虚拟机的很多优化,所以同样功能的代码,反射要比正常方式要慢,所以考虑到采用反射时,要考虑它的时间成本。另外,就如无人驾驶之于汽车一样,用着很爽的同时,其实风险未知。 |
|