分享

说说Java中的枚举(一)

 贾朋亮博客 2015-05-19
在实际编程中,往往存在着这样的“数据集”,它们的数值在程序中是稳定的,而且“数据集”中的元素是有限的。例如星期一到星期日七个数据元素组成了一周的“数据集”,春夏秋冬四个数据元素组成了四季的“数据集”。在Java中想表示这种数据集最容易想到的写法可能是这样,我们以表示一周五天的工作日来举例:

Java代码

public class WeekDay {

        public static final int MONDAY = 1;

        public static final int TUESDAY = 2;

        public static final int WENSDAY = 3;

        public static final int THURSDAY = 4;

        public static final int FRIDAY = 5;

}

 

     现在,你的类就可以使用像WeekDay.TUESDAY这样的常量了。但是这里隐藏着一些问题,这些常量是Javaint类型的常量,这意味着该方法可以接受任何int 类型的值,即使它和WeekDay中定义的所有日期都对应不上。因此,您需要检测上界和下界,在出现无效值的时候,可能还要抛出一个IllegalArgumentException。而且,如果后来又添加另外一个日期(例如WeekDay.SATURDAY ),那么必须改变所有代码中的上界,才能接受这个新值。换句话说,在使用这类带有整型常量的类时,这个方案也许可行,但并不是非常有效。

     Joshua Bloch老大这时站了出来,在他的著作《Effective Java》中提出了在这样场景下的一种非常好的模式——Type-safe enumeration pattern。这个模式简而言之就是给这样的常量类一个私有的构造方法,然后声明一些public static final 的同类型的变量暴露给使用者,例如:

Java代码

public class WeekDay {

         public static final WeekDay MONDAY = new WeekDay(1);

         public static final WeekDay TUESDAY = new WeekDay(2);

         public static final WeekDay WENSDAY = new WeekDay(3);

         public static final WeekDay THURSDAY = new WeekDay(4);

         public static final WeekDay FRIDAY = new WeekDay(5);

        

         public int getValue(){

                   return value;

         }

        

         private int value;

        

         private WeekDay(int i){

                   this.value = i;

         }

         //other methods...

}

     这样做的好处是你的程序现在接受的不是int类型的数据了,而是WeekDay的一个预定义好的static final的实例(WeekDay.TUESDAY等 ),例如:

Java代码

public void setWeekDay(WeekDay weekDay){...}

     而这样做也避免了可以随意向方法中传递一个不合法的int型数值(例如-1)而造成程序错误。同时,它还会带来其他的一些好处:由于这些枚举的对象都是一 些类的实例,所以在里面放一些需要的属性来存放数据;又由于他们都是单例的,你可以使用equals方法或是==符号来比较它们。

Joshua Bloch大大提出的枚举模式,很好用但是好麻烦啊。如果你用过C/C++或是Pascal这样的语言的话一定会对它们的枚举类型有印象,例如在C/C++中我们可以这样定义:

C/C++代码

enum weekday {

   MONDAY,

   TUESDAY,

   WENSDAY,

   THURSDAY,

   FRIDAY

};


    然后在程序中就可以用MONDAY、TUESDAY这些变量了。这样多方便,但是Java 1.4以前的版本并没有提供枚举类型的支持,所以如果你是用JDK 1.4开发程序的话就只能像Joshua Bloch老大那样写了。从Java 5.0(代号为Tiger)开始,这种情况改变了,Java从语言层面支持了枚举类型。

    枚举是Tiger的一个很重要的新特性,它是一种新的类型,允许用常量来表示特定的数据片断,而且全部都以类型安全的形式来表示,它使用“enum”关键字来定义。

    我们先来写一个简单的枚举类型的定义:

 

Java代码

public enum WeekDay{

         MONDAY, TUESDAY, WENSDAY, THURSDAY, FRIDAY; //最后这个“;”可写可不写。

}

    这和类、接口的定义很相像嘛!Tiger中的枚举类型就是一种使用特殊语法“enum”定义的类。所有的枚举类型是java.lang.Enum的子类。 这是Tiger中新引入的一个类,它本身并不是枚举类型,但它定义了所有枚举类型所共有的行为,如下表:

 

 

 

注意:虽然所有的枚举类型都继承自java.lang.Enum,但是你不能绕过关键字“enum”而使用直接继承Enum的方式来定义枚举类型。编译器会提示错误来阻止你这么做。

    WeekDay中定义的五个枚举常量之间使用“,”分割开来。这些常量默认都是“public static final”的,所以你就不必再为它们加上“public static final”修饰(编译器会提示出错),这也是为什么枚举常量采用大写字母来命名的原因。而且每一个常量都是枚举类型WeekDay的一个实例。你可以通 过类似“WeekDay.MONDAY”这种格式来获取到WeekDay中定义的枚举常量,也可以采用类似“WeekDay oneDay = WeekDay.MONDAY”的方式为枚举类型变量赋值(你不能给枚举类型变量分配除了枚举常量和null以外的值,编译器会提示出错)。

    作为枚举类型实例的枚举常量是如何初始化的呢?其实答案很简单,这些枚举常量都是通过Enum中定义的构造函数进行初始化的。

Java代码

//java.lang.Enum中定义的构造函数,

//两个参数分别是定义的枚举常量名称以及它所在的次序。

protected Enum(String name, int ordinal) {

                   this.name = name;

                   this.ordinal = ordinal;

}

    在初始化的过程中,枚举常量的次序是按照声明的顺序安排的。第一个枚举常量的次序是0,依此累加。

    枚举类型除了拥有Enum提供的方法以外,还存在着两个隐藏着的与具体枚举类型相关的静态方法——values()和valueOf(String arg0)。方法values()可以获得包含所有枚举常量的数组;方法valueOf是java.lang.Enum中方法valueOf的简化版本,你可以通过它,根据传递的名称来得到当前枚举类型中匹配的枚举常量。

我们来看一个枚举类型使用的小例子。需求中要求可以对指定的日期进行相应的信息输出。对于这么简单的需求,这里就使用枚举类型来进行处理。前面我们已经定义好了包含有五个工作日的枚举类型。下面的代码则是进行输出的方法:

 

Java代码

/**

         * 根据日期的不同输出相应的日期信息。

         * @param weekDay     代表不同日期的枚举常量

         */

         public void printWeekDay(WeekDay weekDay){

            switch(weekDay){

             case MONDAY:

                System.out.println(“Today is Monday!”);

break;

             case TUESDAY:

                System.out.println(“Today is Tuesday!”);

break;

             case WENSDAY:

                System.out.println(“Today is Wensday!”);

break;

             case THURSDAY:

                System.out.println(“Today is hursday!”);

break;     
             case FRIDAY:

                System.out.println(“Today is Friday!”);

break;     

             default:
                throw new AssertionError("Unexpected value: " + weekDay);

                   }

         }

    在Tiger以前,switch操作仅能对int、short、char和byte进行操作。而在Tiger中,switch增加了对枚举类型的支持,因 为枚举类型仅含有有限个可以使用整数代替的枚举常量,这太适合使用switch语句了!就像上面代码中那样,你在swtich表达式中放置枚举类型变量, 就可以在case标示中直接使用枚举类型中的枚举常量了。

注意:case标示的写法中没有枚举类型前缀,这意味着不能将代码写成 case Operation. PLUS,只需将其写成 case PLUS即可。否则,编译器会提示出错信息。

    像上面的例子一样,虽然你已经在case标示中穷尽了某个枚举类型中的所有枚举常量,但还是建议你在最后加上default标示(就像上面代码示意的那 样)。因为万一为枚举类型添加一个新的枚举常量,而忘了在switch中添加相应的处理,是很难发现错误的。

为了更好的支持枚举类型,java.util中添加了两个新类:EnumMap和EnumSet。使用它们可以更高效的操作枚举类型。下面我一一介绍给你:

    EnumMap是专门为枚举类型量身定做的Map实现。虽然使用 其它的Map实现(如HashMap)也能完成枚举类型实例到值得映射,但是使用EnumMap会更加高效:它只能接收同一枚举类型的实例作为键值,并且 由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值。这使得EnumMap的效率非常高。

    提示:EnumMap在内部使用枚举类型的ordinal()得到当前实例的声明次序,并使用这个次序维护枚举类型实例对应值在数组的位置。

    下面是使用EnumMap的一个代码示例。枚举类型DataBaseType里存放了现在支持的所有数据库类型。针对不同的数据库,一些数据库相关的方法需要返回不一样的值,示例中getURL就是一个。

 

Java代码

//现支持的数据库类型枚举类型定义

public enum DataBaseType{

                   MYSQL,ORACLE,DB2,SQLSERVER

}

//某类中定义的获取数据库URL的方法以及EnumMap的声明。

……

private EnumMap<DataBaseType ,String> urls =

new EnumMap<DataBaseType ,String>(DataBaseType.class);

                  

public DataBaseInfo(){

         urls.put(DataBaseType.DB2,"jdbc:db2://localhost:5000/sample");

         urls.put(DataBaseType.MYSQL,"jdbc:mysql://localhost/mydb");

         urls.put(DataBaseType.ORACLE,"jdbc:oracle:thin:@localhost:1521:sample");

         urls.put(DataBaseType.SQLSERVER,"jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=mydb");

}

/**

* 根据不同的数据库类型,返回对应的URL

* @param type     DataBaseType枚举类新实例

* @return

*/

public String getURL(DataBaseType type){

         return this.urls.get(type);

}

    在实际使用中,EnumMap对象urls往往是由外部负责整个应用初始化的代码来填充的。这里为了演示方便,类自己做了内容填充。

    像例子中那样,使用EnumMap可以很方便的为枚举类型在不同的环境中绑定到不同的值上。如:例子中getURL绑定到URL上,在其它的代码中可能又被绑定到数据库驱动上去。

    EnumSet是枚举类型的高性能Set实现。它要求放入它的枚举常量必须属于同一枚举类型。EnumSet提供了许多工厂方法以便于初始化,见下表:

 

    EnumSet作为Set接口实现,它支持对包含的枚举常量的遍历:

Java代码

for(Operation op : EnumSet.range(Operation.PLUS , Operation.MULTIPLY)) {

                   doSomeThing(op);

}


到目前为止,我们仅仅使用了最简单的语法定义枚举类型,其实枚举类型可以做更多的事情,在Tiger的定义中,枚举是一种新的类型,允许用常量来表示特定 的数据片断,它能胜任普通类的大部分功能,如定义自己的构造函数、方法、属性等等。这也是Java与C/C++或是Pascal中不同的地方,在那两种语 言中枚举类型代表的就是一些int类型的数字,但在Java中枚举更像是一个类。

    接下来我们将丰富一下我们的枚举类型。

 前面定义了包含五个工作日的枚举类型,但是真正在每个工作日进行操作的动作是在其它类中的printWeekDay方法中进行的。假设我们经过分析发现对工作日的操作应该属于枚举类型WeekDay的职责,那我们就可以把枚举类型改造如下:

 

Java代码

public enum WeekDay {

                   MONDAY, TUESDAY, WENSDAY, THURSDAY, FRIDAY;

        

                   /**

                * 根据工作日的不同打印不同的信息。

                */

                 public void printWeekDay(){

                  switch(this){

                   case MONDAY:

System.out.println(“Today is Monday!”);

break;

                   case TUESDAY:

System.out.println(“Today is Tuesday!”);

break;

                   case WENSDAY:

System.out.println(“Today is Wensday!”);

break;

                   case THURSDAY:

System.out.println(“Today is Thursday!”);

break;    

   case FRIDAY:

System.out.println(“Today is Friday!”);

break;    

                   default:

                     throw new AssertionError("Unexpected value: " + this);

                    }

               }

}

//测试程序

for(WeekDay weekDay: EnumSet.allOf(WeekDay.class)){

          System.out.println("the message is : "+weekDay.printWeekDay());

         }

    现在的枚举类型Operation变得丰满多了,我们在枚举类型WeekDay中增加了一个printWeekDay方法,你也可以用 WeekDay.MONDAY.printWeekDay()方法来进行信息的输出了。

    枚举类型也允许定义自己的构造函数,这使得枚举常量可以初始化更多的信息。来看看我们在EnumMap与EnumSet一文中提到过的枚举类型 DataBaseType,它存放了现在支持的所有数据库类型。但它仅是一个“代号”,由于和数据库相关的信息对于一个应用程序来说是固定不变的,所以把 这些数据放置在枚举类型自身中更符合设计的习惯。

 

Java代码

public enum DataBaseType{

        

                   MYSQL("com.mysql.jdbc.Driver", "jdbc:mysql://localhost/mydb"),

                   ORACLE("oracle.jdbc.driver.OracleDriver",

                                     "jdbc:oracle:thin:@localhost:1521:sample"),

                   DB2("com.ibm.db2.jdbc.app.DB2Driver",

                                     "jdbc:db2://localhost:5000/sample"),

                   SQLSERVER("com.microsoft.jdbc.sqlserver.SQLServerDriver",

                                     "jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=mydb");

        

                   private String driver;

        

                   private String url;

                   //自定义的构造函数,它为驱动、URL赋值

                   DataBaseType(String driver,String url){

                            this.driver = driver;

                            this.url = url;

                   }

                   /**

                * 获得数据库驱动

                * @return

              */

                public String getDriver() {

                            return driver;

                }

                   /**

                * 获得数据库连接URL

                * @return

                */

                   public String getUrl() {

                            return url;

                   }

}

//测试程序

for(DataBaseType dataBaseType: EnumSet.allOf(DataBaseType.class)){

              System.out.println("the driver is : "+dataBaseType.getDriver());

              System.out.println("the url is : "+dataBaseType.getUrl());

         }

    你注意到例子中的枚举常量是如何声明使用自定义构造函数初始化的吗?仅需要将初始化使用的数据放入在枚举常量名称后面的括号中就可以了。

    现在我们设计出了两个内容丰富的枚举类型,对枚举类型的使用也变得自然了许多。你也许觉得枚举类型和类之间差别甚微。可是毕竟枚举类型有着诸多限制,你在实现自己的枚举类型时一定要遵循它们。

    1. 枚举类型不能使用extends关键字,但是可以使用implements关键字。这样我们可以把不同枚举类型共有的行为提取到接口中,来规范枚举类型的行为。

    2. 枚举类型的自定义构造函数并不能覆盖默认执行的构造函数,它会跟在默认构造函数之后执行。

    3. 枚举类型的自定义构造函数必须是私有的。你不需要在构造函数上添加private关键字,编译器会为我们代劳的。

    4. 枚举类型中枚举常量的定义必须放在最上面,其后才能是变量和方法的定义。

模板方法
谈这个话题前我们要看一下改写的printWeekDay方法,在那个例子里WeekDay是丰富一些了,不过使用switch对枚举常量逐个判断以便定 制不同的行为,扩展起来要麻烦了一些。假如为WeekDay添加了一个新的枚举常量,如果你忘了同时为它在switch中添加相应的case标示,那么即 使有default标示来提示错误,也只能在运行后才能发现。

    怎么做能更好一点?我们前面已经认识到枚举就是一个特殊的类,它可以有方法和属性,同时每个声明的枚举项都是这个枚举类型的一个实例。那么我们能不能使用 “模板方法模式”来改造一下这个枚举类呢?当然可以!我们把那个例子重构一下,变成下面这个样子:

 

Java代码

public enum WeekDay {

                   MONDAY{

                            @Override

                            public void printWeekDay() {

                                     System.out.println(“Today is Monday!”);

                            }

                   },

                   TUESDAY{

                            @Override

                            public void printWeekDay() {

                                     System.out.println(“Today is Tuesday!”);

                            }

                   },

                   WENSDAY{

                            @Override

                            public void printWeekDay() {

                                     System.out.println(“Today is Wensday!”);

                            }

                   },

                   THURSDAY{

                            @Override

                            public void printWeekDay() {

                                   System.out.println(“Today is Thursday!”);

                            }

                   },

                   FRIDAY{

                            @Override

                            public void printWeekDay() {

                                     System.out.println(“Today is Friday!”);

                            }

                   };

        

                   /**

                * 根据工作日的不同打印不同的信息

                */

                   public abstract void printWeekDay();

}

    首先,我们把方法printWeekDay改为抽象方法,然后我们在每一个枚举常量中实现了在枚举类型里定义的这个抽象方法。这样,每为枚举类型添加一个 新的枚举常量,都必须实现枚举类型中定义的抽象方法,不然编译器提示出错。之所以可以这么做的原因是,虚拟机将枚举类型中声明的每一个枚举常量,创建成为 一个单独的枚举类型的子类。

    这样,再配合使用Tiger里的静态导入,调用者的代码就可以这样写了:

 

Java代码

MONDAY.printWeekDay();

TUESDAY.printWeekDay();

//or better...

getWeekDay().printWeekDay();

    这些代码显然要比常见的if(weekDay == WeekDay.MONDAY){...} else if(weekDay == WeekDay.TUESDAY) else {...}形式强多了,它们易读、容易扩展和维护。

反向查找
前面说到枚举也可以自定义构造函数,可以用属性来关联更多的数据。那如果我们有这样的一种需要该怎么办呢?——我们需要根据关联的数据来得到相应的枚举项,例如下面的这种情况:

 

Java代码

public final enum Status {

     WAITING(0),

     READY(1),

     SKIPPED(-1),

     COMPLETED(5);

     private int code;

     private Status(int code) {

          this.code = code;

     }

     public int getCode() { return code; }

}

    这里每种Status对应了一个code,WAITING对应了0,而COMPLETED对应了5。如果想通过0得到WAITING这个枚举项要怎么做?

    做法也很简单,使用一个静态的java.util.Map来把code和枚举项关联起来就可以了,就像这样:

 

Java代码

public final enum Status {

     WAITING(0),

     READY(1),

     SKIPPED(-1),

     COMPLETED(5);

     private static final Map<Integer,Status> lookup

          = new HashMap<Integer,Status>();

     static {

          for(Status s : EnumSet.allOf(Status.class)){

               lookup.put(s.getCode(), s);

          }

     }

     public static Status get(int code) {

          return lookup.get(code);

     }

     private int code;

     private Status(int code) {

          this.code = code;

     }

     public int getCode() { return code; }

}

    静态方法get(int)提供了需求中的反向查找能力,而静态块里使用EnumSet来把起映射做用的Map组装起来,Over!

总结:使用枚举,但不要滥用!

    学习任何新版语言的一个危险就是疯狂使用新的语法结构。如果这样做,那么您的代码就会突然之间有 80% 是泛型、标注和枚举。所以,应当只在适合使用枚举的地方才使用它。那么,枚举在什么地方适用呢?一条普遍规则是,任何使用常量的地方,例如目前用 switch 代码切换常量的地方。如果只有单独一个值(例如,鞋的最大尺寸,或者笼子中能装猴子的最大数目),则还是把这个任务留给常量吧。但是,如果定义了一组值, 而这些值中的任何一个都可以用于特定的数据类型,那么将枚举用在这个地方最适合不过。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多