`
jspengxue
  • 浏览: 181010 次
  • 性别: Icon_minigender_1
  • 来自: 天堂和地狱的中间
社区版块
存档分类
最新评论

Java 编程的动态性,第3部分: 应用反射

阅读更多
  命令行参数处理是一项令人厌烦的零碎工作,不管您过去已经处理过多少次了,它好像总能重新摆在您的面前。与其一遍又一遍地编写同一块代码的不同变种,为什么不利用反射来简化参数处理的工作呢?Java 顾问 Dennis Sosnoski 向您展示了如何做到这一点。在本文中,Dennis 简明扼要地介绍了一个开源库,这个库可以使得命令行参数实际上自己处理自己。
在 上个月的文章中,我介绍了Java Reflection API,并简要地讲述了它的一些基本功能。我还仔细研究了反射的性能,并且在文章的最后给出了一些指导方针,告诉读者在一个应用程序中何时应该使用反射,何时不应该使用反射。在本月这一期的文章中,我将通过查看一个应用程序来更深入地讨论这一问题,这个应用程序是用于命令行参数处理的一个库,它能够很好地体现反射的强项和弱点。

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

定义问题


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

一开始只有一个或者两个参数,按照某种特定的顺序排列。
考虑到这个应用程序有更多的事情要做,于是添加更多的参数。
厌倦了每次都输入所有的参数,于是让一些参数成为可选的参数,让这些参数带有默认的值。
忘记了参数的顺序,于是修改代码,允许参数以任何顺序排列。
将这个应用程序交给其他感兴趣的人。但是他们并不知道这些参数各自代表什么,于是又为这些参数添加更完善的错误检查和“帮助”描述。
当我进入到第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、 float 和 java.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() 方法在解释完实参值之后,又调用 ArgumentProcessor 的 setValue() 方法。清单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 编程思想中文第三版(PDF)

    《Java编程思想》是Java开发领域的一本经典著作,由Bruce Eckel撰写,中文第三版则是针对中国读者的翻译版本,旨在帮助读者深入理解和掌握Java编程语言。这本书以清晰、简洁的语言介绍了Java的核心概念和技术,适合...

    02-课件:第1部分:Java基础编程.zip

    本课件“课件:第1部分:Java基础编程”将详细讲解以上知识点,结合项目实例和源代码,使学习者能够快速上手,深入理解Java编程的精髓。通过实践,学习者将能熟练运用Java进行程序开发,为后续的进阶学习打下坚实...

    侯捷-java编程思想.pdf

    《侯捷-Java编程思想》是一本深受Java开发者喜爱的经典著作,尽管是繁体版本,但其中也包含英文内容,方便不同语言背景的读者理解。这本书深入浅出地讲解了Java编程的核心概念和技术,旨在帮助读者掌握Java编程的...

    java编程思想第四版源代码.7z

    《Java编程思想》是 Bruce Eckel 的经典著作,第四版更是深入浅出地介绍了Java语言的核心概念和技术。这个压缩包包含的源代码是书中的示例程序,它们旨在帮助读者理解书中阐述的各种编程原理和实践。通过分析这些源...

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

    这些特性都是Java强大灵活性的关键组成部分。 SPI(Service Provider Interface)是Java的一种服务发现机制,它允许程序在运行时动态加载不同的服务提供者。通过定义接口,开发者可以创建可扩展的系统,而具体的...

    JAVA编程第2 版

    ### JAVA编程第2版知识点概览 #### 一、Java编程概述 - **定义与特点**:Java是一种广泛使用的高级编程语言,由Sun Microsystems开发并于1995年首次发布。它具有简单、面向对象、分布式、解释型、健壮、安全、结构...

    Java 3D与计算机三维动态图形网络编程设计

    这个“Java 3D与计算机三维动态图形网络编程设计”主题深入探讨了如何利用Java 3D技术进行创新性的网络编程,尤其是在构建动态三维图形应用方面。 在计算机图形学领域,三维动态图形是极具吸引力的一部分,因为它们...

    《Java编程思想》第四版答案

    8. 反射:Java反射API允许程序在运行时检查和修改自身的结构和行为,这在动态配置、元编程等场景下非常有用。 9. 注解(Annotation):注解为编译器和运行时提供了元数据,用于代码的编译时检查、运行时处理等。了解...

    JAVA编程思想第三版类包

    《JAVA编程思想第三版》是Java编程领域里一本经典的著作,由...这个压缩包可能包含上述所有或部分知识点的代码示例,通过对这些示例的分析和实践,读者可以更好地理解和掌握《JAVA编程思想第三版》中的概念和技巧。

    java 编程思想 第五版 word版

    以上只是《Java编程思想》第五版可能涵盖的部分知识点,实际内容会更为丰富,包括设计模式、并发编程、Java虚拟机(JVM)工作原理等高级主题,旨在帮助读者全面理解和掌握Java编程。通过阅读此书,读者不仅可以学习...

    02-课件:第2部分:Java高级编程.zip

    在本课程"02-课件:第2部分:Java高级编程.zip"中,宋红康老师深入讲解了Java语言的高级编程概念和技术。这个压缩包包含了一系列课件,旨在帮助学习者掌握Java编程的精髓,提升编程技能。以下是根据课程内容提炼出的...

    Java习题_java编程_

    Java编程是计算机科学领域中最广泛使用的编程语言之一,尤其在企业级应用开发中占据着核心地位。本资源针对Java语言的基础部分提供了习题训练,主要覆盖了大学Java教材第八章的内容,旨在帮助学习者巩固和深化对Java...

    java 编程思想第四版习题答案

    《Java编程思想第四版》是Java编程领域里一本经典的教程,由 Bruce Eckel 所著。这本书深入浅出地介绍了Java语言的核心概念和技术,包括面向对象编程、异常处理、集合框架、多线程、网络编程等多个方面。对于学习...

    计算机后端-Java-Java核心基础30天全套教程7_每日一考与复习第1部分:Java基础编程.zip

    以上知识点构成了Java基础编程的核心内容,通过这套30天的教程,学习者应该能够系统地掌握Java编程的基础,并具备解决实际问题的能力。同时,每日一考与复习的设计有助于巩固学习成果,提升编程实践能力。

    Java编程思想源代码及课后练习代码

    8. **反射**:Java的反射机制可以在运行时检查类、接口、字段和方法的信息,动态创建对象并调用方法,这在插件化开发、框架设计中广泛应用。 9. **注解(Annotation)**:注解提供了一种元数据,用于向编译器或JVM...

    JAVA的反射机制与动态代理

    Java的反射机制与动态代理是Java编程中两个非常重要的高级特性,它们在许多实际场景中发挥着关键作用,如框架开发、插件系统、元数据处理等。下面将详细讲解这两个概念及其应用。 首先,Java的反射机制允许我们在...

    java编程深入研究

    《Java编程深入研究》涵盖了Java开发的多个重要方面,旨在帮助开发者从基础到高级,全面理解和掌握这门广泛使用的编程语言。在这个过程中,我们将探讨Java的环境配置、服务器的运用,以及一系列实用示例。 首先,...

    Java完美编程(第三版)

    《Java完美编程(第三版)》是一本专为Java初学者和进阶者设计的教程,旨在帮助读者全面掌握Java编程技术。这本书以其易学习、易上手的特点,深受广大编程爱好者喜爱。以下是对该书内容的详细解读: 1. **Java语言...

    java核心编程第十版示例代码

    泛型在集合框架中的应用尤为广泛,理解其原理和使用方法是现代Java编程的必备技能。 6. **I/O流**:Java的I/O流系统用于读写数据,包括文件操作、网络通信等。了解各种流的分类(如字节流、字符流、输入流、输出流...

    Java编程郭广军PPT

    Java编程是计算机科学领域的重要部分,尤其在企业级应用开发中占据主导地位。"Java编程郭广军PPT"提供了一套完整的Java学习资源,分为三个部分,涵盖从基础到高级的面向对象编程概念。以下是根据提供的文件名称列表...

Global site tag (gtag.js) - Google Analytics