编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议102~105)

建议102:适时选择getDeclaredXXX和getXXX

  Java的Class类提供了不少的getDeclaredXXX方法和getXXX方法,例如getDeclaredMethod和getMethod成对出现,getDeclaredConstructors和getConstructors也是成对出现,那这二者之间有什么差异呢?看以下代码:java

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() {
        }
    }
}

  此段代码运行后输出以下:mysql

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)方法,并且不受限于访问权限。sql

  其它的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方法能够被任何一个类访问。咱们编写一个客户端类来检查该方法是否能够反射执行:ide

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。

相关文章
相关标签/搜索