枚举类型 Java编程思想 读书笔记

来源:互联网 发布:微电影知乎 编辑:程序博客网 时间:2024/05/01 02:40

关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。


以下是一个使用枚举的实例:

/** * 定义一个枚举类型 */enum Shrubbery{GROUND,CRAWLING,HANGING};/** * 对枚举类型的测试类 */public class EnumClass {public static void main(String[] args) {for(Shrubbery s:Shrubbery.values()){System.out.print(s + " ordinal: " + s.ordinal() + " | ");System.out.print(s.compareTo(Shrubbery.CRAWLING)+ " | " );System.out.print(s.equals(Shrubbery.CRAWLING) + " | ");System.out.print(s == Shrubbery.CRAWLING );System.out.print(" | " );System.out.print(s.getDeclaringClass() + " | ");System.out.print(s.name());System.out.println();}System.out.println("---------");for(String s:"HANGING CRAWLING GROUND".split(" ")){Shrubbery shrub = Enum.valueOf(Shrubbery.class, s);System.out.println(shrub);}}}
创建enum时,编译器会生成一个相关的类,这个类继承自java.lang.Enum。

调用enum的values()方法,可以遍历enum实例。values()返回enum实例的数组,而且该数组中的元素严格保持其在enum中声明的顺序。ordinal()方法返回一个int值,这个每个enum实例在声明时的次序,从0开始。编译器会自动提供equals()和hashCode()方法,也可以使用==来比较enum实例。Enum类实现了Comparable接口,所以它具有compareTo()方法。同时,它还实现了Serializable接口。

在enum实例上调用getDeclaringClass()方法,可以获得其所属的enum类。

name()方法返回enum实例声明时的名字,这与使用toString()方法效果相同。Enum.valueOf()根据给定的名字返回相应的enum实例,如果不存在给定名字的实例,将会抛出异常。

向enum中添加新方法

除了不能继承自一个enum之外,我们基本可以将enum看作一个常规的类。我们可以向enum中添加方法,enum甚至可以有main方法。

public enum OzWitch {WEST("Miss Gulch,aka defined first,befor methods"),NORTH("Glinda,the Good Witch of the North"),EAST("Wiched witch of the East.wearer of the Ruby Slippers,crushed by Dorothy's house"),SOUTH("Good by inference,but missing");private String description;private OzWitch(String d){this.description = d;}public String getDescription(){return description;}public static void main(String[] args) {for(OzWitch witch:OzWitch.values())System.out.println(witch + ": " + witch.getDescription());}}
在上面例子中,每个枚举实例都带有一个对自身的描述。为此,必须提供一个接收描述信息的构造器,专门负责处理这些额外的信息,然后添加一个方法,返回这个描述信息。

注意:定义方法或属性前,必须在enum实例序列最后一个添加一个分号。同时,Java要求必须先定义enum实例。如果在定义enum实例之前定义了任何方法或属性,那么在编译时就会得到错误信息。

在上面例子中,即使没有将enum构造器声明为private,也只能在enum定义的内部使用其构造器创建enum实例。一旦enum的定义结束,编译器就不允许我们再使用其构造器来创建任何实例了。

覆盖enum的方法

public enum SpaceShip {SCOUT,CARGO,TRANSPORT,CRUISER,BATTLESHIP,MOTHERSHIP;/** * 覆盖toString()方法,将实例名字除第一个字母外的字符转化为小写 */@Overridepublic String toString(){String id = name();String lower = id.substring(1).toLowerCase();return id.charAt(0) + lower;}public static void main(String[] args) {for(SpaceShip s:values()){System.out.println(s);}}}

上面例子中我们覆盖了toString()方法,可以看到覆盖enum的toString()方法与覆盖一般类的方法没有区别。


switch语句中的enum

一般来说,switch中只能使用整数值,而枚举实例天生就具备整数值的次序,并且可以通过ordinal()方法取得其次序(显然编译器帮我们做了相关工作),因此可以在switch语句中使用enum。

一般情况下我们必须使用enum类型修饰enum实例,但是在case语句中并不必如此。下面例子使用enum构造一个小型状态机:

enum Signal{GREEN,YELLOW,RED}public class TrafficLight {Signal color = Signal.RED;public void change(){switch (color) {case RED:color = Signal.GREEN;break;case GREEN:color = Signal.YELLOW;break;case YELLOW:color = Signal.RED;}}public String toString(){return "The traffic light is " + color;}public static void main(String[] args) {TrafficLight t = new TrafficLight();for(int i = 0; i < 7; i++){System.out.println(t);t.change();}}}


values()的神秘之处

编译器为你创建的enum类都继承自Enum类。但是Enum类并没有values()方法。

values()是由编译器添加的static方法。同时,编译器还添加了valueof方法,不过与Enum本身的valueof需要两个参数,而新增的方法只需一个参数。(Java编程思想通过反射得出这些信息,有兴趣可以参见),同时编译器将生成的类标记为fianl类,因此我们无法继承enum,其中还有一个static的初始化子句。

enum Expore{HERE,THERE}

反射得到:


注意:由于擦除效应,反编译无法得到Enum的完整信息,所以它展示的Explore的父类只是一个原始的Enum,而非事实上的Enum<Explore>。

由于 values方法是由编译器插入到enum定义中的static方法,所以如果将enum实例向上转型为Enum,那么values方法就无法访问了。不过,在Class中有一个getEnumConstants方法,所以即使Enum接口中没有values方法,仍可以通过Class对象的取得所有enum实例:

enum Search{HITHER,YON}public class UpcaseEnum {public static void main(String[] args) {Enum<Search> e = Search.HITHER;// upcase//e.values();// no values mothod in Enumfor(Enum<Search> en:e.getClass().getEnumConstants()){System.out.println(en);}}}

注意:因为getEnumConstants是Class方法,所以甚至可以对不是枚举的类调用此方法:

public class NonEnum {public static void main(String[] args) {Class<Integer> intClass = Integer.class;try{for(Object en:intClass.getEnumConstants()){System.out.println(en);}}catch(Exception e){e.printStackTrace();}}}

只不过,此时该方法返回null,所以你试图使用它则发生异常。

enum实现接口

所有的enum都继承自java.lang.Enum类。由于Java不支持多重继承,所以enum不能再继承其他类了,但它可以实现一个或多个接口

import java.util.Random;/** * 定义一个接口Generator */interface Generator<T>{public T next();}/** * 枚举实现了接口Generator */enum CartoonCharacter implements Generator<CartoonCharacter>{SLAPPY,SPANKY,PUNCHY,SILLY,BOUNCY,NUTTY,BOB;private Random rand = new Random(47);public CartoonCharacter next(){return values()[rand.nextInt(values().length)];}}public class EnumImplementation {public static <T> void printNext(Generator<T> rg){System.out.println(rg.next() + " , ");}public static void main(String[] args) {CartoonCharacter cc = CartoonCharacter.BOB;for(int i = 0; i < 10; i++){printNext(cc);}}}

 

随机选取 泛型使用

CartoonCharacter.next实现了从enum实例中随机选择,我们可以利用泛型,使这个工作一般化,并将其加入到我们的工具库中。

import java.util.Random;public class Enums {private static Random rand = new Random(47);public static <T extends Enum<T>> T random(Class<T> ec){return random(ec.getEnumConstants());}public static <T> T random(T[] values){return values[rand.nextInt(values.length)];}}
 <T extends Enum<T>> T 表示T是一个enum实例。而将Class<T>作为参数,可以利用Class对象得到enum实例 的数组。重载后的random(T[] values)方法只需使用T[]作为参数,因为它并不会调用Enum上的任何操作,它只需从数组中随机选择一个元素即可。这样,最终返回类型正是enum的类型。

下面是random()方法的一个简单实例:

enum Activity{SITTInG,LYING,STANDING,HOPPING,RUNNING,DODGING,JUMPING,FALLING,FLYING}public class RandomTest {public static void main(String[] args) {for(int i = 0; i < 20; i++){System.out.println(Enums.random(Activity.class));}}}

使用EnumSet

EnumSet非常高效。

EnumSet中的元素必须来自一个enum。

下面enum表示在一座大楼时,警报传感器的安放位置:

package enumStudy;public enum AlartPoins {STAIR1,STAIR2,LOBBY,OFFICE1,OFFICE2,OFFICE3,OFFICE4,BATHROOM,UTILITY,KITCHEN;}

然后我们用EnumSet来跟踪报警器的状态:

import java.util.EnumSet;import static enumStudy.AlartPoins.*;public class EnumSets {public static void main(String[] args) {EnumSet<AlartPoins> points = EnumSet.noneOf(AlartPoins.class);//empty setpoints.add(BATHROOM);System.out.println(points);points.addAll(EnumSet.of( STAIR1, STAIR2, KITCHEN));System.out.println(points);points = EnumSet.allOf(AlartPoins.class);points.removeAll(EnumSet.of( STAIR1, STAIR2, KITCHEN));System.out.println(points);points.removeAll(EnumSet.range( OFFICE1,  OFFICE4));System.out.println(points);points = EnumSet.complementOf(points);System.out.println(points);}}
注意我们使用了static import来简化enum常量的使用。
查看EnumSet文档,可以发现of()方法被重载了很多次,不但为可变参数进行了重载,还为接收2至5个显式参数的情况进行了重载,这也从侧面表现了EnumSet对性能的关注。当你只使用2至5个参数调用of()方法时,你可以调用对应的重载过的方法(速度稍快一点),而当你使用一个参数或多于5个参数时,你调用的将是使用可变参数的of()方法。注意:如果你只使用一个参数,编译器并不会构造可变参数的数组,所以与调用只有一个参数的方法相比,也不会有额外的性能损耗。


使用EnumMap

EnumMap要求其中的键必须来自一个enum。EnumMap速度很快,我们可以放心使用enum实例在EnumMap中进行查找。不过,我们只能将enum的实例作为键来调用put()方法,其他操作与使用一般的Map差不多。

下面的例子演示了命令设计模式的用法。一般来说,命令模式首先需要一个只有单一方法的接口,然后从该接口实现具有各自不同的行为的多个子类。接下来,程序员就可以构造命令对象,并在需要的时候使用它们。

import java.util.*;import static enumStudy.AlartPoins.*;interface Command{void action();}public class EnumMpas {public static void main(String[] args) {EnumMap<AlartPoins,Command> em =new EnumMap<AlartPoins, Command>(AlartPoins.class);em.put(KITCHEN, new Command() {public void action() {System.out.println("Kintchen fire");}});em.put(BATHROOM, new Command() {public void action() {System.out.println("Bathroom alert");}});for(Map.Entry<AlartPoins, Command> e:em.entrySet()){System.out.print(e.getKey() + " : ");e.getValue().action();}try{//if there's no value for a particular key;em.get(UTILITY).action();} catch(Exception e){e.printStackTrace();}}}
与EnumSet一样,enum实例定义时的次序决定了其在EnumMap中的顺序。

main方法最后部分说明,enum的每个实例作为一个键,总是存在的。但是,如果你没有为这个键调用put()方法来存入相应的值,其对应的值就是null。


常量相关的方法

程序员可以为enum实例编写方法,从而为每个enum实例赋予各自不同的行为。要实现常量相关的方法,你需要为enum定义一个或多个abstract方法,然后为每个enum实例实现该抽象方法。参考下面的例子:

import java.text.DateFormat;import java.util.Date;public enum ConstantSpecificMethod {DATE_TIME{String getInfo(){return DateFormat.getDateInstance().format(new Date());}},ClASSPATH{String getInfo(){return System.getenv("CLASSPATH");}},VERSION{String getInfo(){return System.getProperty("java.version");}};abstract String getInfo();public static void main(String[] args) {for(ConstantSpecificMethod csm:values()){System.out.println(csm.getInfo());}}}
通过相应的enum实例,我们可以调用其上的方法。这通常也称为表驱动的代码。

在面向对象的程序设计中,不同的行为与不同的类关联。而通过常量相关的方法,每个enum实例都具备自己独特的行为。这似乎说明每个enum实例就像一个独特的类。在上面的例子中,enum实例似乎被当作“超类”ConstantSpecificMethod来使用,在调用getInfo()方法时,体现出多态行为。

然而,enum实例与类的相似之处也仅限于此了。我们并不能将enum实例作为一个类型来使用:


使用enum的状态机

枚举类型非常适合用来创建状态机。一个状态机可以具有有限个特定的状态,它通常根据输入,从一个状态转移到下一个状态,不过也可能存在瞬时状态(transient states),而一旦任务执行结束,状态机就会立刻离开瞬时状态。

每个状态都具有某些可接受的输入,不同的输入会使用状态机从当前状态转移到不同的新状态。由于enum对它的实例有严格限制,非常适合用来表现不同的状态和输入。一般而言,每个状态都具有一些相关的输出。

自动售货机是一个很好的状态机的例子。首先,我们用一个enum定义各种输入:

import java.util.Random;public enum Input {NICKEL(5),DIME(10),QUARTER(25),DOLLAR(100),//投币TOOTHPASTE(200),CHIPS(75),SODA(100),SOAP(50),//可以选择的货品ABORT_TRANSACTION{//中止交易public int amount(){// Disallowthrow new RuntimeException("ABORT.amount()");}},STOP{//退出     this must be the last instancepublic int amount(){throw new RuntimeException("SHUT_DOWN.amount()");}};int value;Input(int value){ this.value = value; };Input(){};int amount(){ return value; };// In centsstatic Random rand = new Random(47);public static Input randomSelection(){// Don't include STOPreturn values()[rand.nextInt(values().length - 1 )];}}


注意:除了两个特殊的Input实例之外,其他的Input都有相应的价格,因此在接口中定义了amount()方法。然而,对那两个特殊实例而言,调用amount()方法并不合适,所以如果程序员调用它们的amount()方法就会抛出异常。这似乎有点奇怪,但由于enum的限制,我们不得不采用这种方式。

下面是状态机:

import static enumStudy.Input.*;import java.util.EnumMap;/** * 每一个输入对应的操作类别 */enum Category{MONEY(NICKEL,DIME,QUARTER,DOLLAR),//投币ITEM_SELECTION(TOOTHPASTE,CHIPS,SODA,SOAP),//选择货品QUIT_TRANSACTION(ABORT_TRANSACTION),//中止操作SHUT_DOWN(STOP);//关机private Input[] values;//记录每种输入对应的类别private static EnumMap<Input,Category> categories = new EnumMap<Input,Category>(Input.class);private Category(Input... types){ values = types; };//将所有的输入加入对应类别记录中categoriesstatic {for(Category c:Category.class.getEnumConstants())for(Input type:c.values)categories.put(type, c);}public static Category categorize(Input input){return categories.get(input);}}public class VendingMachine {//自动售货机的状态private static State state = State.RESTING;//自动售货机的金额private static int amount = 0;//用户输入private static Input selection = null;//瞬时状态enum StateDuration{ TRANSIENT };//当自动售货机处于扣除金额和送出货品 状态时是瞬时状态//状态enum State{RESTING{//休眠void next(Input input){switch(Category.categorize(input)){//根据类型来转换状态case MONEY://投币amount += input.amount();state = ADDING_MONEY;break;case SHUT_DOWN://关机 state = TERMINAL;default:}}},ADDING_MONEY{//已输入金钱了,可以选择货品或增加金额void next(Input input){switch(Category.categorize(input)){case MONEY:   //投币amount += input.amount();break;case ITEM_SELECTION://选择项目selection = input;if(amount < selection.amount())System.out.println("金额不足支付  " + selection);else {state = DISPENSING; System.out.println("23234");};break;case QUIT_TRANSACTION://中止操作state = GIVING_CHANGE;break;case SHUT_DOWN://关机 state = TERMINAL;default:}}},DISPENSING(StateDuration.TRANSIENT){//扣除金额void next(){System.out.println("你选择了: " + selection);amount -= selection.amount();state = GIVING_CHANGE;}},GIVING_CHANGE(StateDuration.TRANSIENT){//送出货品 void next(){if(amount > 0){System.out.println("剩余金额: " + amount);amount = 0;}state = RESTING;//休眠}},TERMINAL{//终止void output(){System.out.println("Halted"); }};private boolean isTransient = false;State(){};State(StateDuration trans){ isTransient = true; };void next(Input input){throw new RuntimeException("Only call next(Input input) for non-transient states");}void next(){throw new RuntimeException("Only call next() for StateDuration.TRANSIENT states");}void output(){System.out.println(amount);}}// state end//对自动售货机状态的处理,如果处理瞬时状态,则要自动跳到下一状态public static void nextInput(Input input){state.next(input);while(state.isTransient){state.next();}}public static void main(String[] args){nextInput(Input.QUARTER); //投币nextInput(Input.QUARTER);//投币nextInput(Input.SODA);//选择货品nextInput(Input.DOLLAR);//投币nextInput(Input.SODA);//选择货品}}

注意:内部的enum实例state是static实例,无法访问外部类的非static元素或方法,所以对于内部的enum的实例而言,其行为与一般的内部类并不相同。

对于每一个State,我们都需要在输入动作的基本分类中查找:用户投入钞票,选择了某个货物,操作被取消,以及机器停止。然而,在这些基本分类下,我们又可以投入不同类型的钞票,可以选择不同的货物。Category enum将不同类型的Input进行分组,因而,可以使用categorize()方法为switch语句生成恰当的Category实例。并且,该方法使用的EnumMap确保了在其中进行查询时的效率与安全。

State还有两个瞬时状态,在nextInput方法中,状态机等待着下一个Input,并一直在各个状态中移动,直到它不再处于瞬时状态。


更多内容请参看Java编程思想

0 0
原创粉丝点击