编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议102~105)
建议102:适时选择getDeclaredXXX和getXXX
Java的Class类提供了很多的getDeclaredXXX方法和getXXX方法,例如getDeclaredMethod和getMethod成对出现,getDeclaredConstructors和getConstructors也是成对出现,那这两者之间有什么差别呢?看如下代码:
public class Client102 { public static void main(String[] args) throws NoSuchMethodException, SecurityException { // 方法名称 String methodName = "doStuff"; Method m1 = Foo.class.getDeclaredMethod(methodName); Method m2 = Foo.class.getMethod(methodName); } //静态内部类 static class Foo { void doStuff() { } } }
此段代码运行后输出如下:
Exception in thread "main" java.lang.NoSuchMethodException: com.study.advice102.Client102$Foo.doStuff() at java.lang.Class.getMethod(Class.java:1622) at com.study.advice102.Client102.main(Client102.java:10)
该异常是说m2变量的getMethod方法没有找到doStuff方法,明明有这个方法呀,为什么没有找到呢?这是因为getMethod方法获得的是所有public访问级别的方法,包括从父类继承的方法,而getDeclaredMethod获得的是自身类的方法,包括公用的(public)方法、私有(private)方法,而且不受限于访问权限。
其它的getDeclaredConstructors和getConstructors、getDeclaredFileds和getFields等于此相似。Java之所以如此处理,是因为反射本意只是正常代码逻辑的一种补充,而不是让正常代码逻辑发生翻天覆地的变化,所以public的属性和方法最容易获取,私有属性和方法也可以获取,但要限定本类。
那么问题来了:如果需要列出所有继承自父类的方法,该如何实现呢?简单,先获得父类,然后使用getDeclaredMethods,之后持续递归即可。
建议103:反射访问属性或方法时将Accessible设置为true
Java中通过反射执行一个方法的过程如下:获取一个方法对象,然后根据isAccessible返回值确定是否能够执行,如果返回值为false则需要调用setAccessible(true),最后再调用invoke执行方法,具体如下:
Method method= ...; //检查是否可以访问 if(!method.isAccessible()){ method.setAccessible(true); } //执行方法 method.invoke(obj, args);
此段代码已经成了习惯用法:通过反射方法执行方法时,必须在invoke之前检查Accessible属性。这是一个好习惯,也确实该如此,但方法对象的Accessible属性并不是用来决定是否可以访问的,看如下代码:
public class Foo { public final void doStuff(){ System.out.println("Do Stuff..."); } }
定义一个public类的public方法,这是一个没有任何限制的方法,按照我们对Java语言的理解,此时doStuff方法可以被任何一个类访问。我们编写一个客户端类来检查该方法是否可以反射执行:
public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { // 反射获取方法 Method m = Foo.class.getMethod("doStuff"); // 打印是否可以访问 System.out.println("Accessible:" + m.isAccessible()); // 执行方法 m.invoke(new Foo()); }
很简单的反射操作,获得一个方法,然后检查是否可以访问,最后执行方法输出。让我们来猜想一下结果:因为Foo类是public的,方法也是public的,全部都是最开放的访问权限Accessible也应该等于true。但是运行结果却是:
Accessible:false
Do Stuff...
为什么Accessible属性会等于false?而且等于false还能执行?这是因为Accessible的属性并不是我们语法层级理解的访问权限,而是指是否更容易获得,是否进行安全检查。
我们知道,动态修改一个类或执行方法时都会受到Java安全体制的制约,而安全的处理是非常耗资源的(性能非常低),因此对于运行期要执行的方法或要修改的属性就提供了Accessible可选项:由开发者决定是否要逃避安全体系的检查。
阅读源代码是最好的理解方式,我们来看AccessibleObject类的源代码,它提供了取消默认访问控制检查的功能。首先查看isAccessible方法,代码如下:
public class AccessibleObject implements AnnotatedElement { //定义反射的默认操作权限suppressAccessChecks static final private java.security.Permission ACCESS_PERMISSION = new ReflectPermission("suppressAccessChecks"); //是否重置了安全检查,默认为false boolean override; //构造函数 protected AccessibleObject() {} //是否可以快速获取,默认是不能 public boolean isAccessible() { return override; } }
AccessibleObject是Filed、Method、Constructor的父类,决定其是否可以快速访问而不进行访问控制检查,在AccessibleObject类中是以override变量保存该值的,但是具体是否快速执行时在Method的invoke方法中决定的,源码如下:
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { //见擦汗是否可以快速获取,其值是父类AccessibleObject的override变量 if (!override) { //不能快速获取,执行安全检查 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(1); checkAccess(caller, clazz, obj, modifiers); } } MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } //直接执行方法 return ma.invoke(obj, args); }
看了这段代码,大家就清楚了:Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度的提升系统性能了(当然了,取消了安全检查,也可以运行private方法、访问private属性的)。经过测试,在大量的反射情况下,设置Accessible为true可以提高性能20倍左右。
AccessibleObject的其它两个子类Field和Constructor与Method的情形类似:Accessible属性决定Field和Constructor是否受访问控制检查。我们在设置Field或执行Constructor时,务必要设置Accessible为true,这并不仅仅是因为操作习惯的问题,还是为我们的系统性能考虑。
建议104:使用forName动态加载类文件
动态加载(Dynamic Loading)是指在程序运行时加载需要的类库文件,对Java程序来说,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时再决定是否需要加载一个类,比如从Web上接收一个String参数作为类名,然后在JVM中加载并初始化,这就是动态加载,此动态加载通常是通过Class.forName(String)实现的,只是这个forName方法到底是什么意思呢?
我们知道一个类文件只有在被加载到内存中才可能生成实例对象,也就是说一个对象的生成必然会经过两个步骤:
- 加载到内存中生成Class的实例对象
- 通过new关键字生成实例对象
如果我们使用的是import关键字产生的依赖包,JVM在启动时会自动加载所有的依赖包的类文件,这没有什么问题,如果好动态加载类文件,就要使用forName的方法了,但问题是我们为什么要使用forName方法动态加载一个类文件呢?那是因为我们不知道生成的实例对象是什么类型(如果知道就不用动态加载),而且方法和属性都不可访问呀。问题又来了:动态加载的意义在什么地方呢?
意义在于:加载一个类即表示要初始化该类的static变量,特别是static代码块,在这里我们可以做大量的工作,比如注册自己,初始化环境等,这才是我们要重点关注的逻辑,例如如下代码:
package com.study.advice103; public class Client103 { public static void main(String[] args) throws ClassNotFoundException { //动态加载 Class.forName("com.study.advice103.Utils"); } } class Utils{ //静态代码块 static{ System.out.println("Do Something....."); } }
注意看Client103类,我们并没有对Utils做任何初始化,只是通过forName方法加载了Utils类,但是却产生了一个“Do Something.....”的输出,这就是因为Utils类加载后,JVM会自动初始化其static变量和static静态代码块,这是类加载机制所决定的。
对于动态加载,最经典的应用是数据库驱动程序的加载片段,代码如下:
//加载驱动 Class.forName("com.mysql..jdbc.Driver"); String url="jdbc:mysql://localhost:3306/db?user=&password="; Connection conn =DriverManager.getConnection(url); Statement stmt =conn.createStatement();
在没有Hibernate和Ibatis等ORM框架的情况下,基本上每个系统都会有这么一个JDBC链接类,然后提供诸如Query、Delete等的方法,大家有没有想过为什么要加上forName这句话呢?没有任何的输出呀,要它干什么用呢?事实上非常有用,我们看一下Driver的源码:
public class Driver extends NonRegisteringDriver implements java.sql.Driver { //构造函数 public Driver() throws SQLException { } //静态代码块 static { try { //把自己注册到DriverManager中 DriverManager.registerDriver(new Driver()); } catch(SQLException E) { //异常处理 throw new RuntimeException("Can't register driver!"); } } }
该程序的逻辑是这样的:数据库驱动程序已经由NonRegisteringDriver实现了,Driver类只是负责把自己注册到DriverManager中。当程序动态加载该驱动时,也就是执行到Class.forName("com.mysql..jdbc.Driver")时,Driver类会被加载到内存中,于是static代码块开始执行,也就是把自己注册到DriverManager中。
需要说明的是,forName只是把一个类加载到内存中,并不保证由此产生一个实例对象,也不会执行任何方法,之所以会初始化static代码,那是由类加载机制所决定的,而不是forName方法决定的。也就是说,如果没有static属性或static代码块,forName就是加载类,没有任何的执行行为。
注意:forName只是加载类,并不执行任何代码。
建议105:动态加载不适合数组
上一个建议解释了为什么要用forName,本建议就来说说那些地方不适合动态加载。如果forName要加载一个类,那它首先必须是一个类___8个基本类型排除在外,它们不是一个具体的类;其次,它必须具有可追溯的类路径,否则就会报ClassNotFoundException。
在Java中,数组是一个非常特殊的类,虽然它是一个类,但没有定义类类路径,例如这样的代码:
public static void main(String[] args) throws ClassNotFoundException { String [] strs = new String[10]; Class.forName("java.lang.String[]"); }
String []是一个类型声明,它作为forName的参数应该也是可行的吧!但是非常遗憾,其运行结果如下:
Exception in thread "main" java.lang.ClassNotFoundException: java/lang/String[]
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:186)
产生ClassNotFoundException异常的原因是数组算是一个类,在声明时可以定义为String[],但编译器编译后为不同的数组类型生成不同的类,具体如下表所示:
数组编译对应关系表 | |
元素类型 | 编译后的类型 |
byte[] | [B |
char[] | [C |
Double[] | [D |
Float[] | [F |
Int[] | [I |
Long[] | [J |
Short[] | [S |
Boolean[] | [Z |
引用类型(如String[]) | [L引用类型(如:[Ljava.lang.String;) |
在编码期,我们可以声明一个变量为String[],但是经过编译后就成为了[Ljava.lang.String。明白了这一点,再根据以上的表格可知,动态加载一个对象数组只要加载编译后的数组对象就可以了,代码如下:
//加载一个数组 Class.forName("[Ljava.lang.String;"); //加载一个Long数组 Class.forName("[J");
虽然以上代码可以加载一个数组类,但这是没有任何意义的,因为它不能产生一个数组对象,也就是说以上代码只是把一个String类型的数组类和Long类型的数组类加载到了内存中(如果内存中没有改类的话),并不能通过newInstance方法生成一个实例对象,因为它没有定义数组的长度,在Java中数组是定长的,没有长度的数组是不允许存在的。
既然反射不能定义一个数组,那问题就来了:如何动态加载一个数组呢?比如依据输入动态生成一个数组。其实可以使用Array数组反射类动态加载,代码如下:
// 动态创建数组 String[] strs = (String[]) Array.newInstance(String.class, 8); // 创建一个多维数组 int[][] ints = (int[][]) Array.newInstance(int.class, 2, 3);
因为数组比较特殊,要想动态创建和访问数组,基本的反射是无法实现的,“上帝对你关闭一扇门,同时会为你打开一扇窗。”,于是Java就专门定义了一个Array数组反射工具类来实现动态探知数组的功能。
注意:通过反射操作数组使用Array类,不要采用通用的反射处理API。