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)。

《Effective Java》读书笔记 - 6.枚举和注解的更多相关文章

  1. Effective Java 读书笔记之五 枚举和注解

    Java1.5中引入了两个新的应用类型家族,新的类为枚举类型,新的接口为注解类型. 一.用enum代替int常量 1.枚举值由一组固定的常量组成合法值的类型. 二.用实例域代替序数 1.不要根据枚举的 ...

  2. Effective java读书笔记

    2015年进步很小,看的书也不是很多,感觉自己都要废了,2016是沉淀的一年,在这一年中要不断学习.看书,努力提升自己 计在16年要看12本书,主要涉及java基础.Spring研究.java并发.J ...

  3. Effective Java读书笔记完结啦

    Effective Java是一本经典的书, 很实用的Java进阶读物, 提供了各个方面的best practices. 最近终于做完了Effective Java的读书笔记, 发布出来与大家共享. ...

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

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

  5. Effective Java 读书笔记(一):使用静态工厂方法代替构造器

    这是Effective Java第2章提出的第一条建议: 考虑用静态工厂方法代替构造器 此处的静态工厂方法并不是设计模式,主要指static修饰的静态方法,关于static的说明可以参考之前的博文&l ...

  6. [Effective Java 读书笔记] 第6章 枚举和注解

    第三十条 用enum代替int 总得来说,使用enum有几点好处 1.编译时的类型安全, 2.可以保证就是自己定义的值,不会有月结风险, 3.每个枚举类型有自己的命名空间 4.枚举可以添加任意的方法和 ...

  7. Effective Java 读书笔记(五):Lambda和Stream

    1 Lamdba优于匿名内部类 (1)DEMO1 匿名内部类:过时 Collections.sort(words, new Comparator<String>() { public in ...

  8. Effective Java 读书笔记之七 通用程序设计

    一.将局部变量的作用域最小化 1.在第一次使用变量的地方声明 2.几乎每个变量的声明都应该包含一个初始化表达式:try-catch语句是一个例外 3.使方法小而集中是一个好的策略 二.for-each ...

  9. Effective Java 读书笔记之一 创建和销毁对象

    一.考虑用静态工厂方法代替构造器 这里的静态工厂方法是指类中使用public static 修饰的方法,和设计模式的工厂方法模式没有任何关系.相对于使用共有的构造器来创建对象,静态工厂方法有几大优势: ...

随机推荐

  1. 0-1-Tree CodeForces - 1156D (并查集)

    大意: 给定树, 边权为黑或白, 求所有有向路径条数, 满足每走过一条黑边后不会走白边. 这题比赛的时候想了个假算法, 还没发现..... 显然所求的路径要么全黑, 要么全白, 要么先全白后全黑, 所 ...

  2. C++ 引用深入理解

    1.引用作为变量的别名存在,因此可以在一些场合代替指针. 引用相当于指针来说具有更好的可读性和实用性. 例如: /* 编译环境 gcc version 7.4.0 (Ubuntu 7.4.0-1ubu ...

  3. Windows向Linux上传文件夹

      1.将文件夹压缩成.tar.gz文件: 安装7-Zip,选择要压缩的文件夹--右键--“7-Zip”--“添加到压缩包...”,压缩格式选择“tar”, 在此目下就生成了“文件夹名.tar”文件, ...

  4. css3 伪类以及伪元素的特效

    菱形          

  5. Linux音频编程(三)混音器介绍

    一.介绍 1.mixer:用来控制多个输入.输出的音量,也控制输入(microphone,line-in,CD)之间的切换,可以将多个信号组合或者叠加在一起.声卡上的混音器由多个混音通道组成,它们可以 ...

  6. Ubuntu18.04 安装redis

    Redis是常用基于内存的Key-Value数据库,比Memcache更先进,支持多种数据结构,高效,快速.用Redis可以很轻松解决高并发的数据访问问题:作为实时监控信号处理也非常不错. 安装red ...

  7. java面试(反射)05

    1.什么是反射 JAVA反射机制是在运行状态中,对于任意一个类,都能够获取这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取类信息以及动态调用对象内容就称为jav ...

  8. springboot-不同名称项目的 redis session共享

    引入JAR <dependency> <groupId>org.springframework.session</groupId> <artifactId&g ...

  9. 常用命令之------tcpdump

    语法: tcpdump [options] [not] proto dir type 案例 tcpdump -i eth0 host 192.168.1.1 and icmp -n -w /tmp/t ...

  10. java 线程池 ScheduledExecutorService

    ScheduledExecutorService执行周期性或定时任务 https://blog.csdn.net/u013851082/article/details/70207640