Effective Item 15 - 枚举

于Java 1.5增加的enum type.

enum type是由一组固定的常量组成的类型,比如四个季节、扑克花色。

在出现enum type之前,通常用一组int常量表示枚举类型。

比如这样:

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;

如果只是想用作枚举,感觉这样也没什么。

但如果把上面的苹果和橘子互作比较,或者写成....

int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;

虽合法但诧异,这是在做果汁吗?

而且,这种常量是compile-time常量,编译后一切都结束了,使用这个常量的地方都被替换为该常量的值。

如果该常量值需要改变,所有使用该常量的代码都必须重新编译。

更糟糕的情况是,不重新编译也可以正常运行,只不过会得到无法预测的结果。

(ps:我觉得更遭的是有人直接把常量值写到代码里...)

另外,比如上面的APPLE_FUJI,我想打印它的名字,不是它的值。

不仅如此,我还想打印所有苹果,我想打印苹果一共有多少种类。

当然,如果想打印也可以,只是相比直接使用enum,无论怎么做都很麻烦。

如果使用enum,比如:

 public enum Apple  { FUJI, PIPPIN, GRANNY_SMITH } 
 public enum Orange { NAVEL, TEMPLE, BLOOD }

看起来就是一堆常量,但是enum没有实例,也没有可访问的构造器,无法对其进行扩展。

enum本身就是final,所以很多时候也直接用enum实现singleton。

enum在编译时是类型安全的,比如有地方声明了上面代码中的Apple类型的参数,那么被传到该参数的引用肯定是三种苹果之一。

而且enum本身就是一个类型,可以有自己的方法和field,而且可以实现接口。

附上书中太阳系enum,很难想象如果有类似需求时用普通常量来实现。

也许我可以声明一个Planet类,再给它加上field的方法,然后在一个constant类中声明为final

但这样却无法保证Planet类仅用作常量,所以还是用enum吧:

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; // In kilograms
	private final double radius; // In meters
	private final double surfaceGravity; // In m / s^2

	// Universal gravitational constant in m^3 / kg s^2
	private static final double G = 6.67300E-11;

	// Constructor
	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; // F = ma
	}
}

然后我们就可以这样使用Planet enum,无论是值还是名字,使用起来都很自然:

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.printf("Weight on %s is %f%n",p, p.surfaceWeight(mass));
    }
}

其实像Planet这样的方式对多数使用枚举的场景而言足够了。

也就是说每个Planet常量表达的是不同的数据,但也有例外。

比如,我们要为enum中的每一个常量赋予不同的行为。

下面是书中用enum表达计算的例子:

import java.util.HashMap;
import java.util.Map;

public enum Operation {
	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;

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

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

	abstract double apply(double x, double y);

	private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();
	static { 
		for (Operation op : values())
			stringToEnum.put(op.toString(), op);
	}

	public static Operation fromString(String symbol) {
		return stringToEnum.get(symbol);
	}

	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.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
	}
}

对不同的枚举常量进行switch..case..其实也能表达出我们想要的效果。

如果以后增加了新的常量则需要再对应加上一个case,当然,不加也不会有任何提示,然后最坏的情况就是运行时出了问题。

如上面的代码是常量行为的正确使用方法,即constant-specific method implementation。

为行为提供一个抽象,并为每一个常量提供一个实现,即一个枚举常量也是constant-specific class body。

采用这种方式时,如果新增一个常量,则必须提供一个方法实现,否则编译器会给出提示,这就多了一层保障。

遗憾的是,这种方式也有缺陷。

比如我们有这样一个需求,计算某一天的薪水,这个某一天可以是一周中的某一天,也可能是某个节日,比如周一到周五使用相同的运算方式,周末另算,某节日另算。

也就是说我需要在枚举中声明代表周一到周日的常量,如果我继续使用之前的方式去声明一个抽象方法,如果周一到周五采用完全一样的计算,则会出现五段完全相同的代码。

但即使这样我们也不能用回switch..case..方式,增加一个常量时强制选择其选择一种行为实现是必须的。

于是我们有一种叫strategy enum的方式,即枚举中声明另外一个枚举的field,该field则代表策略,并提供策略相关的行为。

下面是书中代码:

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 = 8;

		abstract double overtimePay(double hrs, double payRate);

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

Effective Item 15 - 枚举

时间: 2024-10-10 07:55:38

Effective Item 15 - 枚举的相关文章

Item 15:资源管理类需要提供对原始资源的访问 Effective C++笔记

Item 15: Provide access to raw resources in resource-managing classes. 在一个完美的设计中,所有的资源访问都应通过资源管理对象来进行,资源泄漏被完美地克服.然而世界是不完美的, 很多API会直接操作资源,尤其是一些C语言的API.总之,你会时不时地发现有需要直接访问资源, 所以资源管理对象需要提供对原始资源访问.获取资源的方式有两类:隐式地获取和显式地获取. 通常来讲,显式的资源获取会更好,它最小化了无意中进行类型转换的机会.

Effective Item 10 - 尽量使可变性最小化

不可变类,即实例不能被修改的类,实例中包含的所有信息在对象的生命周期内固定不变. 常见的比如String.基本类型的封装类.BigDecimal.BigInteger. 相对与可变类,不可变更易于设计.实现.使用,且更稳定(less prone to error)更安全. 比如,不可变类本质上就是线程安全的,不需要做同步处理,使用起来也非常方便. 设计不可变类,我们有5个原则: 1.不提供任何可修改对象状态的方法.(这种方法也成为mutator,相对词汇"可变性(mutability)"

Effective Item 5 - 覆盖equals方法时需要注意

多数情况下,我们不会去覆盖equals方法. 什么时候不需要覆盖equals? ·类的每个实例本质上是唯一的,我们不需要用特殊的逻辑值来表述,Object提供的equals方法正好是正确的. ·超类已经覆盖了equals,且从超类继承过来的行为对于子类也是合适的. ·当确定该类的equals方法不会被调用时,比如类是私有的. 如果要问什么时候需要覆盖equals? 答案正好和之前的问题相反. 即,类需要一个自己特有的逻辑相等概念,而且超类提供的equals不满足自己的行为. (PS:对于枚举而言

Effective Item 3 - 避免不必要的对象

通常,我们更喜欢重用一个对象而不是重新创建一个. 如果对象是不可变的,它就始终可以被重用. 下面是一个反面例子,Joshua Bloch明确指出[DON'T TO THIS]: String s = new String("stringette"); 该语句每次执行时都创建一个新的实例. String构造器中的参数"stringette"本身是一个实例,功能方面等同于那些通过构造器创建的对象. 如果这种语句放到循环里,效果会变得更糟. 于是我们只需要: String

Effective Item 17 - 关于方法的参数声明

给方法的参数加上限制是很常见的,比如参数代表索引时不能为负数.对于某个关键对象引用不能为null,否则会进行一些处理,比如抛出相应的异常信息. 对于这些参数限制,方法的提供者必须在文档中注明,并且在方法开头时检查参数,并在失败时提供明确的信息,即detect errors as soon as possible after they occur,这将成为准确定位错误的一大保障. 如果没有做到这一点,最好的情况是方法在处理过程中失败并抛出了莫名其妙的异常,错误的源头变得难以定位,但这是最好的情况.

Effective Item 13 - 慎用tagged class

其实作者的原标题是<Prefer class hierarchies to tagged classes>,即用类层次优于tagged class. 我不知道有没有tagged class这么一说,其实作者指的tagged class的是一个类描述了多种抽象,可以根据某个field决定不同的实例. 下面是书中例子,使用shape和部分表示长度的field构成形状并计算面积,脑补一下什么是tagged class: class Figure { enum Shape { RECTANGLE, C

Effective Item 9 - 尽量使可访问性最小化

模块设计是否良好,有个重要的因素在于,相对外部模块是否隐藏内部数据以及实现细节. 设计良好的模块会隐藏实现细节,并将API与其实现隔离开来. 模块之间通过API进行通信,对于内部工作情况互不可见. 即,封装(encapsulation)--软件设计的基本原则之一. 为什么要封装? 通过封装可以有效地接触各个模块之间的耦合关系,使这些模块可以独立地开发.测试.优化.使用.理解和修改. 即: ·可以增加开发效率,模块可以并行开发. ·封装可以减轻维护的负担,可以更有效的进行优化,且不会影响其他模块的

Effective Item 6 - 覆盖equals方法时不要忘记hashCode方法

任何覆盖了equals方法的类都需要覆盖hashCode方法. 忽视这一条将导致类无法与基于散列的数据结构一起正常工作,比如和HashMap.HashSet和Hashtable. 下面是hashCode相关规范: ·在程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这个对象调用多少次hashCode,起结果必须始终如一地返回同一个证书. 如果是同一个程序执行多次,每次调用的结果可以不一致. ·如果两个对象根据equals方法比较是相等的,那么两个对象的hashCo

Effective Item 2 - 遇到多个构造器参数时考虑使用Builder

静态工厂和够构造器有一个共同的局限性:遇到大量的参数时无法很好的扩展. 先说说构造器. 其实field不多时重叠构造器(telescoping constructor)是个不错的方法,易于编写也易于调用,这种方式在参数数量较少时也很常见. 但问题是参数很多(可能越来越多)时,比如(现在已经很难找到对多个参数进行重叠构造的代码了,于是在这里直接引用一下书中的代码): public class NutritionFacts { private final int servingSize; // (m