enum,即枚举类型,在每种编程语言中都有类似的类型。
因为用得少,语法规则很难记得住,我每次看到enum都会感到害怕。
一般的enum语法是这样的:
public class MyClass { private enum Fruit {APPLE, ORANGE, GRAPE, BANANA} //类型定义 private Fruit fruit = Fruit.APPLE; //类型使用 private void useEnum() { if (fruit == Fruit.ORANGE) { System.out.println("这是橘子"); } } }
在上面的“类型定义”部分,就发现定义一个enum类型的语句跟常规的Java语法有很大出入,一个类里面一般就是定义变量、函数、内部类这三样东西,但是这句话感觉什么都不是。
对于enum,包括C++当中的类似的枚举类型,我以前都是采取死记硬背的方法,把它跟其他的语法区别对待来记忆。然而并没有卵用,就是因为枚举类型的“不寻常”,以及用得少,很快就会忘记具体的语法格式。
前两天看《Effective Java》时,发现有一章专门讲enum的,而且书中强调用enum的好处,以及Java当中的enum相比其他语言的枚举类型的强大之处。我看得一知半解,但对enum这一特殊的东西开始重视起来。
通过各方面的研究,我发现了enum不为人知的惊天内幕,以及各种暗箱操作。
首先给出一个最重要的点:enum其实是一个class!
有了这一逻辑,对于enum的基础语法就理解了一半。把上面的“enum”这个词换成“class”试试会怎样?比如枚举值两边用大括号是怎么回事,是不是清楚了很多?
既然enum就是一个类,那我把 Fruit类(可以这么叫了吧?)的定义换个形式
public class MyClass { private class Fruit { //把enum换成新的定义 private Fruit() {} //不允许在外面new出Fruit public static final Fruit APPLE = new Fruit(); public static final Fruit ORANGE = new Fruit(); public static final Fruit GRAPE = new Fruit(); public static final Fruit BANANA = new Fruit(); } private Fruit fruit = Fruit.APPLE; //类型使用 private void useEnum() { if (fruit == Fruit.ORANGE) { System.out.println("这是橘子"); } } }
看完这段转换之后的用常规class写的代码,是不是想惊呼一声:这特么不是效果完全一样的代码吗!
是的,这就是编译器的暗箱操作,编译器一看到enum这个关键字,就会很自然地试图转换成这样。每一个枚举值,并不是C++里面那样的从0开始的int值,而是一个个类实例。而且因为加上了final关键字,知道为什么枚举值是全大写的了吧?又因为加了static关键字,为什么会写出“Fruit.APPLE”这样的静态调用形式了吧?
这还没有完,以上只是enum定义时的原理,enum在具体使用时,如何遍历?如何输出每个枚举值代表的int值?如何输出每个枚举值的字面字符串值?
事实上,上面给出的Fruit类很不完整,替代enum的类继承自java.lang.Enum,这是一个抽象类,编译器遇到enum时自动转化为Enum父类中对应的操作。
//Enum类的定义 public abstract class Enum<E extends Enum<E>> implements Serializable, Comparable<E> {...}
比如上面的Fruit类其实是这样的
private class Fruit extends java.lang.Enum { .... }
来看一下Enum中主要的成员。
//这是Enum类仅有的两个成员变量,比如{APPLE,ORANGE,GRAPE},3个枚举值其实都是Enum类的子类的实例,name分别是"APPLE","ORANGE","GRAPE", ordinal分别是0,1,2 private final String name; //枚举值的字面字符串表示,比如"APPLE","ORANGE", private final int ordinal; //枚举值所代表的int值,跟C++中的枚举类型类似,可以理解为索引,按照定义时顺序,第一个为0,第二个为1,以此类推
可以通过类似于get方法得到这两个属性值
//得到字面值 public final String name() { return name; } //得到索引值 public final int ordinal() { return ordinal; }
另外在输出枚举值时,输出的是枚举值的字面字符串值,因为调用的是toString()方法
@Override public String toString() { return name; }
Enum类实现了Comparable接口,因而可以进行枚举值间的比较,事实上就是索引值比较
//返回两个枚举值的索引值之差 public final int compareTo(E o) { return ordinal - ((Enum<?>) o).ordinal; }
enum还有两个操作应该是编译器调用Enum类的其他方法实现的。
第一个是enum的values()方法,返回所有的类实例组成的数组,比如 enum Fruit {APPLE,ORANGE,GRAPE},就返回 new Fruit[]{Fruit.APPLE, Fruit.ORANGE, Fruit.GRAPE},这可以用于遍历操作。
for (Fruit f :Fruit.values()) { .... }
第二个是用switch做选择的时候,case后面只要写出枚举值即可,不必写类名
Fruit f = Fruit.APPLE; switch(f) { case (APPLE) { //此处不必也不能写成case (Fruit.APPLE),编译器自动判断这是Fruit类的实例 ... break; } case (ORANGE) { ... break; } default { ... } }
关于这是如何实现的,就是编译器自己的暗箱操作了,此处也不太清楚。
另外,更高级一点的,就是把enum完全看成是一个class,因而可以重写自己的方法,添加自己的成员变量。例如用枚举值实现四则运算:
public enum Operation { //每个枚举值,即Operation实例,其实自己在声明时定义了一个匿名的类,继承了Operation类,匿名子类里面重写了父类Operation的apply方法 PLUS {double apply(double x,double y) {return x+y;}}, MINUS {double apply(double x,double y) {return x-y;}}, TIMES {double apply(double x,double y) {return x*y;}}, DIVIDE {double apply(double x,double y) {return x/y;}}; abstract double apply(double x, double y); //Operation类中定义的抽象方法 }
上面的代码翻译过来其实是
public class Operation extends Enum{ public static final Operation PLUS = new Operation(){ //匿名内部类 @Override double apply(double x,double y) {return x+y;} }; ...... abstract double apply(double x, double y); //Operation类中定义的抽象方法 }
当然也可以自己实现enum,添加成员变量,重载构造函数
public enum Operation { PLUS("+"), //等于:public static final Operation PLUS = new Operation("+"); MINUS("-"), TIMES("*"), DIVIDE("/"); private final String symbol; //自己加的成员变量 Operation(String symbol) {this.symbol = symbol;} //构造函数,在列枚举值的时候可以加括号给出参数,注意访问权限最好别写成public @Override public String toString() {return symbol;} //重写Enum类的toString()方法 }
最后,有一个小小的细节要注意,枚举值之间用“,”隔开。当enum定义中只有枚举值,没有其他东西时,枚举值最后可以不加“;”。但还有其他定义,比如还有一个成员方法,必须先写出枚举值,再加其他定义,且枚举值最后加“;”
private enum Haha { private void f() { System.out.println("!!!!!!!!!"); }; FACE,SUGAR,APPLE; //枚举值必须先写 }
枚举值的一大用处是用来替换零散的常量定义(public static final XXX XX = XX;),而且这些常量仅做标识符用,对于常量值是多少并不关心。使用了enum定义之后,可以进行类型检查,当常量很多时还可以归类,而且可以输出标识符的字面值。