JDK1.5中加入一种新的类——枚举类型、一种新的接口——注解类型。
第30条:用enum代替int
枚举类型是指由一组固定的常量组成合法值的类型。例如一年中的季节、太阳系智能柜的行星或一副牌中的花色。在枚举类型出现之前,表示枚举类型的模式是使用一组具名的int常量:
//JDK1.5之前的老式写法
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_CRNNY_SMITH = 2;
...
这种方式称为int枚举模式。有诸多缺点。
而代替为使用枚举类型:
public enum Apple {
FUJI, PIPPIN, CRANNY_SMITH
...
}
Java 枚举本质上是int值。
Java枚举类型就是通过公有的静态final域为每个枚举常量导出实例的类。由于没有可以访问的构造方法,所以客户端不能创建枚举类型的实例,也不能对其扩展,即枚举类型是实例受控的。它们是单例的。
枚举类型还允许添加任意的方法和域,并实现任意的接口。枚举类型提供了所有的Object方法的高级实现,而且还是先了Comparable和Serializable接口。
举个栗子:
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 double surfaceGravity;
private static final double G = 6.67300E-11;
//构造器
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
this.surfaceGravity = G * mass / (radius * radius);
}
public double mass() {
return mass;
}
public double radius() {
return radius;
}
public double surfaceweight(double mass) {
return mass * surfaceGravity;
}
}
在Planet类中便是的是太阳系中的八颗行星。每颗行星都有质量和半径通过这两个属性可以计算出行星的表面重力加速度,从而计算出物体所受重力。
每个枚举常量后括号中的数值就是传递给构造器的参数,这里指的是行星质量和半径。
为了将数据与枚举常量关联起来,需要声明实例域,并编写一个带有数据并将数据保存在域中的构造器。由于枚举类是不可变类,所以所有的域都应该是final的,而且最好是private的。
下面这个方法可以计算出在地球上的某个物体,放到任何行星后所受重力:
public class WeightTable {
public static void main(String[] args){
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for(Planet p : Planet.values()) {
System.out.println("Weight on %s is %f%n",p,p.surfaceWeight(mass));
}
}
}
注意,所有的枚举类都有一个静态的values方法,按照声明的顺序返回它的值数组。下面显示输出结果:
Weight on MERCURY is 66.133672
Weight on VENUS is 158.383926
Weight on EARTH is 175.000000
Weight on MARS is 66.430699
Weight on JUPITER is 442.693902
Weight on SATURN is 186.464970
Weight on URANUS is 158.349709
Weight on NEPTUNE is 198.846116
特定于常量的方法实现可以与特定于常量的额数据结合起来。例如,下面的Operation覆盖了toString来返回通常与该操作符关联的符号:
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 = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for(Operation op : Operation.values()) {
System.out.println("%f %s %f = %f%n",x,op,y,op.apply(x,y));
}
}
输出:
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000
枚举类型还能以嵌套的形式出现:
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);
}
private enum PayType {
WEEKDAY {
double overtimePay(double hours,double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours,double payRate) {
return hours * payRate / 2;
};
private static final int HOURS_PER_SHIFT = 0;
abstract double overtimePay(double hrs,double payRate);
double pay(double hourWorked,double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hourWorked,payRate);
}
}
总结:何时该用枚举类型?每当需要一组固定的常量的时候。当然,这包括“天然的枚举类型”,比如行星,一周天数,棋子等。也包括你在编译时就知道其所有可能只的集合,如菜单选项,操作代码,等。总之,与int常量相比,枚举类型的优势是不言而喻的,枚举易读得多,很多枚举都不需要显式的构造器或成员,其他一部分则受益于“每个常量与属性的关联”以及“提供行为收这个属性影响的方法”。如果有多个枚举共享相同行为,则可以考虑嵌套枚举(如上所示)。
第31条:用实例域代替序数
每个枚举类型都有一个ordinal方法,它返回每个枚举常量在类型中的数字位置:
public enum Ensemble {
SOLO, DUET, TRIO,QUARTET,QUINTET,SWXTETE,SEPTET,OCNET,DECTET;
public int numberOfMusicians() {
return ordinal() + 1;
}
}
上面的枚举类有一个问题:如果改变枚举常量的顺序,那么numberOfMusicians方法将返回不一样的结果。
可以显式地为这些枚举常量指定值:
public enum Ensemble {
SOLO(1),DEUT(2),TRIO(3),QUARTET(4),QUINTET(5),SEXTET(6),SEPTET(7),OCTET(8),DOUBLE_QUARTET(9),NONET(10),DECTET(11),TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}
第32条:用EnumSet代替域位
略。。
第33条:用EnumMap代替序数索引
有时候可能这样使用枚举类型:
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;
thos.type = type;
}
@Override
public String toString() {
return name;
}
}
现在假设有一个香草的数组,表示一座花园中的植物,如果按照类型(一年生、多年生、两年生 等)进行组织之后将这些植物列出来,这样的话,需要3个集合,每种类型一个,并遍历整个花园,将每种香草放到相应的集合中:
Herb[] garden = ...;
Set<Herb>[] herbsByType = new (Set<Herb>[]) 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 < herbVyType.length;++i) {
System.out.println("%s: %s%n",Herb.Type.values()[i],herbsByType[i]);
}
}
上述代码可以完成工作,但存在问题:当访问一个按照枚举的序数进行做引的数组时,使用正确的int值就是程序员的职责了,int不能提供枚举类型的安全,若使用了错误的值,就会出现逻辑错误。
解决办法是使用EnumMap,一种非常快速的Map实现专门用于枚举键:
Map<Herb.Type, Set<Herb>> herbByType = new EnumMap<Herb.Type,Set<Herb>>(Herb.Type.class);
for(Herb.Type t : Herb.Type.values()) {
herbByType .put(t,new HashSet<Herb>());
}
for(Herb h : garden) {
herbByType.get(h.type).add(h);
}
System.out.println(herbByType);
总之,最好不要用序数来索引数组,而要使用EnumMap。
第34条:用接口模拟可伸缩的枚举
这面的程序对30条中的代码进行了修改:
public interface Operation {
double apply(double x,double y);
}
public enum Operation implements 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;
}
虽然枚举类型不可扩张,但接口是可扩展的:
假设需要定义一个上述操作的扩展,由求幂和求余组成:
public 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;
}
}
}
总之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型。
第35条:注解优于命名模式
命名模式是JDK1.5之前使用的模式,现在已经被注解替代。
假设想要定义一个注解类型来指定简单的测试,它们自动运行,并在抛出异常时失败:
import java.lang.annotation.*;
/**
* 该注解只用于无参静态方法
*
**/
@Retention(RententionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
上面就是一个名为Test的注解类型,其实就是一个接口。它自身通过Rentention和Target注解进行了注解。
这两种注解称作元注解。注解@Retention(RententionPolicy.RUNTIME)
表明,Test注解应该在运行时保留(?这啥意思?),如果没有保留,测试工具就知道无法知道Test注解。
元注解@Target(ElementType.METHOD)
表明,Test注解只在方法声明中超时合法的, 即它不能运用到类声明、域声明或者其他程序元素的声明上。
Test注解上还有一个注释:“该注解只用于无参静态方法 ”,这个检查是编译器无法检测的,如果将该注解用在有参数的非静态方法上,编译器检查也不会出错。
下面应用Test注解,将自定义的Test注解用在方法上,称作标记注解,因为它没有参数,只是“标注”被注解的元素,如果您拼错了Test,或是将该注解用在了类上或是域上,则无法编译通过:
public class Sample {
//合法
@Test
public static void mi(){}
public static void m2(){}
//编译合法,但运行失败
@Test
public static void m3() {
throw new RunTimeException("Boom");
}
//不合法 ,不是静态方法
@Test
public void m4(){}
//编译合法,但运行失败
@Test
public void m5(){
throw new RuntimeException("Crash");
}
}
上面的测试中,m3()和m5()抛出异常,m4()不合法,m1()合法,m2()被测试工具忽略。
注意,注解并不会对程序的语义造成任何影响,它只提供信息供相关的程序使用,但是,注解可以通过工具进行特殊处理。:
举个栗子:
public class RunTests {
public static void main(String[] args) {
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);
for(Method m : testClass.getDeclaredMethod()) {
if(m.isAnnotationPresent(Test.Class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch(InvocationTargetException w) {
Throwable exc = w.getClass();
System.out.println(m + " failed: " + exc);
} catch(Exception e) {
System.out.println("Invalid @Test: " + e);
}
}
}
System.out.printf("Passed: %d,Failed: %d%n", passed,tests - passed);
}
}
上面代码的意思是,在命令行上输入一个类名(如Sample),通过反射,查找该类中所有被注解了Test的方法,并执行这些方法,如果方法运行时抛出异常,就把这些异常封装到InvocationTargetException 异常 中,并打印输出。
第二个catch负责捕获Test注解的错误用法,比如给一个非静态的方法注解了Test、给带参数的方法注解了Test等等。
显然,如测试Sample类,那么方法m3()和m5()将被InvocationTargetException异常捕获到,而只有方法m1()可以测试通过,而m4()将被第二个catch,也就是Exception 捕获到。
现在要针对只在抛出异常时才成功的测试添加支持。为此需自定义一个县的注解类型:
@RententionPolicy.RUNTIME
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
这个自定义注解的参数类型是Class<? extends Exception>
,这个通配符类型无疑很绕口。它的意思是“扩展Exception的类的Class对象”,它允许该注解的用户指定任何异常类型,这种用法是有限制的类型令牌的一个示例,下面应用该注解:
public class Sample {
@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 m3() {
}
}
修改测试类:
if(m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.println(Test %s failed: no exception%n",m);
}
catch (InvocationTargetException: wrappedEx) {
Throwable exc = wrappedEx.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);
}
}
这段代码提取了注解参数的值,并用它检验该测试抛出的异常是否为正确的类型。
第36条:坚持使用Override注解
随着JDK1.5增加的注解,类库中也增加了几种注解类型。最常用的注解莫过Override类型了,这个注解只能用在方法声明中,表示备注街的方法声明覆盖了超类中的一个声明。
坚持使用该注解,可以避免一大类的非法错误。下面的类Bigram表示一个双字母组或者有序的字母对:
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;
}
public int hashCode() {
return 31 * first + second;
}
public static void main(String[] args) {
Set<Bigram> s = new HashSet<Bigram>();
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());
}
}
上面的方法期待的结果是得到从”aa”到”zz”的字符循环,Set最终有26个元素。但是,Set集合中最终存储了260个元素。这与实际初衷相悖。原因就是并没有重写equals方法。而是将equals方法给重载了,实际程序在运行中并不会调用这个重载的equals方法,而是调用基类的equals方法,基类的对象知识通过比较对象的地址而判断欲插入的元素与Set容器中的元素有没有相同的如果不相同就插入,显然每新new的一个对象的地址都不同,所以只要新new一个对象们就会被插入到HashSet容器中。
幸运的是,编译器可以帮助发现这个错误。但是我们需要让编译器知道要覆盖这个方法而不是重载。为了做到这一点,需要使用注解@Override标注Bigram.equals方法:
@Override
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
编译后发现编译器报错。于是修改:
@Override
public boolean equals(Object o) {
if(!(o instanceof Bigram)) {
return false;
}
Bigram b = (Bigram)o;
return b.first == first && b.second == second;
}
所以,如想要在子类中覆盖超类中的某个方法,就需要使用Override注解
第37条:用标记接口定义类型
标记接口是没有包含方法声明的接口,这种接口只是指明了一个类实现了具有某种属性的接口,如实现了Serializable接口的类就表示这个类可以被写到ObjectOutputStream。
标记接口优于注解,它们可以被更加精确地锁定。如果注解类型使用@Target(ElementType.TYPE) 声明,他就可以被应用到任何类或接口。假设有一个标记只适用于特殊接口的实现,如果将它定义成一个标记接口,就可以用它将唯一的接口扩展成它适用的接口。