30、用enum代替int常量

枚举类型是指由一组固定的常量组成合法值的类型。在java没有引入枚举类型前,表示枚举类型的常用方法是声明一组不同的int常量,每个类型成员一个常量,这种方法称作int枚举模式。采用int枚举模式的程序是十分脆弱的,因为int值是编译时常量,若与枚举常量关联的int发生变化,客户端就必须重新编译。

  • java枚举类型背后的思想:通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。客户端既不能创建枚举类型的实例,也不能对它进行扩展。枚举类型是实例受控的,它们是单例的泛型化,本质上是单元素的枚举。枚举提供了编译时的类型安全。

  • 包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间。可以增加或重新排列枚举类型中的常量,而无需重新编译客户端代码,因为常量值并没有被编译到客户端代码中。可以调用toString方法,将枚举转换成可打印的字符串。

枚举类型允许添加任意的方法和域,并实现任意的接口。枚举类型默认继承Enum类(其实现了Comparable、Serializable接口)。为了将数据与枚举常量关联起来,得声明实例域,并编写一个将数据保存到域中的构造器。枚举天生就是不可变的,所有的域都必须是final的。

例如:


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; //质量kg
private final double radius; //半径
private final double surfaceGravity; //表面重力,final常量构造器中必须初始化 private static final double G = 6.673E-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 static void main(String[] args) {
double earthWeight = 175;
double mass = earthWeight/Planet.EARTH.surfaceGravity();
for(Planet p : Planet.values()) {
//java的printf方法中换行用%n, C语言中用\n
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}
}

上面的方法对大多数枚举类型来说足够了,但有时你需要将本质上不同的行为与每个常量关联起来。这时通常需要在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主题中实现这个方法。这个方法被称作特定于常量的方法实现。例如:

public enum Operation {
PULS("+") {
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); public static void main(String[] args) {
double x = 2.0;
double y = 4.0;
for(Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x,y));
}
}

枚举类型中的抽象方法,在它的常量中必须被实现。除了编译时常量之外,枚举构造器不可以访问枚举的静态域,因为构造器运行时,静态域还没被初始化。

特定于常量的方法,使得在枚举常量中共享代码变的更加困难。例如:根据给定的工人的基本工资(按小时算)和工作时间,用枚举计算工人当天的工作报酬。其中加班工资为平时的1.5倍。

public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURADAY, FRIDAY, SATURDAY, SUNDAY; private static final int HOURS_PER_SHIFT = 8; double pay(double hoursWorked, double payRate) {
switch(this) {
case SATURDAY: case SUNDAY :
return hoursWorked*payRate*1.5;
default :
return hoursWorked - HOURS_PER_SHIFT > 0
?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate)
: hoursWorked*payRate;
}
} public static void main(String[] args) {
System.out.println(PayrollDay.MONDAY.pay(10,10));
System.out.println(PayrollDay.SUNDAY.pay(10,10));
}
}

上面这段代码虽然十分简洁,但是维护成本很高。每将一个元素添加到该枚举中,就必须修改switch语句。可以使用策略枚举来进行优化,例如:

public enum PayrollDay {
MONDAY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURADAY(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 pay(double hoursWorked, double payRate) {
return hoursWorked - HOURS_PER_SHIFT > 0
?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate)
: hoursWorked*payRate;
}
},
WEEKEND {
double pay(double hoursWorked, double payRate) {
return hoursWorked * payRate * 1.5;
}
};
private static final int HOURS_PER_SHIFT = 8; abstract double pay(double hoursWorked, double payRate);
} public static void main(String[] args) {
System.out.println(PayrollDay.MONDAY.pay(10,10));
System.out.println(PayrollDay.SUNDAY.pay(10,10));
}
}

总之,与int常量相比,枚举类型优势明显。许多枚举都不需要显式的构造器或成员。当需要将不同的行为与每个常量关联起来时,可使用特定于常量的方法。若多个枚举常量同时共享相同的行为,考虑使用策略枚举。

31、用实例域代替序数

所有的枚举都有一个ordinal方法,它返回枚举常量在类中的位置。若常量进行重排序,它们ordinal的返回值将发生变化。所以,永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中。

public enum Planet {
MERCURY(1),
VENUS(2),
EARTH(3),
MARS(4),
JUPITER(5),
SATURN(6),
URANUS(7),
NEPTUNE(8); private final int numOrd;
Planet(int numOrd) {this.numOrd = numOrd; } public int numOrd(){ return numOrd; }
}

Enum规范中谈到ordinal时写道:它是用于像EnumSet和EnumMap这种基于枚举的数据结构的方法,平时最好不要使用它。

32、用EnumSet代替位域

若枚举类型要用在集合中,可以使用EnumSet类。EnumSet类是专为枚举类设计的集合类,EnumSet中的所有元素都必须是单个枚举类型中的枚举值。若元素个数小于64,整个EnumSet就用一个long来表示,所以它的性能比的上位域(通过位操作实现)的性能。

import java.util.*;

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) {
// Body goes here
} // Sample use
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}

EnumSet类集位域的简洁和性能优势及枚举的所有优点与一身。y应使用EnumSet代替位域操作。

33、用EnumMap代替序数索引

EnumMap是一种键值必须为枚举类型的映射表。虽然使用其它的Map实现(如HashMap)也能完成枚举类型实例到值的映射,但是使用EnumMap会更加高效。由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值,这使得EnumMap的效率比其它的Map实现(如HashMap也能完成枚举类型实例到值的映射)更高。

注意:EnumMap在内部使用枚举类型的ordinal()得到当前实例的声明次序,并使用这个次序维护枚举类型实例对应值在数组中的位置。

例如:


import java.util.*;
public class DatabaseInfo {
private enum DBType { MYSQL, ORACLE, SQLSERVER } private static final EnumMap<DBType, String> urls
= new EnumMap<>(DBType.class); static {
urls.put(DBType.MYSQL, "jdbc:mysql://localhost/mydb");
urls.put(DBType.ORACLE, "jdbc:oracle:thin:@localhost:1521:sample");
urls.put(DBType.SQLSERVER, "jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=mydb");
} private DatabaseInfo() {} public static String getURL(DBType type) {
return urls.get(type);
} public static void main(String[] args) {
System.out.println(DatabaseInfo.getURL(DBType.SQLSERVER));
System.out.println(DatabaseInfo.getURL(DBType.MYSQL));
}
}

不要用序数(ordinal方法)来索引数组,而要使用EnumMap。若所表示的关系是多维的,可以使用EnumMap<.., EnumMap<..>>。一般情况下不要使用Enum.ordinal

34、用接口模拟可伸缩的枚举

枚举类型都默认继承自java.lang.Enum类。虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,以实现对程序的扩展。

例如:


import java.util.*;
//测试
public class Test { public static <T extends Enum<T> & Operation> void test(
Class<T> opSet, double x, double y) {
for(Operation op : opSet.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x,y));
}
} public static void main(String[] args) {
double x = 2.0;
double y = 4.0;
test(ExtendedOperation.class, x, y);
test(BasicOperation.class, x, y); }
} //接口
interface Operation {
double apply(double x, double y); //默认为public的
} //基本操作,实现Operation接口
enum BasicOperation implements Operation{
PULS("+") {
//访问权限必须为public,否则报错
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
}; private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() { return symbol; }
} //扩展操作,实现Operation接口
enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) { return Math.pow(x,y); } //必须实现
},
REMAINDER("%") {
public double apply(double x, double y) { return x % y; }
}; private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
} @Override
public String toString() { return symbol; }
}

其中<T extends Enum<T> & Operation>确保了Class对象既是枚举类型又是Operation的子类型。

35、注解优先于命名模式

java1.5之前,一般使用命名模式来对程序进行特殊处理。如,JUnit测试框架要求用test作为测试方法名称的开头。使用命名模式有几个缺点:

  • 文字拼写错误会导致失败,且没有任何提示。如,test写成tset
  • 无法确保它们只用于相应的程序元素上。如,变量名使用test开头
  • 没有提供将参数值与程序元素关联起来的方法

注解很好的解决了这些问题。关于注解的详细用法请看 java基础(二),Annotation(注解)

例如:

import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ExceptionTest {
Class<? extends Exception>[] value();
} class Sample {
@ExceptionTest( { IndexOutOfBoundsException.class,
NullPointerException.class})
public static void doublyBad() {
//List<String> list = new ArrayList<>();
List<String> list = null;
list.add(5,null);
}
} public class RunTest {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("Sample");
for(Method m : testClass.getDeclaredMethods()) {
if(m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try{
m.invoke(null);
}catch (InvocationTargetException ite) {
//Throwable exc = ite.getTargetException();
Throwable exc = ite.getCause();
Class<? extends Exception>[] excTypes
= m.getAnnotation(ExceptionTest.class).value();
for(Class<? extends Exception> excType : excTypes) {
if(excType.isInstance(exc)) {
excType.newInstance().printStackTrace();
}
}
}
}
}
}
}

在利用 Method 对象的 invoke 方法调用目标对象的方法时, 若在目标对象的方法内部抛出异常, 会抛出 InvocationTargetException 异常, 该异常包装了目标对象的方法内部抛出异常, 可以通过调用 InvocationTargetException 异常类的的 getTargetException() 方法得到原始的异常.

在编写一个需要程序员给源文件添加信息的工具时,应该定义一组适当的注解,而不是使用命名模式。

36、坚持使用Override注解

@Override注解只能用在方法声明中,它表示被注解的方法声明覆盖了超类型中的一个声明。使用Override注解可以有效防止覆盖方法时的错误。

例如:想要在String中覆盖equals方法

//这是方法重载,将产生编译错误
@Override
public boolean equals(String obj) {
....
} //覆盖
@Override
public boolean equals(Object obj) {
....
}

37、用标记接口定义类型

标记接口是指没有任何属性和方法的接口,它只用来表明类实现了某种属性。如,Serializable接口,通过实现这个接口,类表明它的实例可以被写到ObjectOutputStream。标记注解是特殊类型的注解,其中不包含成员。标记注解的唯一目的就是标记声明。

  • 标记接口的优点:标记接口允许你在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误。
  • 标记注解的优点:便于扩展,可以给已被使用的注解类型添加更多信息(元注解)。而接口实现后不可能再添加方法。

标记接口与标记注解如何选择:

  • 标记接口和标记注解都各有好处。若想要定义一个任何新方法都不会与之关联的类型,就是用标记接口。若想要标记成员元素而非类和接口(方法或成员变量)或未来可能要给标记添加更多信息,就使用标记注解。

Effective java笔记(五),枚举和注解的更多相关文章

  1. Effective Java笔记一 创建和销毁对象

    Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参数时要考虑用构建器 第3条 用私有构造器或者枚举类型强化Singleton属性 第4条 ...

  2. Effective java笔记(二),所有对象的通用方法

    Object类的所有非final方法(equals.hashCode.toString.clone.finalize)都要遵守通用约定(general contract),否则其它依赖于这些约定的类( ...

  3. effective java笔记之单例模式与序列化

    单例模式:"一个类有且仅有一个实例,并且自行实例化向整个系统提供." 单例模式实现方式有多种,例如懒汉模式(等用到时候再实例化),饿汉模式(类加载时就实例化)等,这里用饿汉模式方法 ...

  4. Effective Java 第三版——39. 注解优于命名模式

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  5. effective java笔记之java服务提供者框架

    博主是一名苦逼的大四实习生,现在java从业人员越来越多,面对的竞争越来越大,还没走出校园,就TM可能面临失业,而且对那些增删改查的业务毫无兴趣,于是决定提升自己,在实习期间的时间还是很充裕的,期间自 ...

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

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

  7. 《Effective Java》读书笔记 - 6.枚举和注解

    Chapter 6 Enums and Annotations Item 30: Use enums instead of int constants Enum类型无非也是个普通的class,所以你可 ...

  8. Effective java笔记5--通用程序设计

    一.将局部变量的作用域最小化      本条目与前面(使类和成员的可访问能力最小化)本质上是类似的.将局部变量的作用域最小化,可以增加代码的可读性和可维护性,并降低出错的可能性. 使一个局部变量的作用 ...

  9. Effective java笔记(七),通用程序设计

    45.将局部变量的作用域最小化 将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性. Java允许在任何可以出现语句的地方声明变量(C语言中局部变量要在代码块开头声明),要使 ...

随机推荐

  1. IOS第三天-新浪微博 - 版本新特性,OAuth授权认证

    *********版本新特性 #import "HWNewfeatureViewController.h" #import "HWTabBarViewController ...

  2. easyUI datagrid 根据查询条件 选中对应数据的行

    开始 输入了 土豆,南瓜,再次是小青菜,每次输入点击搜索的时候(模糊查询),选中的当前数据对应的行 在做之前,在网上查询了许多资料,也在技术群里问过许多次,弄了好久终于好了. 第一次写博客真不知道写啥 ...

  3. ABP理论学习之启动配置

    返回总目录 本篇目录 配置ABP 配置模块 为模块创建配置 为了在应用启动时配置ABP和模块,ABP提供了一个基础设施. 配置ABP 配置ABP是在模块的PreInitialize事件中完成的.下面的 ...

  4. ABP理论学习之功能管理

    返回总目录 本篇目录 介绍 功能类型 定义功能 检查功能 功能管理者 版本说明 介绍 大多数的Saas(多租户)应用都有不同 功能的 版本(包).因此,他们可以给租户(客户)提供不同的 价格和功能选项 ...

  5. Nova PhoneGap框架 第六章 使用Mock

    在我们的框架中引入了一个很重要的设计,那就是使用Mock. 这里的mock是指cordova.mock.js文件,它模拟了PhoneGap(Cordova)的API,从而可以在浏览器中运行测试我们的程 ...

  6. Nodejs之MEAN栈开发(三)---- 使用Mongoose创建模型及API

    继续开扒我们的MEAN栈开发之路,前面两节我们学习了Express.Jade引擎并创建了几个静态页面,最后通过Heroku部署了应用. Nodejs之MEAN栈开发(一)---- 路由与控制器 Nod ...

  7. Silverlight及WPF中实现自定义BusyIndicator

    在开发Silverlight或者WPF项目时,当我们调用Web服务来加载一些数据时,由于数据量比较大需要较长的时间,需要用户等待,为了给用户友好的提示和避免用户在加载数据过程中进行重复操作,我们通常使 ...

  8. Btree 索引

    Btree 索引 索引是帮助数据库高效获取数据的一种数据结构,通过提取句子主干,就可以得到索引的本质. m-way查找树 如果想了解Btree,需要首先了解m-way数据结构. m-way查找树是是一 ...

  9. 《Entity Framework 6 Recipes》中文翻译系列 (33) ------ 第六章 继承与建模高级应用之TPH与TPT (2)

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 6-8  嵌套的TPH建模 问题 你想使用超过一层的TPH继承映射为一张表建模. 解 ...

  10. Hystrix框架4--circuit

    circuit 在Hystrix调用服务时,难免会遇到异常,如对方服务不可用,在这种情况下如果仍然不停地调用就是不必要的,在Hystrix中可以配置使用circuit,当达到一定程度错误,就会自动调用 ...