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 // Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11; // Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
} public double mass() {
return mass;
} public double radius() {
return radius;
} public double surfaceGravity() {
return surfaceGravity;
} public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}

然后我们就可以这样使用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; Operation(String symbol) {
this.symbol = symbol;
} @Override
public String toString() {
return symbol;
} abstract double apply(double x, double y); private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();
static {
for (Operation op : values())
stringToEnum.put(op.toString(), op);
} public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
} public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(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); 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 {
double overtimePay(double hours, double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
* payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * 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);
}
}
}

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);
}
} if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Exception>[] excTypes = m.getAnnotation(
ExceptionTest.class).value();
int oldPassed = passed;
for (Class<? extends Exception> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", 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的契约,只是为实现类多提供了一种抽象描述。

但即便如此,也不能把注解设计成至少有一个参数的形式。
首先不得不承认,能标记的类型比接口更多,这个确实是一个优势。
另外,在一个类中,同一种标记注解可以出现多次,这一点也是其优势。
而最重要的,相比接口这种约定(即,声明后被一些类提供了实现,在后期版本中很难修改这个接口),注解则可以在后期变得更丰富。

Java - 枚举与注解的更多相关文章

  1. java枚举和注解

    枚举 一.枚举(enumeration) 是一组常量的集合,可以理解为:枚举属于一种特殊的类,里面只包含一组有限的特定的对象,构造方法默认为private. 二.枚举的两种实现方式 1.自定义实现枚举 ...

  2. 编写高质量代码:改善Java程序的151个建议(第6章:枚举和注解___建议88~92)

    建议88:用枚举实现工厂方法模式更简洁 工厂方法模式(Factory Method Pattern)是" 创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其它子类" ...

  3. Effective java笔记(五),枚举和注解

    30.用enum代替int常量 枚举类型是指由一组固定的常量组成合法值的类型.在java没有引入枚举类型前,表示枚举类型的常用方法是声明一组不同的int常量,每个类型成员一个常量,这种方法称作int枚 ...

  4. [Effective Java]第六章 枚举和注解

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  5. 《Effective Java》学习笔记 —— 枚举、注解与方法

    Java的枚举.注解与方法... 第30条 用枚举代替int常量 第31条 用实例域代替序数 可以考虑定义一个final int 代替枚举中的 ordinal() 方法. 第32条 用EnumSet代 ...

  6. Java复习——枚举与注解

    枚举 枚举就是让某些变量的取值只能是若干固定值中的一个,否则编译器就会报错,枚举可以让编译器在编译阶段就控制程序的值,这一点是普通变量无法实现的.枚举是作为一种特殊的类存在的,使用的是enum关键字修 ...

  7. [Java读书笔记] Effective Java(Third Edition) 第 6 章 枚举和注解

    Java支持两种引用类型的特殊用途的系列:一种称为枚举类型(enum type)的类和一种称为注解类型(annotation type)的接口. 第34条:用enum代替int常量 枚举是其合法值由一 ...

  8. Java基础(十)——枚举与注解

    一.枚举 1.介绍 枚举类:类的对象只有有限个,确定的.当需要定义一组常量时,强烈建议使用枚举类.如果枚举类中只有一个对象,则可以作为单例模式的实现. 使用 enum 定义的枚举类默认继承了 java ...

  9. java 反射,注解,泛型,内省(高级知识点)

     Java反射 1.Java反射是Java被视为动态(或准动态)语言的一个关键性质.这个机制允许程序在运行时透过Reflection APIs    取得任何一个已知名称的class的内部信息, 包括 ...

随机推荐

  1. php—Smarty-2

    一.注释 *注释内容* Html注释显示客户端源文件中 Smarty注释不会发给客户端 Smarty的注释主要给模板设计者来看的 二.模板中的变量 l  由php文件分配 1)  普通变量 2)  数 ...

  2. 如何给LOJ补全special judge

    首先你要会写一个叫$data.yml$的东西, 这里面记录了这道题的$subtask$计分策略 也告诉了评测姬这道题是提交答案还是$spj$还是交互题 那么,$YAML$语言是啥啊? 别问我,我也不会 ...

  3. Linux 开机过程(转)

    Linux 开机过程 初始化 POST(加电自检)并执行硬件检查: 当 POST 完成后,系统的控制权将移交给启动管理器的第一阶段(first stage),它存储在一个硬盘的引导扇区(对于使用 BI ...

  4. TCP 和 UDP 协议

    TCP 和 UDP 协议 一.socket层 Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐 ...

  5. 资产管理 cmdb之ansible 获取服务器硬件、软件等信息

    cmdb抓取服务信息的方式有很多种,可以使用自动化工具saltstack.ansible.puppet,或者使用其它模块直接ssh远程连接抓取服务器信息.这里记录一下用ansible的API接口调用s ...

  6. QuantLib 金融计算——基本组件之 DateGeneration 类

    目录 QuantLib 金融计算--基本组件之 DateGeneration 类 QuantLib 金融计算--基本组件之 DateGeneration 类 许多产品的估值依赖于对未来现金流的分析,因 ...

  7. 任务调度SpringTask

    一.什么是任务调度 在企业级应用中,经常会制定一些“计划任务”,即在某个时间点做某件事情,核心是以时间为关注点,即在一个特定的时间点,系统执行指定的一个操作.常见的任务调度框架有Quartz和Spri ...

  8. 网络基础 08_NAT

    1 NAT的基本概念 为什么需要NAT IPv4地址紧缺 什么是NAT NAT(Network Address Translation) 私有IPv4地址 10.0.0.0 - 10.255.255. ...

  9. BZOJ - 1009 KMP+可达矩阵

    题意:存在一个长度为m的串str,求长度为n的不含str子串的字符串的方案数 什么鬼题目 设\(f[i][j]\):长为\(i\)的串中以\(i\)结尾的长度为\(j\)的后缀 与 模式串(str)中 ...

  10. 物联网学习之路——物联网通信技术:NBIoT

    NBIoT是什么 NB-IoT,Narrow Band Internet of Things,窄带物联网,是一种专为万物互联打造的蜂窝网络连接技术.顾名思义,NB-IoT所占用的带宽很窄,只需约180 ...