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

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