你的位置:首页 > Java教程

[Java教程]编写高质量代码:改善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。