Java语言基础特性——第二部分

使用Java5中的类型安全枚举和注解(以及元注解)编程

深入研究安全的枚举类型,并学会在switch语句中正确的使用;然后开始使用Java注解和元注解类型,元注解用来阐明Java代码里注解的作用和功能,例如Target。

在我的Java语言基础特性的第一篇(上)第一篇(下)文章中介绍了断言和泛型,最后对为什么泛型组件是Java5中相当有争议的一个新特性进行了讨论。本文我将介绍Java5另外两个特性:安全的枚举类型和注解,设计它们主要为了提高Java程序的安全性和效率。

安全的枚举类型

枚举指定了一组相关的常量作为它的值,比如说一周的七天、东南西北四个方向、硬币正反面等。
枚举一般由一组顺序的int型常量来表示,比如下面这组方向常量:

    static final int DIR_NORTH = 0;
    static final int DIR_WEST = 1;
    static final int DIR_EAST = 2;
    static final int DIR_SOUTH = 3;

这种方法存在一些问题:

  • 缺乏类型安全性:由于表示枚举的常量只是一个int型整数,所以任何整数都可以作为一个常量,此外,加法、减法以及其他数学运算都可以应用于这些常量上(例如:(DIR_NORTH+DIR_EAST)/DIR_SOUTH ),而这些运算对于枚举来说都是没有意义的。
  • 未提供命名空间:枚举的常量必须以某些特定的标识符(比如DIR_)作为前缀,防止与其他的枚举常量冲突。
  • 脆弱性:编译时,枚举的常量及他们的字面值会保存在类文件中(常量池中),因此,如果需要改变某一常量的值,不仅需要重新编译这些类文件,也需要重新编译依赖于它们的应用类文件。否则,运行时会发生未定义错误。
  • 缺乏可读性:打印一个常量时,只会输出它的整数值,但仅仅通过这个整数值,无法得知它的意义,甚至也无法判断它是属于哪个枚举。

认识到枚举类型传统实现模式的问题,开发者使用了一个基于类的实现方式,即安全的枚举类型模式。这种模式已经被广泛地描述和讨论。Joshua Bloch曾在Effiective Java Programming Language Guide(Addison-Wesley,2001)这本书的21章介绍过该模式,并指出了它存在的一些问题,他认为把安全的枚举类型的常量放进集合中是不合适的,而且这种枚举常量也不能在switch语句中使用。
为了正确的理解安全的枚举类型模式,请看如下代码片段。Suit 类声明了一个枚举类型,该类型描述了四种扑克花色(梅花、方片、红桃和黑桃):

    public final class Suit // 不能被继承
    {
       public static final Suit CLUBS = new Suit();
       public static final Suit DIAMONDS = new Suit();
       public static final Suit HEARTS = new Suit();
       public static final Suit SPADES = new Suit();
       private Suit() {} // 不能增加新的常量
    }

使用这个类时,需要引入一个Suit类型的变量,并且将 Suit类的某个常量值赋值给这个变量,如下所示:

    Suit suit = Suit.DIAMONDS;

在switch语句中,判断变量suit的值时,可能通过如下方式:

    switch (suit){
       case Suit.CLUBS   : System.out.println("clubs"); break;
       case Suit.DIAMONDS: System.out.println("diamonds"); break;
       case Suit.HEARTS  : System.out.println("hearts"); break;
       case Suit.SPADES  : System.out.println("spades");
    }

当执行到Suit.CLUBS时,编译器会报错:需要一个常量表达式,此时可能对代码进行如下修改:

    switch (suit)
    {
       case CLUBS   : System.out.println("clubs"); break;
       case DIAMONDS: System.out.println("diamonds"); break;
       case HEARTS  : System.out.println("hearts"); break;
       case SPADES  : System.out.println("spades");
    }

然而,当运行到CLUBS时,编译器仍然会报错:找不到变量值。
安全的枚举类型模式不能运用于switch语句。但是,我们可以使用Java5中新引入的安全的枚举类型,它不仅解决了这种模式的问题,同时也继承了这种模式的优点。

在switch语句中使用安全的枚举类型
在java代码中声明一个简单的安全的枚举类型与在C ,C++及C#中的声明类似:

    enum Direction { NORTH, WEST, EAST, SOUTH }

上述声明使用了关键字enum来定义Direction这个枚举类,这个类里可以随意的增加方法、实现接口。枚举常量NORTH,WEST,EAST,SOUTH作为特定的常量类主体,通过定义的继承了封装Direction的匿名类进行了实现。
类Direction和其他枚举类一样,都继承了原始的枚举类:java.lang.Enum<E extends Enum<E>>.Enum枚举类继承了java.lang.Enum<E extends Enum<E>>的各种方法,包括对java.lang.Ojbect类中方法(比如public Strins toString())的高质量实现。同时,Java编译器也会为类Direction自动生成某些成员方法(如values()).
注意:由于Enum<E extends Enum<E>> 实现了java.lang.Comparable 接口,因此在同一个枚举类中,两个枚举常量可根据类里常量声明的顺序进行大小的比较。
示例1中,应用中声明使用了前面的Direction枚举类,如下:
示例1:TEDemo.java(版本1)

    public class TEDemo
    {
       enum Direction { NORTH, WEST, EAST, SOUTH }
       public static void main(String[] args)
       {
          for (int i = 0; i < Direction.values().length; i++)
          {
             Direction d = Direction.values()[i];
             System.out.println(d);
             switch (d)
             {
                case NORTH: System.out.println("Move north"); break;
                case WEST : System.out.println("Move west"); break;
                case EAST : System.out.println("Move east"); break;
                case SOUTH: System.out.println("Move south"); break;
                default   : assert false: "unknown direction";
             }
          }
          System.out.println(Direction.NORTH.compareTo(Direction.SOUTH));
       }
    }

示例1 中声明了枚举类Drieciton,并且通过values()方法迭代的取出Direction中的常量值。变量d被赋值为Direction中的常量值,switch语句根据d的值执行不同的分支,进而输出相应的信息。(这样就不需要在每个常量值前加上枚举类型的前缀)
编译运行示例1(javac TEDemo.java,java TEDemo),输出为:

    NORTH
    Move north
    WEST
    Move west
    EAST
    Move east
    SOUTH
    Move south
    -3

由输出可见:继承的toString()方法返回的是枚举常量的名字,根据比较的结果可知,在Direction枚举类中,NORTH在SOUTH之前声明。

在类型安全的枚举中增加数据和方法
在类型安全的枚举中可以增加数据和方法,例如,引入一个枚举类用来表明加拿大货币,同时这个类必须提供一个方法,该方法返回任意面额的便士换算成5加分、10加分、25加分或者100加分硬币的数量。示例2实现了这一功能:
示例2 TEDemo.java(版本2)

    enum Coin
    {
       NICKEL(5),   // 必须最先声明常量
       DIME(10),
       QUARTER(25),
       DOLLAR(100); //常量声明以分号结束
       private final int valueInPennies;
       Coin(int valueInPennies)
       {
          this.valueInPennies = valueInPennies;
       }
       int toCoins(int pennies)
       {
          return pennies/valueInPennies;
       }
    }
    public class TEDemo
    {
       public static void main(String[] args)
       {
          if (args.length != 1)
          {
              System.err.println("usage: java TEDemo amountInPennies");
              return;
          }
          int pennies = Integer.parseInt(args[0]);
          for (Coin coin: Coin.values())
               System.out.println(pennies+" pennies contains "+
                                  coin.toCoins(pennies)+" "+
                                  coin.toString().toLowerCase()+"s");
       }
    }

示例2中,Coin枚举类声明了一个构造方法,该方法包含一个参数,该参数为硬币的面额。传入参数值时,通过Coin中的int toCoins(int pennies) 方法,能计算出任意面额的便士换算成不同面额硬币的数量。例如:编译运行示例2(javac TEDemo.java java TEDemo 198),输出如下:

    198 pennies contains 39 nickels
    198 pennies contains 19 dimes
    198 pennies contains 7 quarters
    198 pennies contains 1 dollars

注意:示例2中使用了java5中的加强for循环:for (Coin coin: Coin.values())。 Coin.values()返回Coin实例的数组,这种for循环能够很方便的取得每一个Coin实例,并将其赋值给coin变量。在第三部分,我将进一步说明加强for循环。
上面对switch中使用安全的枚举类型及在安全的枚举类型中增加数据和方法进行了一个快速的介绍。关于使用安全的枚举类型的更多内容可参考Oracle的JDK 5.0 Enum documentation中的Java Collections Framework部分,Dustin Marx在JavaWorld也写了一篇非常有用的文章using Java enums for unit conversions对其进行了介绍。

注解

在Java应用程序中,可能需要通过相关的元数据(描述其他数据的数据)来注解某些元素。Java 一直以来通过transient保留字提供一种特定的注解机制:被注解的属性不会被序列化。但在Java 5之前,一直没有一种标准的方法来注解程序的元素。
Java 5的注解机制包含四个部分:

  1. @interface:声明注解类型。
  2. 元注解类型:用以标识应用某一注解类型的应用元素;用以标识注解(注解类型的一个实例)的生命期;等等;
  3. 通过扩展Java 反射API来支持对注解的处理,同时介绍处理注解的一般工具:使用反射,能够获得程序运行时的注解。
  4. 标准的注解类型。

接下来,我将要介绍如何使用这些功能,在以下的例子里也会指出注解的一些挑战。

通过@interface声明注解类型
通过@后紧跟interface保留字及类名来声明一个注解类型。示例3定义了一个简单的注解类型,使用这个注解来表明这段代码是线程安全的。
示例3 ThreadSafe.java

    public @interface ThreadSafe
    {
    }

声明了注解类型后,可以在认为线程安全的方法头前加上 “@类型名”生成实例进行标注。示例4中,在main()前面加上了@ThreadSafe注解。
示例4.AnnDemo.java(版本1)

    public class AnnDemo
    {
       @ThreadSafe
       public static void main(String[] args)
       {
       }
    }

与其他注解类型名相比较而言,ThreadSafe实例并没有提供任何元数据。但是,可以通过在类型里增加元素来提供元数据,这些元素可以作为方法头放在注解类型的类体中。

注解类型里的元素没有任何的代码体,同时有如下限制:
1. 方法头不能含有参数
2. 方法头不能使用throws语句
3. 方法头返回类型必须为原始数据类型(如int型,java.langString,java.lang.Class,枚举,注解类型,或者这些类型的数组。其他的类型不能作为其返回类型。

下面的例子,示例5定义了一个Todo注解类型,该类型包含三个元素,id,finishDate()(完成日期),coder()负责人。
示例5 ToDo.java(版本1)

    public @interface ToDo
    {
       int id();
       String finishDate();
       String coder() default "n/a";
    }

注意:每一个元素都没有参数或者throws语句,都有一个合法的返回类型(int,String),以分号结束。最后一个元素指明了一个默认值,当注解没有指定值给这个元素时返回默认值。
示例6使用了ToDo注解一个未完成的类的方法。
示例6. AnnDemo.java (版本2)

    public class AnnDemo
    {
       public static void main(String[] args)
       {
          String[] cities = { "New York", "Melbourne", "Beijing", "Moscow",
                              "Paris", "London" };
          sort(cities);
       }
       @ToDo(id=1000, finishDate="10/10/2013", coder="John Doe")
       static void sort(Object[] objects)
       {
       }
    }

示例6中给每个元素都指明了一个值,例如Id为1000,id和finishDate的值必须指定,coder的值可以不指定,否则编译器会报错。当没有指定coder的值时,会取它的默认值。
Java提供了一个特殊的元素,String value(),该方法返回元素值的列表(以逗号分隔),示例7为ToDo的重构版本。
示例7. ToDo.java (版本2)

    public @interface ToDo
    {
       String value();
    }

当value()是注解类里唯一的元素时,给这个元素赋值时,可以省略=操作符,如示例8所示,运用了两种方法赋值:
示例8 AnnDemo.java (版本3)

    public class AnnDemo
    {
       public static void main(String[] args)
       {
          String[] cities = { "New York", "Melbourne", "Beijing", "Moscow",
                              "Paris", "London" };
          sort(cities);
       }
       @ToDo(value="1000,10/10/2013,John Doe")
       static void sort(Object[] objects)
       {
       }
       @ToDo("1000,10/10/2013,John Doe")
       static boolean search(Object[] objects, Object key)
       {
          return false;
       }
    }

使用元注解类型—灵活性的问题
注解可以在类、方法、本地变量等各种情况下使用,但是灵活性却是一个大问题。例如,如果需要严格限制ToDo必须作用于方法上,但将它作用于其他元素上也是可以的。如示例9所示:
示例9. AnnDemo.java (版本4)

    @ToDo("1000,10/10/2013,John Doe")
    public class AnnDemo
    {
       public static void main(String[] args)
       {
          @ToDo(value="1000,10/10/2013,John Doe")
          String[] cities = { "New York", "Melbourne", "Beijing", "Moscow",
                              "Paris", "London" };
          sort(cities);
       }
       @ToDo(value="1000,10/10/2013,John Doe")
       static void sort(Object[] objects)
       {
       }
       @ToDo("1000,10/10/2013,John Doe")
       static boolean search(Object[] objects, Object key)
       {
          return false;
       }
    }

在示例9中,Todo被作用于AnnDemo类和cities本地变量上。这些错误的注解可能会使复审代码的人感到困惑,甚至对自己的代码处理工具也会产生误解。为了解决这一问题,java的Target注解类型(在java.lang.annotation包中)应运而生。
Target是一种元注解类型,主要用来标识注解应用于的程序的元素类型。程序的元素类型是通过Target的ElementValue[] value()方法来标识的。
Java.lang.annotaion.ElementType是一个枚举类,它的常量值描述了程序的元素类型。例如,CONSTRUCTOR表明的是构造器方法,而PARAMETER表明的是参数。示例10重构了示例7中的ToDo注解类,限制其只能作用于方法上。
示例10. ToDo.java (版本3)

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Target;
    @Target({ElementType.METHOD})
    public @interface ToDo
    {
       String value();
    }

如果使用重构的ToDo注解类型,编译示例9时,会出现以下错误:

    AnnDemo.java:1: error: annotation type not applicable to this kind of
    declaration
    @ToDo("1000,10/10/2013,John Doe")
    ^
    AnnDemo.java:6: error: annotation type not applicable to this kind of
    declaration
          @ToDo(value="1000,10/10/2013,John Doe")
          ^
    2 errors

额外的元注解类型

Java5在包java.lang.annotation中提供了三种额外的元注解类型:

  • Retention用来表明注解保留的时间。这一类型与java.lang.annotaion.RetentionPolicy这一枚举类型相关,其中枚举类的常量有三个值:CLASS,RUNTIME,SOURCE。CLASS 表明编译器会在class类文件里记录注解,java虚拟机为了节省内存不会保留注解,CLASS为默认的策略;RUNTIME表明编译器会在class类文件里记录注解,同时java虚拟机也会保留注解;SOURCE表明编译器不记录注解。
  • Documented表明使用javadoc和其他类似的工具时,注解将被保留
  • Inherited表明注解类自动被子类继承。

注解的处理
注解一定会被处理,否则使用注解没有意义。Java 5 继承了反射API以助于创建自己的注解处理工具。例如,Class声明了一个Annotation[] getAnnotations()方法返回一个java.lang.Annotaion实例的数组,这些实例为Class的元素中所有的注解。
示例11为一个简单的应用:加载类文件,询问方法里是否有ToDo注解,如果有的话,输出其内容。
示例11. AnnProcDemo.java

    import java.lang.reflect.Method;
    public class AnnProcDemo
    {
       public static void main(String[] args) throws Exception
       {
          if (args.length != 1)
          {
             System.err.println("usage: java AnnProcDemo classfile");
             return;
          }
          Method[] methods = Class.forName(args[0]).getMethods();
          for (int i = 0; i < methods.length; i++)
          {
             if (methods[i].isAnnotationPresent(ToDo.class))
             {
                ToDo todo = methods[i].getAnnotation(ToDo.class);
                String[] components = todo.value().split(",");
                System.out.printf("ID = %s%n", components[0]);
                System.out.printf("Finish date = %s%n", components[1]);
                System.out.printf("Coder = %s%n%n", components[2]);
             }
          }
       }
    }

当验证到命令行参数指明了类文件时,main()通过Class.forName()加载这个类文件,再通过getMethod()返回类文件里所有的public方法,返回值为java.lang.reflect.Method的数组,取得所有的public方法后,再逐一进行处理。
对方法进行处理时,通过Method的boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) 判断这方法是否使用了ToDo,如果是,通过Method的 T getAnnotation(Class annotationClass)方法来取得这个注解。
只有那些声明了String value()方法的ToDo(示例7)注解被处理。由于String value()返回的是以逗号分隔的字符串,因此取值时需要将返回的字符串以逗号分隔成数组,再进行处理和输出。
通过javac AnnProcDemo.java编译源代码。在运行前,需要一个合适的类文件,该文件的public方法上含有@ToDo注解。例如,修改示例8中的AnnDemo源代码,在其sort()和search()方法头上加上public。运行前,还需要示例12中的ToDo注解类,指明其注解保留策略为RUNTIME。
示例 12. ToDo.java (版本4)

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ToDo
    {
       String value();
    }

编译修改的AnnDemo.java和示例12,通过以下命令处理AnnDemo的ToDo注解:
java AnnProcDemo AnnDemo
正常情况下,会得到如下输出:

    ID = 1000
    Finish date = 10/10/2013
    Coder = John Doe
    ID = 1000
    Finish date = 10/10/2013
    Coder = John Doe

一般的注解处理
java 5 引用了一个apt工具来对注解进行处理。这个工具并没有在java 7和java 8中继续使用。apt的功能目前已经迁移至javac 编译器,java 6及以后的版本都具有这个功能。更多的java中的注解处理请看原作者的文章:
“Build your own annotation processors”

标准的注解类型
除了 Target, Retention, Documented, 和 Inherited这几种,java5 还引用了java.lang.Deprecated,java.lang.Override, 和 java.lang.SuppressWarnings三种注解类型。这三种注解类型只能应用于编译器上下文中,他们的注解保留策略为SOURCE。
Deprecated注解类型将被淘汰。这种类型的注解不应该继续使用,当编译器发现Deprecated注解时,会发出警告。
下面的java.util.Date构造器使用了Deprecated注解:

    @Deprecated
    public Date(int year, int month, int date, int hrs, int min)
    {
       // ... body of this constructor
    }

当子类准备覆盖父类的方法时,可以在子类的相应方法上添加Override注解。如果添加了Override注解但没有覆盖父类方法,编译器会报错。
下面的例子中,java.lang.Runnable接口的 public void run() 方法被一个匿名类覆盖时,使用了Override注解。

    Runnable r = new Runnable()
                 {
                    @Override
                    public void run()
                    {
                       // ... body of this method
                    }
                 };

SuppressWarnings表明在注解的元素(以及包含在注解元素中的所有程序元素)取消显示指定的编译器警告(如deprecation和unchecked警告)。
下面的例子使用了SuppressWarnings来取消 显示在E[]上的unchecked的警告,由于注解不能应用于表达式,因此该注解放在构造器上:

    public class Container<E>
    {
       private E[] elements
       // ...
       @SuppressWarnings("unchecked")
       public Container<E>(int size)
       {
          // ...
          elements = (E[]) new Object[size];
       }
       // ...
    }

取消显示unchecked警告一定要小心。首先证明了一个 ClassCastException不能被抛出,然后提供了 @SuppressWarnings(“unchecked”)注解抑制警告。

总结

与泛型一样,安全的枚举类型是为了提高java程序的安全性,而注解主要是用于提高开发者的效率。这篇文章简单的介绍了每一种新特性基本的使用方法。这个系列的下一篇文章将介绍Java 5中一些与效率相关的特性——即,自动拆装箱,加强的循环,静态引入,可变参数和协变的返回类型。以后会对java 7中的新的语言特性进行介绍。

原文链接: javaworld 翻译: ImportNew.com - will
译文链接: http://www.importnew.com/11053.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: will

(新浪微博:@Fighter_D_Will

查看will的更多文章 >>



可能感兴趣的文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部