`

Java编程的动态性(3): 应用反射

阅读更多
命令行参数处理是一项令人厌烦的零碎工作,不管您过去已经处理过多少次了,它好像总能重新摆在您的面前。与其一遍又一遍地编写同一块代码的不同变种,为什么不利用反射来简化参数处理的工作呢?Java 顾问 Dennis Sosnoski 向您展示了如何做到这一点。在本文中,Dennis 简明扼要地介绍了一个开源库,这个库可以使得命令行参数实际上自己处理自己。

上个月的文章中,我介绍了Java Reflection API,并简要地讲述了它的一些基本功能。我还仔细研究了反射的性能,并且在文章的最后给出了一些指导方针,告诉读者在一个应用程序中何时应该使用反射,何时不应该使用反射。在本月这一期的文章中,我将通过查看一个应用程序来更深入地讨论这一问题,这个应用程序是用于命令行参数处理的一个库,它能够很好地体现反射的强项和弱点。

一开始,在真正进入编写实现代码的工作之前,我将首先定义要解决的问题,然后为这个库设计一个接口。不过,在开发这个库的时候,我并不是按照上述步骤进行的――我先是尽力简化一群有公共代码基础的应用程序中的现有代码,然后使之通用化。本文中使用的“定义-设计-构建”这种线性序列比起完完整整地描述开发过程要简练得多,而且,按照这种方式来组织对开发过程的描述,我可以修正我原先的一些假设,并清理掉这个库的代码中一些不必要的方面。您完全有希望发现将上述方式作为开发您自己的基于反射的应用程序时所使用的模型十分管用。

定义问题

 

我曾经写过许多使用命令行参数的Java应用程序。一开始,大多数应用程序都很小,但最后有些应用程序却变得大到出乎我的意料。下面是我观察到的这些应用程序的变大过程的标准模式:

  1. 一开始只有一个或者两个参数,按照某种特定的顺序排列。
  2. 考虑到这个应用程序有更多的事情要做,于是添加更多的参数。
  3. 厌倦了每次都输入所有的参数,于是让一些参数成为可选的参数,让这些参数带有默认的值。
  4. 忘记了参数的顺序,于是修改代码,允许参数以任何顺序排列。
  5. 将这个应用程序交给其他感兴趣的人。但是他们并不知道这些参数各自代表什么,于是又为这些参数添加更完善的错误检查和“帮助”描述。

当我进入到第5步的时候,我通常会后悔没有将整个过程都放在第一步来做。好在我很快就会忘记后面的那些阶段,不到一两个星期,我又会考虑另外一个简单的小命令行程序,我想拥有这个应用程序。有了这个想法之后,上述整个恶心的循环过程的重现只是时间的问题。

有一些库可以用来帮助进行命令行参数处理。不过,在本文中我会忽略掉这些库,而是自己动手创建一个库。这不是(或者不仅仅是)因为我有着“非此处发明(not invented here)”的态度(即不愿意用外人发明的东西,译者注),而是因为想拿参数处理作为一个实例。这样一来,反射的强项和弱点便正好体现了对参数处理库的需求。特别地,参数处理库:

  • 需要一个灵活的接口,用以支持各种应用程序。
  • 对于每个应用程序,都必须易于配置。
  • 不要求顶级的性能,因为参数只需处理一次。
  • 不存在访问安全性问题,因为命令行应用程序运行的时候通常不带安全管理器。

这个库中实际的反射代码只代表整个实现的一小部分,因此我将主要关注与反射最相关的一些方面。如果您想找到有关这个库的更多内容(或许还想将它用到您自己的简单命令行应用程序中去),您可以在 参考资料部分找到指向Web站点的链接。

草拟出一份设计

应用程序访问参数数据最方便的方式或许是通过该应用程序的 main 对象的一些字段。例如,假设您正在编写一个用于生成业务计划的应用程序。您可能想使用一个 boolean 标记来控制业务计划是简要的还是冗长的,使用一个 int 作为第一年的收入,使用一个 String 作为对产品的描述。我将把这些会影响应用程序的运行的变量称作 形参(parameters),以便与命令行提供的

 实参(arguments)――即形参的值区分开来。通过为这些形参使用字段,将使得在需要形参的应用程序代码中的任何地方都可以方便地调用它们。而且,如果使用字段的话,在定义形参字段时为任意形参设置默认值也很方便,如清单1所示:


清单 1.业务计划生成器(部分清单)

public class PlanGen {
    private boolean m_isConcise;          // rarely used, default false
    private int m_initialRevenue = 1000;  // thousands, default is 1M
    private float m_growthRate = 1.5;     // default is 50% growth rate
    private String m_productDescription = // McD look out, here I come
        "eFood - (Really) Fast Food Online";
    ...
    private int revenueForYear(int year) {
        return (int)(m_initialRevenue * Math.pow(m_growthRate, year-1));
    }
    ...

 

反射将使得应用程序可以直接访问这些私有字段,允许参数处理库在应用程序代码中没有任何特殊钩子的情况下设置参数的值。但是我 的确需要某种方法能让这个库将这些字段与特定的命令行参数相关起来。在我能够定义一个参数和一个字段之间的这种关联如何与库进行通信之前,我需要决定我希望如何格式化这些命令行参数。

对于本文,我将定义一种命令行格式,这是UNIX惯例的一种简化版本。形参的实参值可以以任何顺序提供,在最前面使用一个连字符以指示一个实参给出了一个或者多个单字符的形参标记(与实际的形参的值相对)。对于这个业务计划生成器,我将采用以下形参标记字符:

  • c -- 简要计划
  • f -- 第一年收入(千美元)
  • g -- 增长率(每年)
  • n -- 产品名称

boolean 形参只需标记字符本身就可以设置一个值,而其他类型的形参还需要某种附加的实参信息。对于数值实参,我只将它的值紧跟在形参标记字符之后(这意味着数字不能用作标记字符),而对于带 String 类型值的形参,我将在命令行中使用跟在标记字符后面的实参作为实际的值。最后,如果还需要一些形参(例如业务计划生成器的输出文件的文件名),我假设这些形参的实参值跟在命令行中可选形参值的后面。有了上面给出的这些约定,业务计划生成器的命令行看上去就是这个样子:

java PlanGen -c -f2500 -g2.5 -n "iSue4U - Litigation at Internet Speed" plan.txt

如果把它放在一起,那么每个实参的意思就是:

  • -c -- 生成简要计划
  • -f2500 -- 第一年收入为 $2,500,000
  • -g2.5 -- 每年增长率为250%
  • -n "iSue4U . . ." -- 产品名称是 "iSue4U . . ."
  • plan.txt -- 需要的输出文件名

这时,我已经得到了参数处理库的基本功能的规范说明书。下一步就是为这个应用代码定义一个特定的接口,以使用这个库。

选择接口

您可以使用单个的调用来负责命令行参数的实际处理,但是这个应用程序首先需要以某种方式将它的特定的形参定义到库中。这些形参可以具有不同的几种类型(对于业务计划生成器的例子,形参的类型可以是 boolean , int、 floatjava.lang.String )。每种类型可能又有一些特殊的需求。例如,如果给出了标记字符的话,将 boolean 形参定义为 false 会比较好,而不是总将它定义为 true 。而且,为一个 int 值定义一个有效范围也很有用。

我处理这些不同需求的方法是,首先为所有形参定义使用一个基类,然后为每一种特定类型的形参细分类这个基类。这种方法使得应用程序可以以基本形参定义类的实例数组的形式将形参定义提供给这个库,而实际的定义则可以使用匹配每种形参类型的子类。对于业务计划生成器的例子,这可以采用清单2中所示的形式:


清单 2. 业务计划生成器的形参定义

private static final ParameterDef[] PARM_DEFS = { 
    new BoolDef('c', "m_isConcise"),
    new IntDef('f', "m_initialRevenue", 10, 10000),
    new FloatDef('g', "m_growthRate", 1.0, 100.0),
    new StringDef('n', "m_productDescription")
}

 

有了得到允许的在一个数组中定义的形参,应用程序对参数处理代码的调用就可以像对一个静态方法的单个调用一样简单。为了允许除形参数组中定义的实参之外额外的实参(要么是必需的值,要么是可变长度的值),我将令这个调用返回被处理实参的实际数量。这样应用程序便可以检查额外的实参并适当地使用它们。最后的结果看上去如清单3所示:


清单 3. 使用库

public class PlanGen
{
    private static final ParameterDef[] PARM_DEFS = {
        ...
    };
    
    public static void main(String[] args) {
    
        // if no arguments are supplied, assume help is needed
        if (args.length > 0) {
        
            // process arguments directly to instance
            PlanGen inst = new PlanGen();
            int next = ArgumentProcessor.processArgs
                (args, PARM_DEFS, inst);
            
            // next unused argument is output file name
            if (next >= args.length) {
                System.err.println("Missing required output file name");
                System.exit(1);
            }
            File outf = new File(args[next++]);
            ...
        } else {
            System.out.println("\nUsage: java PlanGen " +
            "[-options] file\nOptions are:\n  c  concise plan\n" +
            "f  first year revenue (K$)\n  g  growth rate\n" +
            "n  product description");
        }
    }
}

 

最后剩下的部分就是处理错误报告(例如一个未知的形参标记字符或者一个超出范围的数字值)。

出于这个目的,我将定义 ArgumentErrorException 作为一个未经检查的异常,如果出现了某个这一类的错误,就将抛出这个异常。

如果这个异常没有被捕捉到,它将立即关闭应用程序,并将一条错误消息和栈跟踪 输出到控制台。

一个替代的方法是,您也可以在代码中直接捕捉这个异常,并且用其他方式处理异常(例如,可能会与使用信息一起输出真正的错误消息)。

 

 

为了让这个库像计划的那样使用反射,它需要查找由形参定义数组指定的一些字段,然后将适当的值存到这些来自相应的命令行参数的字段中。这项任务可以通过只查找实际的命令行参数所需的字段信息来处理,但是我反而选择将查找和使用分开。我将预先找到所有的字段,然后只使用在参数处理期间已经被找到的信息。

预先找到所有的字段是一种防错性编程的步骤,这样做可以消除使用反射时带来的一个潜在的问题。如果我只是查找需要的字段,那么就很容易破坏一个形参定义(例如,输错相应的字段名),而且还不能认识到有错误发生。这里不会有编译时错误,因为字段名是作为 String 传递的,而且,只要命令行没有指定与已破坏的形参定义相匹配的实参,程序也可以执行得很好。

这种被蒙蔽的错误很容易导致不完善代码的发布

假设我想在实际处理实参之前查找字段信息,清单4显示了用于形参定义的基类的实现,这个实现带有一个 bindToClass() 方法,用于处理字段查找。

清单 4. 用于形参定义的基类

public abstract class ParameterDef
{
    protected char m_char;          // argument flag character
    protected String m_name;        // parameter field name
    protected Field m_field;        // actual parameter field
    
    protected ParameterDef(char chr, String name) {
        m_char = chr;
        m_name = name;
    }
    public char getFlag() {
        return m_char;
    }
    protected void bindToClass(Class clas) {
        try {
        
            // handle the field look up and accessibility
            m_field = clas.getDeclaredField(m_name);
            m_field.setAccessible(true);
            
        } catch (NoSuchFieldException ex) {
            throw new IllegalArgumentException("Field '" +
                m_name + "' not found in " + clas.getName());
        }
    }
    public abstract void handle(ArgumentProcessor proc);
}

 

实际的库实现还涉及到本文没有提及的几个类。我不打算一一介绍每一个类,因为其中大部分类都与库的反射方面不相关。我将提到的是,我选择将目标对象存为 ArgumentProcessor 类的一个字段,并在这个类中实现一个形参字段的真正设置。这种方法为参数处理提供了一个简单的模式: ArgumentProcessor 类扫描实参以发现形参标记,为每个标记查找相应的形参定义(总是 ParameterDef 的一个子类),再调用这个定义的 handle() 方法。 handle() 方法在解释完实参值之后,又调用 ArgumentProcessorsetValue() 方法。清单5显示了 ArgumentProcessor 类的不完整版本,包括在构造函数中的形参绑定调用以及 setValue() 方法:

清单 5. 主库类的部分清单

 

public class ArgumentProcessor
{
    private Object m_targetObject;  // parameter value object
    private int m_currentIndex;     // current argument position
    ...
    public ArgumentProcessor(ParameterDef[] parms, Object target) {
        
        // bind all parameters to target class
        for (int i = 0; i < parms.length; i++) {
            parms[i].bindToClass(target.getClass());
        }
        
        // save target object for later use
        m_targetObject = target;
    }
    
    public void setValue(Object value, Field field) {
        try {
        
            // set parameter field value using reflection
            field.set(m_targetObject, value);
            
        } catch (IllegalAccessException ex) {
            throw new IllegalArgumentException("Field " + field.getName() +
                " is not accessible in object of class " + 
                m_targetObject.getClass().getName());
        }
		}
    
    public void reportArgumentError(char flag, String text) {
      throw new ArgumentErrorException(text + " for argument '" + 
        flag + "' in argument " + m_currentIndex);
    }
    
    public static int processArgs(String[] args,
        ParameterDef[] parms, Object target) {
        ArgumentProcessor inst = new ArgumentProcessor(parms, target);
        ...
    }
}

 

最后,清单6显示了 int 形参值的形参定义子类的部分实现。这包括对基类的 bindToClass() 方法(来自 清单4)的重载,这个重载的方法首先调用基类的实现,然后检查找到的字段是否匹配预期的类型。其他特定形参类型( boolean、 float、 String ,等等)的子类与此十分相似。

清单 6. int 形参定义类

 

public class IntDef extends ParameterDef
{
    private int m_min;              // minimum allowed value
    private int m_max;              // maximum allowed value
    
    public IntDef(char chr, String name, int min, int max) {
        super(chr, name);
        m_min = min;
        m_max = max;
    }
    protected void bindToClass(Class clas) {
        super.bindToClass(clas);
        Class type = m_field.getType();
        if (type != Integer.class && type != Integer.TYPE) {
            throw new IllegalArgumentException("Field '" + m_name +
                "'in " + clas.getName() + " is not of type int");
        }
    }
    public void handle(ArgumentProcessor proc) {
        
        // set up for validating
        boolean minus = false;
        boolean digits = false;
        int value = 0;
        
        // convert number supplied in argument list to 'value'
        ...
        
        // make sure we have a valid value
        value = minus ? -value : value;
        if (!digits) {
            proc.reportArgumentError(m_char, "Missing value");
        } else if (value < m_min || value > m_max) {
            proc.reportArgumentError(m_char, "Value out of range");
        } else {
            proc.setValue(new Integer(value), m_field);
        }
    }
}

 

结束库

在本文中,我讲述了一个用于处理命令行参数的库的设计过程,作为反射的一个实际的例子。这个库很好地阐明了如何有效地使用反射――它简化应用程序的代码,而且不用明显地牺牲性能。牺牲了多少性能呢?从对我的开发系统的一些快速测试中可以看出,一个简单的测试程序在使用整个库进行了参数处理时比起不带任何参数处理时运行起来平均只慢40毫秒。多出来的这些时间大部分是花在库类和库代码所使用的其他类的装载上,因此,即使是对于那些定义了许多命令行形参和许多实参值的应用程序,也不大可能会比这一结果糟很多。对于我的命令行应用程序,额外的40毫秒根本不能引起我的注意。

通过 参考资源中的链接可以找到完整的库代码。它包括我在本文没有提到的一些特性,包括这样一些细节,比如钩子,用于容易地生成一列格式化的形参标记,还有一些描述,有助于为应用程序提供使用指令。欢迎您在自己的程序中使用这个库,并以任何您发现有用的方式扩展这个库。

现在我已讲过了 第1部分中Java类的基础,也讲过了 第2部分中的 Java Reflection API 的原理以及第3部分,本系列剩下的部分将改变话题,讲讲大家不大熟悉的字节码处理。在第4部分,我将从容易的开始,先看看用于使用二进制类的用户友好的 Javassist 库。您是否想转换方法,但是又不愿在字节码中启动程序呢?Javassist 正好适合您的需求。下个月我们将看看如何实现这一点。

分享到:
评论

相关推荐

    Java编程的动态性

    介绍Java的动态性,涉及反射的很多实例。这里是第二部分,第一部分类的加载也很不错....

    java基础编程必须知道的:SPI、反射、位运算

    这样可以让应用程序在运行时选择不同的实现,从而实现了动态的可扩展性。 典型的用例包括数据库驱动程序的加载、日志框架的实现选择等。例如,在 JDBC 中,不同数据库厂商提供了不同的 JDBC 驱动程序,通过 SPI 机制...

    基础深化和提高-java反射技术

    通过反射,程序可以在运行时检查类、实例化对象、调用方法、获取和设置属性,甚至可以动态修改类的结构。 Java反射技术的核心在于java.lang.reflect包,它提供了一系列类和接口,用于在运行时获取关于类和对象的...

    Java高级程序设计实战教程第三章-Java反射机制.pptx

    Java高级程序设计 第3章 Java反射机制 3.1 应用场景 3.2 相关知识3.3 实施过程 3.4 拓展知识3.5 拓展训练 3.6 课后小结3.7 课后习题 3.8 上机实训 Java高级程序设计实战教程第三章-Java反射机制全文共15页,当前为第...

    JAVA反射机制的简单理解

    有时候我们说某个语言具有很强的动态性,有时候我们会区分动态和静态的不同技术与作法。我们朗朗上口动态绑定(dynamic binding)、动态链接(dynamic linking)、动态加载(dynamic loading)等。然而“动态”一...

    Java算法:车牌识别.zip

    动态性:Java可以通过反射、注解等机制实现在运行时动态加载类和修改行为,增加了程序的灵活性。 综上所述,Java凭借其强大的特性和广泛的适用范围,在企业级应用、互联网服务、移动开发等领域均扮演着举足轻重的...

    java基础的注解和反射的相关知识点总结

    ,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记 ...**这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言(在程序运行的时候可以改变其结构)的关键。

    Java开发详解.zip

    031505_【第15章:Java反射机制】_动态代理笔记.pdf 031506_【第15章:Java反射机制】_工厂设计模式笔记.pdf 031601_【第16章:Annotation】_系统内建Annotation笔记.pdf 031602_【第16章:Annotation】_自定义...

    Java的反射机制讲解案例代码 Class类、 获取类的结构信息:构造函数、方法和字段,动态创建对象、调用方法和设置属性

    适用于有一定Java编程基础的开发人员,希望了解和应用Java反射机制的使用者。 使用场景及目标 使用反射机制的典型场景包括以下几个方面: 在运行时动态加载和创建类对象。 通过反射调用对象的方法和访问对象的字段...

    java_反射实战代码

    反射API可以获取程序在运行时刻的内部结构,反射API提供的动态代理是非常强大的功能,可以原生的实现AOP中的方法拦截功能,反射API就好像在看一个Java类在水中的倒影,知道Java类的内部结构,就可以和它们进行交互,...

    Java 反射机制浅析

    Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和...这一概念的提出很快引发了计算机科学领域关于应用反射性的研究。它首先被程序语言的设计领域所采用,并在Lisp和面向对象方面取得了成绩。

    候捷谈Java反射机制

     有时候我们说某个语言具有很强的动态性,有时候我们会区分动态和静态的不同技术与作法。我们朗朗上口动态绑定(dynamic binding)、动态链接(dynamic linking)、动态加载(dynamic loading)等。然而“动态”一...

    SM2密码算法 JAVA 调用演示程序。.zip

    动态性:Java可以通过反射、注解等机制实现在运行时动态加载类和修改行为,增加了程序的灵活性。 综上所述,Java凭借其强大的特性和广泛的适用范围,在企业级应用、互联网服务、移动开发等领域均扮演着举足轻重的...

    JAVA_API1.6文档(中文)

    java.awt.datatransfer 提供在应用程序之间和在应用程序内部传输数据的接口和类。 java.awt.dnd Drag 和 Drop 是一种直接操作动作,在许多图形用户界面系统中都会遇到它,它提供了一种机制,能够在两个与 GUI 中...

    CS 自学指南(Java编程语言、数据库、数据结构与算法、计算机组成原理、操作系统、计算机网络、英语、简历、面试).zip

    动态性:Java可以通过反射、注解等机制实现在运行时动态加载类和修改行为,增加了程序的灵活性。 综上所述,Java凭借其强大的特性和广泛的适用范围,在企业级应用、互联网服务、移动开发等领域均扮演着举足轻重的...

    《数据结构与算法:Java语言描述》源码.zip

    动态性:Java可以通过反射、注解等机制实现在运行时动态加载类和修改行为,增加了程序的灵活性。 综上所述,Java凭借其强大的特性和广泛的适用范围,在企业级应用、互联网服务、移动开发等领域均扮演着举足轻重的...

    java 编程入门思考

    1.11.4 一个独立的领域:应用程序 1.12 分析和设计 1.12.1 不要迷失 1.12.2 阶段0:拟出一个计划 1.12.3 阶段1:要制作什么? 1.12.4 阶段2:开始构建? 1.12.5 阶段3:正式创建 1.12.6 阶段4:校订 1.12.7 计划的...

    收集优秀的编程资源(Java为主).zip

    动态性:Java可以通过反射、注解等机制实现在运行时动态加载类和修改行为,增加了程序的灵活性。 综上所述,Java凭借其强大的特性和广泛的适用范围,在企业级应用、互联网服务、移动开发等领域均扮演着举足轻重的...

    算法实践(JavaScript &amp; Java),排序,查找、树、两指针、动态规划等.zip

    动态性:Java可以通过反射、注解等机制实现在运行时动态加载类和修改行为,增加了程序的灵活性。 综上所述,Java凭借其强大的特性和广泛的适用范围,在企业级应用、互联网服务、移动开发等领域均扮演着举足轻重的...

    Android 工程师成长之路:JAVA算法的实现,数据结构 和 Android源码笔记等 分享.zip

    动态性:Java可以通过反射、注解等机制实现在运行时动态加载类和修改行为,增加了程序的灵活性。 综上所述,Java凭借其强大的特性和广泛的适用范围,在企业级应用、互联网服务、移动开发等领域均扮演着举足轻重的...

Global site tag (gtag.js) - Google Analytics