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语言中用
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。标记注解是特殊类型的注解,其中不包含成员。标记注解的唯一目的就是标记声明。
- 标记接口的优点:标记接口允许你在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误。
- 标记注解的优点:便于扩展,可以给已被使用的注解类型添加更多信息(元注解)。而接口实现后不可能再添加方法。
标记接口与标记注解如何选择:
- 标记接口和标记注解都各有好处。若想要定义一个任何新方法都不会与之关联的类型,就是用标记接口。若想要标记成员元素而非类和接口(方法或成员变量)或未来可能要给标记添加更多信息,就使用标记注解。