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

  • 相关阅读:
    oracle 快速删除大批量数据方法(全部删除,条件删除,删除大量重复记录) 转
    linux rm删除含有特殊符号目录或者文件
    Linux下安装SVN(Subversion)
    linux还原svn
    linux 备份svn
    Linux下Tomcat重新启动
    Linux下rz,sz与ssh的配合使用
    深度分析Linux下双网卡绑定七种模式
    Ubuntu 12.04 安装Scrapy爬虫框架
    maven 添加本地jar
  • 原文地址:https://www.cnblogs.com/selene/p/5939595.html
Copyright © 2020-2023  润新知