Chapter 6 Enums and Annotations

Item 30: Use enums instead of int constants

Enum类型无非也是个普通的class,所以你可以给他加class能有的东西,比如constructor:

public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6);
private final double mass; // In kilograms
private final double radius; // In meters
// Constructor,不能是public的哦(我猜的)
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}

我记得我在thinking in java里看过,比如这里的EARTH无非就是编译器自动生成的Plannet类型的一个field:static final Plannet EARTH = Planet(5.975e+24, 6.378e6)。

那么如果你想加一些constant specific(不同的enum实例有不同的行为)的行为的话,可能你会用这么做:

public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
double apply(double x, double y) {
switch(this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}

这里我学到一招,如果不写那句throw,编译会不通过“你必须在每个代码可能到达的地方写一个return”。但是,我个人感觉,switch case是一切不良编程习惯的典型代表,如果你要加一个新的enum constant,你还要加一个case,很可能你就忘了然后就吃瘪了。于是你可以用下面这种语法:

// Enum type with constant-specific method implementations
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;} }; abstract double apply(double x, double y);
}

应该很好懂所以我不解释了。

每一个Enum类型都有一个valueOf(String),从一个constant的名字得到一个真正的这个constant,但是如果你override了toString方法,也就意味着你的constant的名字和toString不一致,所以最好写一个fromString(String)方法,方法参考书上。

但是上面这种解决constant specific的方法有个缺点就是,不能“share code”,比如

// Enum that switches on its value to share code - questionable
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,SATURDAY, SUNDAY;
private static final int HOURS_PER_SHIFT = 8;
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
double overtimePay; // Calculate overtime pay
switch(this) {
case SATURDAY: case SUNDAY:
overtimePay = hoursWorked * payRate * 2;//假如周末是两倍加班费
default: // Weekdays
overtimePay = hoursWorked <= HOURS_PER_SHIFT ?0 : (hoursWorked - HOURS_PER_SHIFT) * payRate * 1.5 ;//假如平时是1.5倍
break;
}
return basePay + overtimePay;
}
}

这里的pay方法就是根据输入的某一天的工作小时数和payrate(就是你每小时的基本工资)计算出这一天的总薪酬(基本工资+加班费),比如计算星期一的薪酬:MONDAY.pay(10, 50)。

刚才说过了,switch case这种写法没有很好的可维护性,刚才介绍的“constant-specific method”语法也一样,你只能通过增加一些helper method来减少重复代码,不管怎么样都会降低可读性。

其实你可以想一下这个问题的本质:对于每个上面enum constant,你都需要一个计算overtime pay的strategy,所以你可以把这件事委托给另一个人专门负责(Strategy Pattern):

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);//委托给payType去做
}
// The strategy enum type
private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
* payRate * 1.5;
}
},
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);
}
}
}

虽然这个方法乍一看很繁琐,但是如果你的enum constants一旦变多就会体现出优势(所有strategy pattern的优势)。这个pattern适用于这种情况:if multiple enum constants share common behaviors。

虽然switch不适用于enum的内部实现,但是对某个enum的client来说还是很适用的。

Item 31: Use instance fields instead of ordinals

每一个enum constant都关联着一个int,而你可以用ordinal()来获得这个int。这条item的意思就是,永远都不要依靠ordinal()来计算或得到某个状态值,甚至最好完全不要用ordinal(),因为这个方法主要是被设计成服务于EnumSet和EnumMap的,举例:假设你要给每一个enum constant编个号,千万别这样:

// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Student {
JACK,JOE,JANE,JAKE;
public int studentNumber() { return ordinal() + 1; }
}

坏处一大堆,比如如果你声明enum constants的顺序变了,那么各个学生的编号就变了。正确做法是:

public enum Student {
JACK(1),JOE(2),JANE(3),JAKE(4);
private final int studentNumber;
Student(int number) { this.studentNumber= number; }
public int studentNumber() { return studentNumber; }
}

Item 32: Use EnumSet instead of bit fields

以前看Win32编程的时候经常看到这种:

// Bit field enumeration constants - OBSOLETE!
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
// Parameter is bitwise OR of zero or more STYLE_ constants
public void applyStyles(int styles) { ... }
}

然后你可以这么用:text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

现在有更好的选择:EnumSet,它实现了Set接口,于是可以把上面的改进一下:

// EnumSet - a modern replacement for bit fields
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) { ... }
}

然后可以这么用:text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

唯一的缺点是1.6版本的EnumSet不是immutable的。

Item 33: Use EnumMap instead of ordinal indexing

EnumMap是“key是Enum类型的Map”,比HashMap更高效,其内部用数组实现。

当你需要把一种enum constant映射到另一个value的时候,不要用”用ordinal方法作为数组下标“的数组 的方法,可读性和可维护性都很差。举个具体的例子吧(复习时可以选择性地跳过),首先千万别这样:

//植物类,分为“一年绿一次”,“常年绿”,“半年绿一次”三种类型。
public class Herb {
public enum Type { ANNUAL, PERENNIAL, BIENNIAL }
private final String name;
private final Type type;
Herb(String name, Type type) {
this.name = name;
this.type = type;
}
@Override public String toString() {return name;}
}

然后现在我们现在有a list of herbs,然后要根据类型分类,把相同类型的herb放到一个Set里去,所以有三种类型就有三个Sets,我们用一个Set[]数组herbsByType来表示:

// Using ordinal() to index an array - DON'T DO THIS!
Herb[] garden = ... ;
// Indexed by Herb.Type.ordinal()
Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length];
for (int i = 0; i < herbsByType.length; i++)
herbsByType[i] = new HashSet<Herb>();
for (Herb h : garden)
herbsByType[h.type.ordinal()].add(h);
// Print the results,也就是怎么取回来
for (int i = 0; i < herbsByType.length; i++) {
System.out.printf("%s: %s%n",Herb.Type.values()[i], herbsByType[i]);
}

这种做法问题一大堆,随便举一个:因为数组和泛型不兼容,所以你必须有一个有警告的cast:(Set<Herb>[])(这里我暂停想了一下为什么不能创建一个Set<Herb>的数组,个人觉得是因为数组是runtime保证type safe的,到了runtime的时候Set<随便什么>都等于Set,数组分不清)。正确做法是:

// Using an EnumMap to associate data with an enum
Map<Herb.Type, Set<Herb>> herbsByType =
new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);
for (Herb.Type t : Herb.Type.values())
herbsByType.put(t, new HashSet<Herb>());
for (Herb h : garden)
herbsByType.get(h.type).add(h);
System.out.println(herbsByType);

上面传给EnumMap的constructor的参数是一个Type Token:Herb.Type.class。

有的时候你可能需要两个enum作为key,就相像你需要一个二维平面的坐标(x,y)来得到一个位置。书上举得例子是:从liquid到solid是freezing,从liquid到gas是boiling等等,也就是:从两个“物质形态enum“(固态液态气态)map到一种”变化enum“(freezing,boiling...)。(艹,我顺便去复习了一下初中物理,原来从固态到气态叫升华(SUBLIME)) 下面举一下反例:

// Using ordinal() to index array of arrays - DON'T DO THIS!
public enum Phase { SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// Rows indexed by src-ordinal, cols by dst-ordinal
private static final Transition[][] TRANSITIONS = {
{ null, MELT, SUBLIME },
{ FREEZE, null, BOIL },
{ DEPOSIT, CONDENSE, null }
};
// Returns the phase transition from one phase to another
public static Transition from(Phase src, Phase dst) {
return TRANSITIONS[src.ordinal()][dst.ordinal()];
}
}
}

这个二维数组想象成一张表格就行。那么用EnumMap怎么做?这里我觉得需要一种“技巧”,因为EnumMap的key没法定义成“(x,y)”这种形式,所以你可以这样声明它的类型:Map<Phase, Map<Phase,Transition>> m,可以这么理解:第一次指定一个Phase的时候:m.get(src)得到的是“那张表格的某一行”,然后接着第二次get的时候:m.get(src).get(dst)得到的就是刚才那一行的某一列了。我个人认为,这种“技巧”是为了得到EnumMap的性能,如果不需要考虑性能,完全可以用"(x,y)"作为一个HashMap的key,感觉会比这个“技巧”好理解,从而提升可读性。下面放一下具体的代码:

public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), BOIL(LIQUID, GAS), CONDENSE(
GAS, LIQUID), SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase src;
private final Phase dst;
Transition(Phase src, Phase dst) {
this.src = src;
this.dst = dst;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase, Transition>> m = new EnumMap<Phase,
Map<Phase, Transition>>(Phase.class);//传入作为key的Enum类型
static {
for (Phase p : Phase.values())
m.put(p, new EnumMap<Phase, Transition>(Phase.class));
for (Transition trans : Transition.values())
m.get(trans.src).put(trans.dst, trans);//这句加粗
} public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
}

你可以注意到这里把Transition这个Enum Type扩展了一下,为的是上面那句加粗的语句,同时也提供了“(x,y)到z的映射”,这样就不用再被“下标是几的元素对应的是谁”的问题搞脑子了,此外,如果你要新加一种enum constant的话,这种写法甚至不需要你修改任何“EnumMap的部分”,很爽。

Item 34: Emulate extensible enums with interfaces

因为Enum类型不能被继承,如果你想“模拟继承”,可以给你的Enum加个接口。而你的Enum的所有API都是通过这个接口暴露出来的,也就是client在用你的Enum的时候,变量类型肯定是这个接口。这样的话client就可以实现自己的Enum类型,只要实现这个接口就行,然后任何需要“base enum”的地方,你都可以用你的“extension enum”来代替。个人理解:这里说继承不如说是“扩展”,只是如果库的enum类型无法满足你的需求,你就可以实现自己的,但是你自己的并没有包含原先enum类型中的任何功能。

作为类库实现者,你可以定义如下的方法,让其参数可以接收client自定义的enum:

public static <T extends Enum<T> & Operation> void test(Class<T> opSet)

<T extends Enum<T> & Operation>的意思是T必须满足 是一个Enum类型并且实现了Operation这个接口。然后比如client实现了一个叫ExtendedOperation的enum类型,直接传ExtendedOperation.class进去就行了。

或者你也可以把方法定义成:

public static void test(Collection<? extends Operation> opSet)

这样的好处是client可以自己先combine一些opSet(来自“base enum”的也好,来自其“extension enum”的也好)到同一个集合,然后再传进去,比如用Arrays.asList(ExtendedOperation.values())

但缺点是在方法内无法用EnumSet和EnumMap。

Item 35: Prefer annotations to naming patterns

这里的naming patterns就是指在Annotation出现之前,只能用某种约定的命名格式来完成 现在用annotation可以强制 的事。比如JUnit起初要求测试方法名必须以test开头,这种做法的坏处一大堆。而用annotation就好很多,下面举几个Annotation定义和用法的具体例子(复习时可以选择性跳过):

/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

上面那段注释是因为我们在tool或framework中用 被这个Annotation mark过的方法 的时候,假定都是static和parameterless的方法, 往下看你就懂了。用这个Annotation很简单,在方法声明上面一贴就行,而这么一贴对方法本身或者其所在的类没什么影响,只是一个信息(提供给那些对这个信息感兴趣的tools或者framework),比如像这样:

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);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}

因为上面用了m.invoke(null),所以如果没有遵守“必须在static和parameterless的方法上mark这个Annotation“的规定,就会抛出InvocationTargetException。

现在我们再写一个Annotation,用来标记那些“期待指定异常”的方法:

// Annotation type with a parameter
/**
* Indicates that the annotated method is a test method that
* must throw the designated exception to succeed.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}

我发现定义Annotation的时候不需要指定构造函数,只需要指定它的fields就行了。下面是这个Annotation的用法:

@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}

下面是相应的“Test Runner”的代码:

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(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Exception> excType =
m.getAnnotation(ExceptionTest.class).value();
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.printf("Test %s failed: expected %s, got %s%n",
m, excType.getName(), exc);
}
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}

这段代码从我们定义的ExceptionTest中提取出了value字段,然后判断和真正抛出的异常是不是一致。

再要加强的话,你还可以修改ExceptionTest让它接受一系列异常的class object,表示抛出其中任何一个异常都视为test通过,那么你只要把value这个字段改一下就行:

Class<? extends Exception>[] value();//只是变成数组而已

然后在用这个Annotation的时候,刚才上面所有的“single parameter”语法也是可以的,表示一个“single-element array”。而“multi-element”的参数语法如下:

@ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class })
public static void someMethod(){...}

对应的test runner也很好改,这里就不写了。

所以说,其实除了tool或者某些framework的实现者,我们一般是不需要定义Annotation的,但是我们应该用 tool或者framework以及Java platform本身提供的Annotation。

Item 36: Consistently use the Override annotation

像我就曾经因为C#带过来的命名习惯,把toString写成ToString结果也没加@Override,结果就变成overload了。所以应该养成用@Override的习惯,但是override接口或抽象类中的方法时,可以不加@Override

Item 37: Use marker interfaces to define types

一个marker interface就是一个里面啥方法声明都没有的interface。一个类实现这么一个interface只是为了“标记”一下自己有某种特点,比如Serializable这个interface。与这个marker interface类似的就是“marker annotation”,比如上上个item中最一开始的那个Test就是。作者认为marker interface和marker annotation各有各的好和坏。看到这我不经想问“你TM不是在讲clone方法的时候说用interface来标记某种特点是不对的吗?”,然后我貌似理解他的意思了:作者的意思是:Object里面的clone方法不应该接受一个Object类型的参数然后再if(obj instanceof Cloneable),而应该直接让Object里面的clone方法接受一个Cloneable类型的参数,这样就直接在编译时保证类型安全而不至于到运行时出错。marker annotation的优点在于可以“不断进化和改进”,而且可以贴在方法上;而marker interface一旦定义后就定死了,以后就不能再往里面新增方法了(Item 18)。

最新文章

  1. C++网络套接字编程TCP和UDP实例
  2. Codeforces Round #383 (Div. 2) 解题报告
  3. 20151215单选按钮列表,复选框列表:CheckBoxList
  4. pthread_create如何传递两个参数以上的参数
  5. vimrc配置文件_version1.0_+pathogen, taglist, wordcomplete插件说明
  6. Node.js 学习(二) 创建第一个应用
  7. 项目中的那些事---JavaScript
  8. UVa 884 - Factorial Factors
  9. JavaScript window.setTimeout() 的详细用法
  10. CLR基础与术语
  11. DotNetCore深入了解之三HttpClientFactory类
  12. ALSA概述--高级linux声音驱动基本介绍和应用
  13. 爬虫_拉勾网(selenium)
  14. surface pro app
  15. make[1]: *** [storage/perfschema/unittest/CMakeFiles/pfs_connect_attr-t.dir/all] 错误 2 解决方法
  16. VSTO:使用C#开发Excel、Word【15】
  17. NTFS文件系统简介
  18. SELinux介绍
  19. 课程一(Neural Networks and Deep Learning)总结——1、Logistic Regression
  20. NetScaler 10.1的配置以及结合StoreFront的部署

热门文章

  1. svn下载项目的时候出现 Path to certificate
  2. HTTP、HTTPS 了解一下
  3. 使用elasticsearch7.3版本在一台主机上部署多个实例组建集群
  4. 使用layer弹窗和layui表单做新增功能
  5. 如何申请百度小程序的appid(目前不支持个人账号申请)
  6. 十、LaTex数学公式初步
  7. ceph对接openstack
  8. Python 输出百分比
  9. 网络流 最大流 Drainage Ditches Dinic
  10. HelloWorld的解析