第30条: 用enum代替int常量

枚举类型是指由一组固定的常量组成合法值得类型。例如一年中的季节,太阳系中的行星或一副牌中的花色。在开发中我们经常在类使用static final来定义一个int常量。这种方式存在诸多不足,在类型安全性和使用方便性方面没有任何帮助。

看几个枚举类型的例子,体会一下其用法。

太阳系中的行星:

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;
private final double radius;
private final double surfaceGravity;
private static final double G = 6.67300E-11;
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;
}
}
public class WeightTable {

    public static void main(String[] args) {
// TODO Auto-generated method stub
double earthWeight = 70;
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for(Planet p : Planet.values()){
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
} }

一个操作符的例子:

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);
}
}

使用switch方式显然不够好,我们改进一下。

public enum Operation2 {
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;
Operation2(String symbol){
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
private final static Map<String, Operation2> stringToEnum = new HashMap<String, Operation2>();
static {
for(Operation2 op : Operation2.values()){
stringToEnum.put(op.toString(), op);
}
}
public static Operation2 fromString(String symbol){
return stringToEnum.get(symbol);
}
abstract double apply(double x, double y);
public static void main(String[] args){
double x = Double.parseDouble("2");
double y = Double.parseDouble("4");
for(Operation2 op : Operation2.values()){
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
Operation2 op = fromString("+");
System.out.println(op);
}
}

一个计算正常工资和加班工资的例子:

public 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;
switch(this){
case SATURDAY: case SUNDAY:
overtimePay = hoursWorked * payRate / 2;
default:
overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
break;
}
return basePay + overtimePay;
}
}

代码十分简洁,但从维护角度来看,它非常危险,同样是因为switch。如果新添加一个枚举,但忘记在switch语句中添加相应的case,则程序逻辑会出问题。

修改版:

public enum PayrollDay2 {
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;
private PayrollDay2(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;
}
};
abstract double overtimePay(double hrs, double payRate);
private static final int HOURS_PER_SHIFT = 8;
double pay(double hoursWorked, double payRate){
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
}

总而言之,与int相比,枚举类型的优势是不言而喻的。枚举易读得多,也更加安全,功能更加强大。

第31条:用实例域代替序数

枚举类型有一个ordinal方法,它范围该常量的序数从0开始,不建议使用这个方法,因为这不能很好地对枚举进行维护,正确应该是利用实例域,例如:

public enum Ensemble2 {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10);
private final int nuberOfMusicians;
Ensemble2(int size){
this.nuberOfMusicians = size;
}
public int numberOfMusicians(){
return nuberOfMusicians;
}
}

第32条:用EnumSet代替位域

位域表示法允许利用位操作,有效地执行先联合和交集这样的集合操作。但是位域有着int枚举常亮的所有缺点,甚至更多。当位域一数字形式打印时,翻译位域比翻译简单的int枚举常量要困难得多。甚至,要遍历位域表示的所有元素都没有很容易的方法。

public class Text {
public static final int STYLE_BOLD = 1 << 0; //
public static final int STYLE_ITALIC = 1 << 1; //
public static final int STYLE_UNDERLINE = 1 << 3; //
public static final int STYLE_STRIKETHROUGH = 1 << 3; // public void applyStyles(int styles){}
}

java.util 包提供了EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合。这个类实现Set接口,提供了丰富的功能,类型安全性,以及可以从任何其他Set实现中得到的互用性。但是在内部具体的实现上,每个EnumSet内容都表示为位矢量。如果底层的枚举类型有64个或者更少的元素——大多数如此。整个EnumSet就用单个long来表示,因此它的性能比的上位域的性能。批处理,如removeAll和retainAll,都是利用位算法来实现的。就像手工替代位域实现得那样。但是可以避免手工操作时容易出现的错误以及不太雅观的代码,因为EnumSet替你完成了这项艰巨的工作。`

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

总而言之,正因为枚举类型要用在集合Set中,所以没有理由用位域来表示它。

第33条:用EnumMap代替序数索引

有时候我们会利用ordinal()方法来索引数组的代码。这方法可行,但隐藏着问题,例如数组与泛型不能兼容,程序需要进行未受检的转换,序数int不能提供枚举类型的安全,容易使用错误的值等。举些例子就明白了:

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;
} public static void main(String[] args){
//Using ordinal() to index an array - Don't do this
Herb[] garden = {new Herb("plant1", Type.ANNUAL), new Herb("plant2", Type.BIENNIAL), new Herb("plant3", Type.PERENNIAL),
new Herb("plant4", Type.PERENNIAL), new Herb("plant5", Type.ANNUAL), new Herb("plant6", Type.BIENNIAL)};
// @SuppressWarnings("unchecked")
// 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);
// }
// for(int i = 0; i < herbsByType.length; i++){
// System.out.printf("%s : %s%n", Herb.Type.values()[i], herbsByType[i]);
// } //Using an EnumMap to associate data with an enum
Map<Herb.Type, Set<Herb>> herbsByType2 = new EnumMap<>(Herb.Type.class);
for(Herb.Type t : Herb.Type.values()){
herbsByType2.put(t, new HashSet<Herb>());
}
for(Herb h : garden){
herbsByType2.get(h.type).add(h);
}
System.out.println(herbsByType2);
}
}
public enum Phase2 {
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 Phase2 src;
private final Phase2 dst;
Transition(Phase2 src, Phase2 dst){
this.src = src;
this.dst = dst;
}
private static final Map<Phase2, Map<Phase2, Transition>> m = new EnumMap<>(Phase2.class);
static {
for(Phase2 p : Phase2.values()){
m.put(p, new EnumMap<Phase2, Transition>(Phase2.class));
}
for(Transition trans : Transition.values()){
m.get(trans.src).put(trans.dst, trans);
}
}
public static Transition from(Phase2 src, Phase2 dst){
return m.get(src).get(dst);
}
}
}

总之,最好不用序数来索引数组,而要用EnumMap。

第34条:用接口模拟可伸缩的枚举

之前写过一个operation的例子:

/**
* 加减乘除枚举
* Created by yulinfeng on 8/20/17.
*/
public enum Operation {
PLUS {
double apply(double x, double y) {
return x + y;
}
},
MIUS {
double apply(double x, double y) {
return x - y;
}
},
TIMES {
double apply(double x, double y) {
return x * y;
}
},
DEVIDE {
double apply(double x, double y) {
return x / y;
}
}; abstract double apply(double x, double y);
}

这个方法也不错,但从软件开发的可扩展性来说这并不是一个好的方法,软件可扩展性并不是在原有代码上做修改,这个时候就需要接口出场了,修改上述例子:

public interface IOperation {
double apply(double x, double y);
}
public enum ExtendedOperation implements IOperation{
EXP("^"){
public double apply(double x, double y){
return Math.pow(x, y);
}
};
private final String symbol;
private ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public static void main(String[] args){
double x = Double.parseDouble("2");
double y = Double.parseDouble("3");
}
}

这样当我们需要扩展操作符枚举的时候只需要重新实现IOperation接口即可。这样就达到代码的可扩展性,但这样做的有一个小小的不足,就是无法从一个枚举类型继承到另外一个枚举类型。

第35条:注解优先于命名模式

使用命名模式来表明有些程序元素需要通过某种工具或者框架来进行特殊处理有几个严重的缺点:

1.文字拼写错误会导致失败,且没有任何提示

2.无法确保它们只用于相应的程序元素上

3.不能提供将参数值与程序元素关联起来的好方法。

举几个注解的例子:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test { }
public class Sample {
@Test
public static void m1(){
System.out.println("aaaxxx");
}
public static void m2(){}
@Test
public static void m3(){
throw new RuntimeException("Boom");
}
@Test
public void m4(){}
@Test
public static void m5(){
System.out.println("aaaxxx2");
}
@Test
public static void m6(){
throw new RuntimeException("Boom2");
}
}
public class RunTests {

    public static void main(String[] args) throws Exception{
int tests = 0;
int passed = 0;
String className = "chapter6.Sample";
Class testClass = Class.forName(className);
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);
} }

测试main方法通过反射技术来获取相关注解,并测试相关方法。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1(){
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2(){
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m4(){
} public static void main(String[] args) throws Exception{
int tests = 0;
int passed = 0;
String className = "chapter6.Sample2";
Class testClass = Class.forName(className);
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);
}
}catch(Exception exc){
System.out.println("INVALID @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}

上面这个例子类似于处理Test注解的代码,但点不同,这段代码提取了注解参数的值,并用它检验该测试抛出的异常是否为正确的类型。

总之,我们大多数程序员都不需要定义注解类型,但是我们应该使用平台提供的注解类型。

第36条:坚持使用override注解

对于传统程序员而言,在所有的注解类型中,最重要的就是override注解了。它表示被注解的方法声明覆盖了超类中的一个声明。如果坚持使用该注解,可以防止一大类的非法错误。举个双字母对的例子:

public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second){
this.first = first;
this.second = second;
} // public boolean equals(Bigram b){
// return b.first == first && b.second == second;
// }
@Override
public boolean equals(Object obj) {
if(!(obj instanceof Bigram)){
return false;
}
Bigram b = (Bigram) obj;
return b.first == first && b.second == second;
}
public int hashCode(){
return 31 * first + second;
}
public static void main(String[] args){
Set<Bigram> s = new HashSet<>();
for(int i = 0; i < 10; i++){
for(char ch = 'a'; ch <= 'z'; ch++){
s.add(new Bigram(ch, ch));
}
}
System.out.println(s.size());
}
}

从代码中可以看出,被注释掉的equals方法没有正确覆盖超类的该方法。而如果坚持使用override注解,则会进行代码检验,不会出现这种错误。

第37条:用标记接口定义类型

标记接口是没有包含方法声明的接口而只是指明一个类实现了具有某种属性的接口,例如实现了Serializable接口表示它可被实例化。在有的情况下使用标记注解比标记接口可能更好,但书中提到了两点标记接口胜过标记注解:

1) 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。 //这条有点不好理解,希望有大神有更加通俗地解释

2) 尽管标记注解可以锁定类、接口、方法,但它是针对所有,标记接口则可以被更加精确地锁定。

另外书中也提到标记注解优于标记接口的地方:那就是能标记程序元素而非类和接口,且在未来能给标记添加更多的信息。

5.枚举和注解_EJ的更多相关文章

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

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

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

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

  3. java--加强之 jdk1.5简单新特性,枚举,注解

    转载请申明出处:http://blog.csdn.net/xmxkf/article/details/9944041 Jdk1.51新特性(静态导入,可变参数,加强for循环,自动拆装箱) 08.ja ...

  4. 第8章 枚举类&注解

    8.枚举及注解 8.1 如何自定义枚举类 1课时 8.2 如何使用关键字enum定义枚举类 1课时 8.3 枚举类的主要方法 1课时 8.4 实现接口的枚举类 1课时 8-1 枚举类 枚举类入门 枚举 ...

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

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

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

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

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

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

  8. 【Java基础】枚举和注解

    在Java1.5版本中,引入了两个类型:枚举类型enum type和注解类型annotation type. Num1:用enum代替int常量 枚举类型enum type是指由一组固定的常量组成合法 ...

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

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

随机推荐

  1. Monad Explained in One Picture

    The point of Monad is composability. In the green category, T -> Monad<U> and U -> Monad ...

  2. Windows 10 IoT Core 17101 for Insider 版本更新

    除夕夜,微软发布了Windows 10 IoT Core 17101 for Insider 版本更新,本次更新只修正了一些Bug,没有发布新的特性. 已知的问题: F5 driver deploym ...

  3. 【npm】伙计,给我来一杯package.json!不加糖

    前言:夜深了,我熬了一锅热气腾腾的package.json,给大家端上来,希望大家喜欢 json和JS对象的区别 package.json,顾名思义,它是一个json文件,而不能写入JS对象. 所以我 ...

  4. QQ的成功,远没有你想象的那么顺利和轻松

    本文来自公众号“傅老师”(ID:fustory)的原创分享,感谢作者. 1.引言 如果QQ是一个人,看似风光,其实从出生到成长,过程饱经错荡,堪算坎坷.它的人生历程确实也够励志的了.   学习交流: ...

  5. 干了这碗鸡汤:从理发店小弟到阿里P10技术大牛

    1.引言 MIT TR 35(MIT Technology Review 35 Innovators Under 35)——“全球 35 位 35 岁以下科技创新青年”榜单,是全球最权威的青年科技创新 ...

  6. 腾迅云获取免费SSL证书并布置

    上次申请了SSL证书一直没时间布置,今天重新再来操作一次 首先需要申请SSL证书,腾迅云买的域名有免费一年的SSL证书申请,网址:https://console.cloud.tencent.com/s ...

  7. 解决ionic3 android 运行出现Application Error - The connection to the server was unsuccessful

    在真机上启动ionic3打包成的android APK,启动了很久结果弹出这个问题: Application Error - The connection to the server was unsu ...

  8. Salesforce Sales Cloud 零基础学习(四) Chatter

    Chatter是一个Salesforce实时协作应用程序,它允许你的用户一起工作.互相交谈和共享信息,不管用户角色或位置如何,连接.并激励用户在整个组织内高效工作. Chatter 让用户们在 Opp ...

  9. 使用docker搭建数据分析环境

    注:早在学习<云计算>这门课之前就已经知道docker,学习这门课时老师还鼓励我们自己尝试一下:但是直到去年年底才有机会尝试,用过之后感觉确实很好用.最近需要部署几个shiny应用,又回顾 ...

  10. SpringBoot 启动概述

    透过现象看本质,SpringApplication 只是将一个典型的Spring应用的启动流程进行了扩展,因此,透彻理解 Spring 容器是打开 Spring Boot 大门的一把钥匙. Sprin ...