Java - 枚举与注解
Enumeration
于 Java 1.5 增加的 enum type...
enum type 是由一组固定的常量组成的类型,比如四个季节、扑克花色。
在出现 enum type 之前,通常用一组 int 常量表示枚举类型。
比如这样:
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
如果只是想用作枚举,感觉这样也没什么。
但如果把上面的苹果和橘子互作比较,或者写成....
int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;
虽合法但诧异,这是在做果汁吗?
而且,这种常量是 compile-time 常量,编译后一切都结束了,使用这个常量的地方都被替换为该常量的值。
如果该常量值需要改变,所有使用该常量的代码都必须重新编译。
更糟糕的情况是,不重新编译也可以正常运行,只不过会得到无法预测的结果。
(ps: 我觉得更遭的是有人直接把常量值写到代码里...)
另外,比如上面的 APPLE_FUJI,我想打印它的名字,不是它的值。
不仅如此,我还想打印所有苹果,我想打印苹果一共有多少种类。
当然,如果想打印也可以,只是相比直接使用 enum,无论怎么做都很麻烦。
如果使用 enum,比如:
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
看起来就是一堆常量,但是 enum 没有实例,也没有可访问的构造器,无法对其进行扩展。
enum 本身就是 final,所以很多时候也直接用 enum 实现 singleton。
enum 在编译时是类型安全的,比如有地方声明了上面代码中的 Apple 类型的参数,那么被传到该参数的引用肯定是三种苹果之一。
而且 enum 本身就是一个类型,可以有自己的方法和 field,而且可以实现接口。
附上书中太阳系 enum,很难想象如果有类似需求时用普通常量来实现。
也许我可以声明一个 Planet 类,再给它加上 field 的方法,然后在一个 constant 类中声明为 final
但这样却无法保证 Planet 类仅用作常量,所以还是用 enum 吧:
public enum Planet {
MERCURY(3.302e+23, 2.439e6), VENUS(4.869e+24, 6.052e6), EARTH(5.975e+24,
6.378e6), MARS(6.419e+23, 3.393e6), JUPITER(1.899e+27, 7.149e7), SATURN(
5.685e+26, 6.027e7), URANUS(8.683e+25, 2.556e7), NEPTUNE(1.024e+26,
2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
<span class="hljs-comment">// Universal gravitational constant in m^3 / kg s^2</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">double</span> <span class="hljs-variable">G</span> <span class="hljs-operator">=</span> <span class="hljs-number">6.67300E-11</span>;
<span class="hljs-comment">// Constructor</span>
Planet(<span class="hljs-type">double</span> mass, <span class="hljs-type">double</span> radius) {
<span class="hljs-built_in">this</span>.mass = mass;
<span class="hljs-built_in">this</span>.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
<span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">mass</span><span class="hljs-params">()</span> {
<span class="hljs-keyword">return</span> mass;
}
<span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">radius</span><span class="hljs-params">()</span> {
<span class="hljs-keyword">return</span> radius;
}
<span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">surfaceGravity</span><span class="hljs-params">()</span> {
<span class="hljs-keyword">return</span> surfaceGravity;
}
<span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">surfaceWeight</span><span class="hljs-params">(<span class="hljs-type">double</span> mass)</span> {
<span class="hljs-keyword">return</span> mass * surfaceGravity; <span class="hljs-comment">// F = ma</span>
}
}
然后我们就可以这样使用 Planet enum,无论是值还是名字,使用起来都很自然:
public class WeightTable {
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Weight on %s is %f%n",p, p.surfaceWeight(mass));
}
}
其实像 Planet 这样的方式对多数使用枚举的场景而言足够了。
也就是说每个 Planet 常量表达的是不同的数据,但也有例外。
比如,我们要为 enum 中的每一个常量赋予不同的行为。
下面是书中用 enum 表达计算的例子:
import java.util.HashMap;
import java.util.Map;
public enum Operation {
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;
}
};
private final String symbol;
<span class="hljs-title class_">Operation</span>(<span class="hljs-title class_">String</span> symbol) {
<span class="hljs-variable language_">this</span>.<span class="hljs-property">symbol</span> = symbol;
}
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-title class_">String</span> <span class="hljs-title function_">toString</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">return</span> symbol;
}
<span class="hljs-keyword">abstract</span> double <span class="hljs-title function_">apply</span>(double x, double y);
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> final <span class="hljs-title class_">Map</span><<span class="hljs-title class_">String</span>, <span class="hljs-title class_">Operation</span>> stringToEnum = <span class="hljs-keyword">new</span> <span class="hljs-title class_">HashMap</span><<span class="hljs-title class_">String</span>, <span class="hljs-title class_">Operation</span>>();
<span class="hljs-keyword">static</span> {
<span class="hljs-keyword">for</span> (<span class="hljs-title class_">Operation</span> op : <span class="hljs-title function_">values</span>())
stringToEnum.<span class="hljs-title function_">put</span>(op.<span class="hljs-title function_">toString</span>(), op);
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-title class_">Operation</span> <span class="hljs-title function_">fromString</span>(<span class="hljs-params"><span class="hljs-built_in">String</span> symbol</span>) {
<span class="hljs-keyword">return</span> stringToEnum.<span class="hljs-title function_">get</span>(symbol);
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">main</span>(<span class="hljs-params"><span class="hljs-built_in">String</span>[] args</span>) {
double x = <span class="hljs-title class_">Double</span>.<span class="hljs-title function_">parseDouble</span>(args[<span class="hljs-number">0</span>]);
double y = <span class="hljs-title class_">Double</span>.<span class="hljs-title function_">parseDouble</span>(args[<span class="hljs-number">1</span>]);
<span class="hljs-keyword">for</span> (<span class="hljs-title class_">Operation</span> op : <span class="hljs-title class_">Operation</span>.<span class="hljs-title function_">values</span>())
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">printf</span>(<span class="hljs-string">"%f %s %f = %f%n"</span>, x, op, y, op.<span class="hljs-title function_">apply</span>(x, y));
}
}
对不同的枚举常量进行 switch..case.. 其实也能表达出我们想要的效果。
如果以后增加了新的常量则需要再对应加上一个 case,当然,不加也不会有任何提示,然后最坏的情况就是运行时出了问题。
如上面的代码是常量行为的正确使用方法,即 constant-specific method implementation。
为行为提供一个抽象,并为每一个常量提供一个实现,即一个枚举常量也是 constant-specific class body。
采用这种方式时,如果新增一个常量,则必须提供一个方法实现,否则编译器会给出提示,这就多了一层保障。
遗憾的是,这种方式也有缺陷。
比如我们有这样一个需求,计算某一天的薪水,这个某一天可以是一周中的某一天,也可能是某个节日,比如周一到周五使用相同的运算方式,周末另算,某节日另算。
也就是说我需要在枚举中声明代表周一到周日的常量,如果我继续使用之前的方式去声明一个抽象方法,如果周一到周五采用完全一样的计算,则会出现五段完全相同的代码。
但即使这样我们也不能用回 switch..case.. 方式,增加一个常量时强制选择其选择一种行为实现是必须的。
于是我们有一种叫 strategy enum 的方式,即枚举中声明另外一个枚举的 field,该 field 则代表策略,并提供策略相关的行为。
下面是书中代码:
enum PayrollDay {
MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(
PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(
PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> PayType payType;
PayrollDay(PayType payType) {
<span class="hljs-built_in">this</span>.payType = payType;
}
<span class="hljs-type">double</span> <span class="hljs-title function_">pay</span><span class="hljs-params">(<span class="hljs-type">double</span> hoursWorked, <span class="hljs-type">double</span> payRate)</span> {
<span class="hljs-keyword">return</span> payType.pay(hoursWorked, payRate);
}
<span class="hljs-keyword">private</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">PayType</span> {
WEEKDAY {
<span class="hljs-type">double</span> <span class="hljs-title function_">overtimePay</span><span class="hljs-params">(<span class="hljs-type">double</span> hours, <span class="hljs-type">double</span> payRate)</span> {
<span class="hljs-keyword">return</span> hours <= HOURS_PER_SHIFT ? <span class="hljs-number">0</span> : (hours - HOURS_PER_SHIFT)
* payRate / <span class="hljs-number">2</span>;
}
},
WEEKEND {
<span class="hljs-type">double</span> <span class="hljs-title function_">overtimePay</span><span class="hljs-params">(<span class="hljs-type">double</span> hours, <span class="hljs-type">double</span> payRate)</span> {
<span class="hljs-keyword">return</span> hours * payRate / <span class="hljs-number">2</span>;
}
};
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">HOURS_PER_SHIFT</span> <span class="hljs-operator">=</span> <span class="hljs-number">8</span>;
<span class="hljs-keyword">abstract</span> <span class="hljs-type">double</span> <span class="hljs-title function_">overtimePay</span><span class="hljs-params">(<span class="hljs-type">double</span> hrs, <span class="hljs-type">double</span> payRate)</span>;
<span class="hljs-type">double</span> <span class="hljs-title function_">pay</span><span class="hljs-params">(<span class="hljs-type">double</span> hoursWorked, <span class="hljs-type">double</span> payRate)</span> {
<span class="hljs-type">double</span> <span class="hljs-variable">basePay</span> <span class="hljs-operator">=</span> hoursWorked * payRate;
<span class="hljs-keyword">return</span> basePay + overtimePay(hoursWorked, payRate);
}
}
}
Annotation
在 Java 1.5 之前时常有这样的情况,通过为程序元素进行特殊的命名以提供特殊的功能,比如 JUnit 中测试方法必须为 test 开头。
当然,这种方式在某种程度上确实可行,但不够优雅。
比如:
- 错误的文字拼写并不会有任何提示,直到运行时才会发现出了问题。
- 其次,这种方式无法特指某个程序元素,比如用户将某个类名的开头做了特殊命名,希望作用于类中所有的方法,结果可能没有提示、没有效果、没有意义。
- 而且,这种方式太单调,比如我想和某个方法的参数或者和声明抛出的异常进行交互。当然,反射也可以,但问题是我如何在不知道用户行为的情况下提供反射方法。
平时工作很少提供过注解,大多数情况都是使用别人提供的注解。
没想过没有注解会是什么样子,但和 naming pattern 一比较发现确实太重要了。
比如在下面的例子,声明一个注解用于表示测试方法:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
//..
}
对于代码中的 retention 和 target,我们有专门的术语叫做 "元注解 (meta-annotation)"。
而对于这种没有参数,仅仅标注程序元素的注解,我们称作 "标记注解 (marker annotation)"。
如果需要给注解声明参数并不复杂,只是相当于给一个类添加实例 field。
如下代码,表示测试时发生异常数组中的异常时进行通过:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
当然,注解本身对程序元素并没有直接的影响,它无法改变代码本身的语义。
我们需要依赖于特定的注解处理类。
当然,并不是一个注解就对应一个处理类,一个处理类也可以处理很多种注解。
比如下面的代码为 Test 和 ExceptionTest 提供了处理:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + "failed:" + exc);
} catch (Exception exc) {
System.out.println("INVALID @Test:" + m);
}
}
<span class="hljs-keyword">if</span> (m.<span class="hljs-title function_ invoke__">isAnnotationPresent</span>(ExceptionTest.<span class="hljs-keyword">class</span>)) {
tests++;
<span class="hljs-keyword">try</span> {
m.<span class="hljs-title function_ invoke__">invoke</span>(<span class="hljs-literal">null</span>);
System.out.<span class="hljs-title function_ invoke__">printf</span>(<span class="hljs-string">"Test %s failed: no exception%n"</span>, m);
} <span class="hljs-keyword">catch</span> (<span class="hljs-built_in">Throwable</span> wrappedExc) {
<span class="hljs-built_in">Throwable</span> exc = wrappedExc.<span class="hljs-title function_ invoke__">getCause</span>();
Class<span class="hljs-meta"><?</span> <span class="hljs-keyword">extends</span> <span class="hljs-built_in">Exception</span>>[] excTypes = m.<span class="hljs-title function_ invoke__">getAnnotation</span>(
ExceptionTest.<span class="hljs-keyword">class</span>).<span class="hljs-title function_ invoke__">value</span>();
<span class="hljs-keyword">int</span> oldPassed = passed;
<span class="hljs-keyword">for</span> (Class<span class="hljs-meta"><?</span> <span class="hljs-keyword">extends</span> <span class="hljs-built_in">Exception</span>> excType : excTypes) {
<span class="hljs-keyword">if</span> (excType.<span class="hljs-title function_ invoke__">isInstance</span>(exc)) {
passed++;
<span class="hljs-keyword">break</span>;
}
}
<span class="hljs-keyword">if</span> (passed == oldPassed)
System.out.<span class="hljs-title function_ invoke__">printf</span>(<span class="hljs-string">"Test %s failed: %s %n"</span>, m, exc);
}
}
}
System.out.<span class="hljs-title function_ invoke__">printf</span>(<span class="hljs-string">"Passed: %d, Failed: %d%n"</span>, passed, tests - passed);
}
}
代码就不多做解释了,主要是通过反射判断注解和获取异常。
其实标记注解非常常见,但说到标记注解就不得不说标记接口,比如 Serializable 什么的仅仅是作为注明。
相比接口只能在类名后面加上 implements,注解可以作用于更多的程序元素。于是便得出结论,标记接口可以淘汰了?
但这样过于片面。
首先,被接口标记的类提供该接口的实现,而这一点是注解无法做到的,就算有处理类进行补助也无法成为一种约束。
就 Serializable 而言,如果被标记的类没有提供实现,ObjectOutputStream.write(Object)
则毫无意义。
另外,这个接口有点特殊,它确实是一种约束,但在编译期没给出警告。
我之前以为 write 方法没有定义在 Serializable 中可能有什么特殊意义,但作者原话是:
Inexplicably, the authors of the ObjectOutputStream API did not take advantage of the Serializable interface in declaring the write method.
可见他也不知道其中的意义,既然如此,我们也不仿效这种作法了吧。
第二点是接口标记地更加精确。
乍一看似乎有些矛盾,相比接口只能作用于类元素,注解可以作用于多种元素不是注解的优点吗?
其实作者表达的并不是这个观点,就一个接口和 Target 为 ElementType.Type 的注解而言,后者可以作用于任何类和接口。
作者用 Set 接口进行了说明,Set 这种情况有些特殊,Set 继承了 Collection 接口。
乍一看,Set 似乎不是一个标记接口,它声明了太多方法。
参考:
The Set interface places additional stipulations, beyond those inherited from the Collection interface, on the contracts of all constructors and on the contracts of the add, equals and hashCode methods. Declarations for other inherited methods are also included here for convenience. (The specifications accompanying these declarations have been tailored to the Set interface, but they do not contain any additional stipulations.)
但作者将其描述为 "a restricted marker interface",它声明的方法与 Collection 接口是相同的。
Set 并没有改进 Collection 的契约,只是为实现类多提供了一种抽象描述。
但即便如此,也不能把注解设计成至少有一个参数的形式。
首先不得不承认,能标记的类型比接口更多,这个确实是一个优势。
另外,在一个类中,同一种标记注解可以出现多次,这一点也是其优势。
而最重要的,相比接口这种约定 (即,声明后被一些类提供了实现,在后期版本中很难修改这个接口),注解则可以在后期变得更丰富。