枚举和注解
第30条 用enum代替int常量
int枚举模式的缺点:
1.无命名空间,所以要加前缀防止名称冲突
2.int枚举是编译时常量,一旦常量关联的int值变化,就要重新编译
3.没有很好的打印字符串的方法(有一种String枚举常量,但是性能不好)
Enum优点:
1.有命名空间
2.toString方法提供良好的打印字符串的功能
3.提供**编译时**类型安全
另外,还可以添加方法和域到枚举类型,举个例子:
// Enum type with data and behavior - Pages 149-150
package org.effectivejava.examples.chapter06.item30;
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
}
}
// Takes earth-weight and prints table of weights on all planets - Page 150
package org.effectivejava.examples.chapter06.item30;
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));
}
}
所有枚举类型都有一个静态的values方法,按照声明顺序返回它的值数组,toString方法返回每个枚举值的声明名称,如果不满意该toString方法,可以覆盖.
将不同的行为与每个枚举常量关联起来:在枚举类型中生命一个抽象方法(如下apply方法),并在特定于**常量的类主体**(constant-specific class body)中,用具体的方法覆盖每个常量的抽象方法。这种方法被称作**特定于常量的方法实现**(constant-specific method implementation):
// Enum type with constant-specific class bodies and data - Page 153
package org.effectivejava.examples.chapter06.item30;
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);
//因为枚举类型会自动产生一个ValueOf(String)方法,将常量的名字转变成常量本身,如果覆盖了toString,就要考虑加一个fromString方法
// Implementing a fromString method on an enum type - Page 154
private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();
static { // Initialize map from constant name to enum constant
for (Operation op : values())
stringToEnum.put(op.toString(), op);
}
// Returns Operation for string, or null if string is invalid
public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
}
// Test program to perform all operations on given operands
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));
}
}
要注意我们不能使每个常量都从自己的构造器将自身放入到map中,这回导致编译时错误。这是好事,因为如果这是合法的,就会抛出NullPointerException。除了编译时常量域,枚举构造器不可以访问枚举静态域。这一编译时限制是有必要的,因为构造器运行时,这些静态域还没有被初始化。
特定于常量的方法实现不足在于使得在枚举常量中共享代码更加困难。比如算薪资,枚举常量是星期一到星期天,周一到周五是一种算法,双休日是另一种算法,用switch的方法容易在增加常量是遗漏,特定于常量的方法实现则重复代码太多。可以用策略枚举:
// The strategy enum pattern
package org.effectivejava.examples.chapter06.item30;
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);
}
// The strategy enum type
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);
}
}
}
第31条 用实例域代替序数
// Enum with integer data stored in an instance field
package org.effectivejava.examples.chapter06.item31;
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal()+1;
}
}
这种就是用序数的做法,枚举类型会自动生成一个ordinal()方法,返回常量所处的位置索引。
但是不要用这种方法:1.以后重排序,会破坏numberOfMusicians() 2.这种方法得出的常量对应的int值一定是唯一的,但有时我们要不唯一。
解决方法就是下面:
// Enum with integer data stored in an instance field
package org.effectivejava.examples.chapter06.item31;
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), OCTET(8),
DOUBLE_QUARTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}
第32条 用EnumSet代替位域
什么叫位域呢,就是用int枚举模式,将2的不同倍数赋予每个常量,这种表示法让你用OR位运算将几个常量合并到一个集合中(int值),称作位域(bit field)。这种方法的缺点在于位域以数字形式打印时,翻译位域很困难。以下是替代方案:
// EnumSet - a modern replacement for bit fields - Page 160
package org.effectivejava.examples.chapter06.item32;
import java.util.EnumSet;
import java.util.Set;
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));
}
}
第33条 用EnumMap代替序数索引
// Using an EnumMap to associate data with an enum - Pages 161-162
package org.effectivejava.examples.chapter06.item33;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
// Simplistic class representing a culinary herb - Page 161
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;
this.type = type;
}
@Override
public String toString() {
return name;
}
public static void main(String[] args) {
Herb[] garden = { new Herb("Basil", Type.ANNUAL),
new Herb("Carroway", Type.BIENNIAL),
new Herb("Dill", Type.ANNUAL),
new Herb("Lavendar", Type.PERENNIAL),
new Herb("Parsley", Type.BIENNIAL),
new Herb("Rosemary", Type.PERENNIAL) };
// Using an EnumMap to associate data with an enum - Page 162
//他改进的是使用数组+ordinal()作为索引的方法,那种方法的缺点:
//1.数组与泛型不兼容
//2.因为数组不知道其索引代表这什么,所以要手工label这些索引的输出
//3.访问数组时你还要确保index正确
Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(
Herb.Type.class);
for (Herb.Type t : Herb.Type.values())
herbsByType.put(t, new HashSet<Herb>());
for (Herb h : garden)
herbsByType.get(h.type).add(h);
System.out.println(herbsByType);
}
}
// Using a nested EnumMap to associate data with enum pairs - Pags 163-164
package org.effectivejava.examples.chapter06.item33;
import java.util.EnumMap;
import java.util.Map;
public enum Phase {
SOLID, LIQUID, GAS;//固,液,气
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID)/*凝固*/, BOIL(LIQUID, GAS)/*气化*/, CONDENSE(
GAS, LIQUID), SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase src;
private final Phase dst;
Transition(Phase src, Phase dst) {
this.src = src;
this.dst = dst;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase, Transition>> m = new EnumMap<Phase, Map<Phase, Transition>>(
Phase.class);
static {
for (Phase p : Phase.values())
m.put(p, new EnumMap<Phase, Transition>(Phase.class));
for (Transition trans : Transition.values())
m.get(trans.src).put(trans.dst, trans);
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
// Simple demo program - prints a sloppy table
public static void main(String[] args) {
for (Phase src : Phase.values())
for (Phase dst : Phase.values())
if (src != dst)
System.out.printf("%s to %s : %s %n", src, dst,
Transition.from(src, dst));
}
}
第33条 用接口模拟可伸缩的枚举
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对他进行模拟:
// Emulated extensible enum using an interface - Page 165
package org.effectivejava.examples.chapter06.item34;
public interface Operation {
double apply(double x, double y);
}
// Emulated extensible enum using an interface - Basic implementation - Page 165
package org.effectivejava.examples.chapter06.item34;
public enum BasicOperation implements Operation {
PLUS("+") {
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;
}
}
// Emulated extension enum - Page 166-167
package org.effectivejava.examples.chapter06.item34;
import java.util.Arrays;
import java.util.Collection;
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;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
// Test class to exercise all operations in "extension enum" - Page 167
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
System.out.println(); // Print a blank line between tests
test2(Arrays.asList(ExtendedOperation.values()), x, y);
}
//下面是两种使用方式:
// test parameter is a bounded type token (Item 29)
private 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));
}
// test parameter is a bounded wildcard type (Item 28)
private static void test2(Collection<? extends Operation> opSet, double x,
double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
第35条 注解优于命名模式
什么叫命名模式,就是通过对类或方法取一些特殊名字来让某种工具或框架来处理他,比如JUnit测试框架要求他要测试的方法必须以test开头。
注解:
1.元注解:注解注解的注解(注解类型通过Retention和Target等注解进行声明)
2.标记注解,如:
/**
* Indicates that the annotated method is a test method. Use only on
* parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
将它注解在方法前,当拼错的是时候不会通过编译(相对来讲命名模式如果拼错还可以通过编译,无法发现)
下面是用注解开发的一个测试框架,从中可以学习到注解的一般用法:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("INVALID @Test: " + m);
}
}
// Array ExceptionTest processing code - Page 174
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Exception>[] excTypes = m.getAnnotation(
ExceptionTest.class).value();
int oldPassed = passed;
for (Class<? extends Exception> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
使用方法1:
public class Sample {
@Test
public static void m1() {
} // Test should pass
public static void m2() {
}
@Test
public static void m3() { // Test Should fail
throw new RuntimeException("Boom");
}
public static void m4() {
}
@Test
public void m5() {
} // INVALID USE: nonstatic method
public static void m6() {
}
@Test
public static void m7() { // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() {
}
}
使用方法2:
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {
} // Should fail (no exception)
// Code containing an annotation with an array parameter - Page 174
@ExceptionTest({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() {
List<String> list = new ArrayList<String>();
// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
}
第36条 坚持使用Override注解
理由:
1.防止把覆盖和重载弄混了(Override会提醒你的)
2.IDE的代码检验
第37条 用标记接口定义类型
标记接口(marker interface)是没有包含方法声明的接口,而只是指明(或者"标明")一个类实现了具有某种属性的接口。例如通过实现Serializable接口,类表明它的实例可以被写到ObjectOutputStream(或者“被序列化”)。
第35条提到过一种标记注解,有一种说法是标记注解使得标记接口过时了,这是错误的。
标记接口相对标记注解的优点有二
首先,标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型,这个类型允许你在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误。
但是,就Serializable标记接口而言,如果他的参数没有实现该接口,ObjectOutputStream.write(Object)方法将会失败,不过可能是在运行时失败,那怎么前面说会在编译时通不过呢,原因在于,该方法接收Object,如果接收Serializable类型,那传一个没有实现Serializable接口的类就通不过编译了。
Set接口就是例子:
public interface Set<E> extends Collection<E>
书上说为什么他是这种有限制的标记接口是因为他只适用于Collection子类型,这样有点绕,其实可以这样想:一个类实现了Set接口就一定实现了Collection接口(就是说是Collection子类型),这样就跟书上那句话等价了。至于为什么他里面有方法声明却还被叫做标记接口,可能是因为他里面的方法全部都是继承自Collection接口。但是书上也说了一般不把它看作标记接口,因为他改进了几个Collection方法的契约,包括add,equals和hashCode。