Effective Java笔记第五章枚举和注解第一节用enum代替int常量

Effective Java笔记第五章枚举和注解

第一节用enum代替int常量

1.枚举类型是指由一组固定的常量组成合法值的类型,例如:一年中的季节,太阳系中的行星或者一副牌中的花色。在编程语言中还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具名的int常量,每个类型成员一个常量:

public class Demo {

    public static final int APPLE_FUJI=0;
    public static final int APPLE_PIPPIN=1;
    public static final int APPLE_GRANNY_SMITH=2;

    public static final int ORANGE_NAVEL=0;
    public static final int ORANGE_TEMPLE=1;
    public static final int ORANGE_BLOOD=2;

    public static void main(String[] args) {
        int i = APPLE_FUJI - ORANGE_TEMPLE;
        System.out.println(i);
    }

}

这种方法称作int枚举模式,存在诸多不足。他在类型安全性和使用方便性方面没有任何帮助。
因为java没有为int枚举组提供命名空间。当两个int枚举组具有相同的命名常量时,前缀可以防止名称发生冲突。

2.采用int枚举模式的程序是十分脆弱的。因为int枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的int发生了变化,客户端就必须重新编译。如果没有重新编译,程序还是可以运行,但是他们的行为就是不确定的。

3.将int枚举常量翻译成可打印的字符串,并没有很便利的方法。如果将这种常量打印出来,或者从调试器中将他显示出来,你所见到的就是一个数字,这没有太大的作用。要遍历一个组中的所有int枚举常量,甚至获得int枚举组的大小,这些都说没有很可靠的方法。

4.有时候会碰到String枚举模式,就是使用的是String常量。虽然为这些常量提供了可打印的字符串,但是会导致性能问题,因为它依赖于字符串的比较操作。更糟糕的是,它会导致初级用户把字符串常量硬编码到客户端代码中,而不是使用适当的域名。如果这样的硬编码字符串中包含有书写错误,那么,这样的错误在编译时不会被检测到,但是在运行时却会报错。

5.从java1.5开始提出了一种可以替代的方法,使用enum枚举,这样就可以避免int和String枚举模式的缺点了。

public enum  Apple {
    FUJI,PIPPIN,GRANNY_SMITH;
}
public class Test {
    public static void main(String[] args) {
        System.out.println(Apple.FUJI);
	}
}

java的枚举本质上是int值。

6.java枚举类型背后的基本想法非常简单:他们就是通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。因为客户端既不能创建枚举类型的实例,也不能对他进行扩展,因此很可能没有实例,而只有声明过的枚举常量。换句话说,枚举类型是实例受控的。他们是单例的泛型化。本质上是单元素的枚举。枚举类型为类型安全的枚举模式。

7.枚举提供了编译时的类型安全。如果声明一个参数的类型为Apple,就可以保证,被传到该参数上的任何非null的对象一定属于三个有效的Apple值之一。试图传递类型错误的值时,会导致编译时错误,就像试图将某种枚举类型的表达式赋给另一种枚举类型的变量,或者试图利用==操作符比较不同枚举类型的值一样。

8.包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间。你可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码,因为导出常量的域在枚举类型和他的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中,而是在int枚举模式中。最终,可以通过调用toString方法,将枚举转换成可打印的字符串。

9.枚举类型还允许添加任意的方法和域,并实现任意的接口。他们提供了所有Object方法的高级实现,实现了Comparable和Serializable接口,并针对权举类型的可任意改变性设计了序列化方式。

10.将方法或者域添加到枚举类型中的作用:
1)将数据与他的常量关联起来。
2)可以利用任何适当的方法来增强枚举类型。枚举类型可以先作为枚举常量的一个简单集合,随着时间的推移再演变成为全功能的抽象。
下面我们举个例子:

public enum Planet {

    MERCURY(3.302e+32, 2.342e4),
    VENUS(4.302e+32, 3.342e4),
    EARTH(5.302e+32, 4.342e4),
    MARS(6.302e+32, 5.342e4),
    JUPITER(7.302e+32, 6.342e4),
    SATURN(8.302e+32, 7.342e4),
    URANUS(9.302e+32, 8.342e4),
    NEPTUNE(1.302e+32, 9.342e4);

    private final double mass;
    private final double radius;
    private final double surfaceGravity;

    private static final double G = 6.673E-11;
    //Constructor
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }

    public double getMass() {
        return mass;
    }

    public double getRadius() {
        return radius;
    }

    public double getSurfaceGravity() {
        return surfaceGravity;
    }

    public double surfaceWeight(double mass) {
        return mass * surfaceGravity;
    }

}

为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域都应该为final的。它们可以是公有的,但最好将他们做成是私有的,并提供公有的访问方法。
Planet功能十分强大,下面我们举个例子:

public class WeightTable {

    public static void main(String[] args) {
        double earthWeight = 123.1;
        double mass = earthWeight / Planet.EARTH.getSurfaceGravity();
        for (Planet p : Planet.values()) {
            System.out.printf("Weight on %s is %f%n",p,p.surfaceWeight(mass));
        }
    }

}

输出:
Weight on MERCURY is 263.512521
Weight on VENUS is 168.599190
Weight on EARTH is 123.100000
Weight on MARS is 96.664854
Weight on JUPITER is 79.467092
Weight on SATURN is 67.414286
Weight on URANUS is 58.510517
Weight on NEPTUNE is 6.530241

注意Planet就像所有的枚举一样,它有一个静态的values方法,按照声明顺序返回他的值数组。注意没有覆写的toString方法返回每个枚举值的声明名称。

11.与枚举常量关联的有些行为,可能只需要用在定义了枚举的类或者包中。这种行为最好被实现成私有的或者包级私有的方法。于是,每个枚举常量都带有一组隐蔽的行为,这使得包含该枚举的类或者包在遇到这种常量时都可以做出适当的反应。就像其他的类一样,除非迫不得已要将枚举方法导出至它的客户端,否则都应该将它声明为私有的,如有必要,则声明为包级私有的。

12.如果一个枚举具有普遍适用性,他就应该成为一个顶层类;如果他只是被用在一个特定的顶层类中,他就应该成为该顶层类的一个成员类。

13.有时需要将本质上不同的行为与每个常量关联起来。下面我们举个例子:

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

}

这段代码很脆弱,如果你添加了新的枚举常量,却忘记了给switch添加相应的条件,枚举仍然可是编译,但是当你试图运行新的运算时,就会运行失败。

14.对于上面的例子,我们有一个更好的方法:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法。这种方法被称作特定于常量的方法实现。

public enum OperationImporve {

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

    abstract double apply(double x, double y);

}

上面的代码,即使你忘记了写方法,编译器也会提醒,因为枚举类型中的抽象方法必须被它所有常量中的具体方法所覆盖。

15.特定于常量的方法实现可以与特定于常量的数据结合起来。比如说:上面的例子覆盖了toString来返回通常与该操作关联的符号。

public enum OperationImporve2 {

    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;

    OperationImporve2(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }

    abstract double apply(double x, double y);

}

在一些情况下,在没枚举中覆盖toString非常有用。例如上面的例子使得打印算数表达式非常容易。

public class Test {
    public static void main(String[] args) {
        double x=2.0;
        double y=4.0;
        for (OperationImporve2 value : OperationImporve2.values()) {
            System.out.printf("%f %s %f = %f%n",x,value,y,value.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

16.枚举类型有一个自动产生的valueOf(String)方法,它将常量的名字转变成常量本身。如果在枚举类型中覆盖toString,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举。下列代码(适当的改变了类型名称)可以为任何枚举完成这一技巧,只要每个常量都有一个独特的字符串表示法。
还是引用上文的的OperationImporve2类作为例子

//Implementing a  fromString method on an enum type
    private static final Map<String, OperationImporve2> stringToEnum = new HashMap<>();

    //Initialize map from constant name to enum constant
    //初始化从常量名称到enum常量的映射
    static {
        for (OperationImporve2 value : values()) {
            stringToEnum.put(value.toString(), value);
        }
    }

    //Return OperationImporve2 for string , or null if string is invalid
    public static OperationImporve2 fromString(String symbol) {
        return stringToEnum.get(symbol);
    }
public class Test {
    public static void main(String[] args) {
        //相当于调用OperationImporve2.toString
        System.out.println(OperationImporve2.PLUS);
        OperationImporve2 op = OperationImporve2.fromString("+");
        System.out.println(op.name());
        //相当于调用OperationImporve2.toString
        System.out.println(op);
        double apply = op.apply(1, 2);
        System.out.println(apply);
    }
}

输出:
+
PLUS
+
3.0
在常量被创建之后,OperationImporve2常量从静态代码块中被放入到stringToEnum的map中。试图使每个常量都从自己的构造器将自身放入到map中,会导致编译时错误。这是好事,因为如果这是合法的,就会抛出NullPointException异常。枚举构造器不可以访问枚举的静态域,除了编译时常量域之外。这一限制是很有必要的,因为构造器运行的时候,这些静态域还没有被初始化。

17.特定于常量的方法实现有一个美中不足的地方。他们使得在枚举常量中共享代码变得更加困难了。比如说:

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语句添加相应的case。程序依然可以编译,但pay方法会悄悄地将假期的工资计算成与正常工作日相同。

对于上文的例子,我们可以对他进行改进:将加班工资计算移到一个私有的嵌套枚举中,将这个策略枚举实例传到PayrollDay 枚举的构造器中。之后PayrollDay 枚举将加班工资计算委托给枚举策略,PayrollDay 中就不需要switch语句或者特定于常量的方法实现了。

public enum PayrollDayImprove {

    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;

    PayrollDayImprove(PayType payType) {
        this.payType = payType;
    }

    double pay(double hoursWorked, double payRate) {
        return payType.pay(hoursWorked, payRate);
    }

    //The strategy enum type
    private enum PayType {

        WEEKDAY {
            double overtimePay(double hoursWorked, double payRate) {
                return hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            double overtimePay(double hoursWorked, double payRate) {
                return hoursWorked * payRate / 2;
            }
        };

        private static final int HOURS_PER_SHIFT = 8;

        abstract double overtimePay(double hoursWorked, double payRate);

        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked, payRate);
        }

    }

}

18.枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为。比如说:OperationImporve2枚举不受你的控制,你希望它有一个实例方法来返回每个运算的反运算。

//Switch on an enum to simulate a missing method
    //打开枚举来模拟缺失的方法,
    public static OperationImporve2 inverse(OperationImporve2 op) {
        switch (op) {
            case PLUS:
                return OperationImporve2.MINUS;
            case MINUS:
                return OperationImporve2.PLUS;
            case TIMES:
                return OperationImporve2.DIVIDE;
            case DIVIDE:
                return OperationImporve2.TIMES;
            default:
                throw new AssertionError("Unknown op" + op);
        }
    }

19.与int常量相比,枚举有个小小的性能缺点,即装载和初始化枚举时会有空间和时间的成本。除了受资源约束的设备,在实践中不必太在意这个问题。

20.枚举的使用场景:每当需要一组固定常量的时候。
1)天然的枚举类型:行星,一周的天数以及棋子的数目等等。
2)在编译时就知道其所有可能值的其他集合:菜单的选项,操作代码以及命令行标记等。
枚举类型中的常量集并不一定要始终保持不变。专门设计枚举特性是考虑到枚举类型的二进制兼容演变。

21.总之,与int常量相比,枚举类型的优势是不言而喻的。枚举要易读得多,也更加安全,功能更加强大。许多枚举都不需要显示的构造器或者成员,但许多其他枚举则受益于"每个常量与属性的关联"以及"提供行为受这个属性影响的方法"。只有极少数的枚举受益于将多种行为与单个方法关联。在这种相对少见的情况下,特定于常量的方法要优先于启用自有值的枚举。如果多个枚举常量同时共享相同的行为,则考虑枚举策略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值