Effective java筆記-第六章 列舉和註解
阿新 • • 發佈:2019-02-15
列舉和註解
第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。