枚举类型 enum type 是指由一组固定常量组成合法值的类型,比如一年中的季节、太阳系中的行星。
在编程语言中还没有枚举类型之前,表示枚举类型的常用模式是声明一组 int 常量,每个类型成员一个常量:
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
这种模式被称为 int 枚举模式,有诸多不足:
1. 类型安全性:以上的 APPLE 和 ORANGE 本来是不同类型,然而都用 int 表示,那么如果将 APPLE 传到了需要 ORANGE 的地方,编译器也不会发现问题。
2. 由于没有命名空间,每个枚举值都需要用前缀 APPLE_ 或 ORANGE_ 来区分。
3. 将 int 枚举常量翻译成可打印的字符串,并没有很便利的方法。
4. int 枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的 int 值发生了变化,那么客户端也需要重新编译。
从 Java1.5 开始,出现一个特殊的枚举类型:
public enum Apple {
FUJI,
PIPPIN,
GRANNY;
}
public enum Orange {
NAVEL,
TEMPLE,
BLOOD;
}
枚举类型 enum 是实例受控的,是单例的泛型化,同时也是类型安全的。同时,其默认的 toString 方法输出的是枚举常量名字符串,比如 FUJI、PIPPIN。
Java 中的枚举本质上是 int 值,可以使用 ordinal() 获取,默认从 0 开始依次递增。
调整枚举常量的顺序后,并不需要重新编译客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中。比如下面的这个类,被反编译后我们发现字节码中存储的是枚举类型和常量名,而不是 int 值:
public class TestEnum2 {
public static void main(String[] args) {
System.out.println(Apple.FUJI);
}
}
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Fieldref #23.#24 // com/qunar/flight/tts/afare/utils/Apple.FUJI:Lcom/qunar/flight/tts/afare/utils/Apple;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #3 // Field com/qunar/flight/tts/afare/utils/Apple.FUJI:Lcom/qunar/flight/tts/afare/utils/Apple;
6: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
9: return
Java 中的枚举 enum 也可以拥有 field、方法、抽象方法,还可以覆盖 toString 方法:
public enum Operation {
PLUS("+") {
@Override
double apply(double a, double b) {
return a + b;
}
},
MINUS("-") {
@Override
double apply(double a, double b) {
return a - b;
}
},
TIMES("*") {
@Override
double apply(double a, double b) {
return a + b;
}
},
DIVIDE("/") {
@Override
double apply(double a, double b) {
return a + b;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
// 覆盖 toString,输出 symbol
@Override
public String toString() {
return symbol;
}
// 将 symbol 转换成 Operation
public Operation fromString(String symbol) {
for (Operation operation : values()) {
if (operation.symbol.equals(symbol)) {
return operation;
}
}
return null;
}
abstract double apply(double a, double b);
}
考虑用一个枚举表示薪资包中的工作天数,这个枚举有一个方法,根据给定工人的基本工资以及当天工作时间,计算当天报酬。在 5 个工作日中,超过 8 小时会产生加班工资,而在周末则所有工作都产生加班工资。解决这一问题的比较好的方案是使用策略枚举模式:将加班工资的计算移到了私有的嵌套枚举中,将某一天适用的策略枚举传递到 PayrollDay 的构造器中。PayrollDay 中不再需要 switch 语句或特定于常量的方法实现,这种模式虽然没有 switch 语句简洁,但是更加安全(减少增加枚举时遗漏增加逻辑的可能性),也更加灵活。
public enum PayrollDay {
MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(
PayType.WEEKDAY), SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
private enum PayType {
WEEKDAY {
@Override
double overtimePay(double hrs, double payRate) {
return hrs <= HOURS_PER_SHIFT ? 0 : (hrs - HOURS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
@Override
double overtimePay(double hrs, double payRate) {
return hrs * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hrs, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
}
所以枚举都有一个 ordinal 方法,返回每个枚举常量在类型中的数字位置,其值与枚举常量的顺序相关,完全由编译器定义。
ordinal 一般是用于像 EnumSet、EnumMap 这样的通用数据结构的。在程序中,不要去调用 ordinal 方法,如果需要这样的一个 int 值,请在实例域中维护。
位域表示法允许利用位操作,有效地执行像 union 并集、intersection 交集这样的集合操作。位域有着 int 枚举常量所有的缺点,可以使用 EnumSet 代替,EnumSet 就是用单个 long 表示,很多操作都是利用位算法来实现,其性能和位域相当。
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 1;
public static final int STYLE_UNDERLINE = 1 << 2;
// 使用例子
int STYLE_BOLD_ITALIC = STYLE_BOLD | STYLE_ITALIC;
// RegularEnumSet 是 EnumSet 的一个实现
class RegularEnumSet> extends EnumSet {
/**
* Bit vector representation of this set. The 2^k bit indicates the
* presence of universe[k] in this set.
*/
private long elements = 0L;
// add 利用位操作实现
public boolean add(E e) {
typeCheck(e);
long oldElements = elements;
elements |= (1L << ((Enum>)e).ordinal());
return elements != oldElements;
}
}
最好不要用序数 ordinal 来索引数组,而要使用 EnumMap。如果你所表达的关系是多维的,就是用 EnumMap< …, EnumMap< …>>。
EnumMap 运行速度能够与序数索引的数组相媲美,因为其内部实现也是序数索引的数组。但是,使用 EnumMap 更不容易出错(类型安全,隐藏了实现细节,替你干了脏活、累活)。
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
implements java.io.Serializable, Cloneable
{
private final Class keyType;
private transient K[] keyUniverse;
private transient Object[] vals;
public V put(K key, V value) {
typeCheck(key);
int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}
}
枚举类是 final 的,不可被继承扩展的。如果想要可伸缩的枚举,可以使用接口来模拟:
public interface IOperation {
double apply(double a, double b);
}
public enum Operation implements IOperation {
PLUS("+") {
@Override
public double apply(double a, double b) {
return a + b;
}
},
MINUS("-") {
@Override
public double apply(double a, double b) {
return a - b;
}
},
TIMES("*") {
@Override
public double apply(double a, double b) {
return a + b;
}
},
DIVIDE("/") {
@Override
public double apply(double a, double b) {
return a + b;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
// 覆盖 toString,输出 symbol
@Override
public String toString() {
return symbol;
}
}
虽然枚举类型不是可扩展的,但是接口类型是可扩展的。在使用任何基础操作的地方,都可以使用新的枚举,只要 API 是被写成采用接口类型,而非实现。
泛型 < T extends Enum< T> & IOperation> 表示 T 既是枚举,又是 IOperation 的子类型。
在 Java1.5 之前,经常使用命名模式 naming pattern 表明有些程序元素需要通过某种工具或框架进行特殊处理。例如,Junit 测试框架原本要求用户一定要用 test 作为测试方法名称的开头。这种方法可行,但是有很多缺点:
1. 拼写错误后难以发现。
2. 无法确保他们只用于相应的程序元素上,容易被多种框架误用。
3. 没有提供将参数值与程序元素关联起来的好方法,方法名所能携带的信息太少。
注解能够很好的解决以上的所有问题。比如 Junit 中标记测试方法的 Test 注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test {
static class None extends Throwable {
private static final long serialVersionUID= 1L;
private None() {
}
}
// 期望抛出的异常
Class extends Throwable> expected() default None.class;
// 方法执行超时时间,0 表示不设置超时时间
long timeout() default 0L;
}
注解只是提供信息给程序使用,并不会改变被注解代码的语义。
Override 注解只能用在方法声明中,表示被注解的方法覆盖率超类中的一个方法声明。坚持使用 Override 注解能够让编译器帮你检查大量的错误。
标记接口 marker interface 是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口,比如 Serializable 接口。
注解也是一种标记,那么它和标记接口有什么区别呢?标记注解只提供说明信息,无法定义类型;而标记接口是一种类型,由被标记类的实例实现,能够在编译时捕捉使用注解在运行时才能捕捉到的一些错误。另一方面,标记接口可以继承别的接口,而注解不能。
标记注解相对于标记接口的一大优点是,可以给已被使用的注解类型添加更多的信息,可以被用到类、方法、域上,适用范围更广。
简单来说,他们的区别就是注解和接口的区别。