编写高质量代码:改善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。

posted @ 2016-10-09 13:23  阿赫瓦里  阅读(1723)  评论(3编辑  收藏  举报