【effective java讀書筆記】枚舉(一)
前言:
首先講講為什麼我寫這篇文章,當然第一點就是effective java的讀書筆記。不動筆墨不讀書,軟體行業也是這樣子。而且總要有個東西記錄時常翻看,才能更熟悉的運用。再加上看自己寫的,比看别人寫的容易了解回憶。就算忘的差不多,回過頭一看就明白怎麼回事了。希望我能堅持看完并寫完這一系列的讀書筆記吧。如果有人看的話,并且看到錯誤了,也希望指出。
一、用enum代替int常量
首先說一下為什麼要用枚舉代替int常量。第一個原因:列印日志的時候,int常量列印出來是數字?估計自己寫的代碼,從日志中也很難記起來并知道1代表是什麼蘋果還是橘子~當然,很多人會說,用字元串String常量可以解決這個問題,正如書中說的,String的比較比起int來性能差了不是一星半點。第二個原因:int常量編譯之後,就成了數字1,2,3等,那麼同一個類當中如果我蘋果汁和橘子汁都是1,那麼是否可以互相比較呢?沒有一絲防備的無限制比較不安全。
舉個例子:
反例代碼一:
public class test {
public static final int APPLE_JURCE = 0;
public static final int APPLE_TREE = 1;
public static final int ORANGE_JURCE = 0;
public static final int ORANGE_TREE = 1;
@Test
public void test() {
if(APPLE_JURCE==ORANGE_JURCE){
System.out.println("相同果汁");
}else{
System.out.println("不相同果汁");
}
System.out.println(APPLE_JURCE+"----"+ORANGE_JURCE);
}
}
執行結果:(此處即我上面說的第二個原因)
相同果汁
0----0
二、枚舉中添加方法和屬性:
然後看看枚舉能做什麼?添加方法和屬性。
添加方法和屬性做什麼?看下面這個例子。
代碼一:
public enum Planet {
MERCURY(3.0, 2.0),
VENUS(1.0, 2.0),
EARTH(4.0, 3.0);
private final double mass;
private final double radius;
private final double surfaceGravity;
private Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
this.surfaceGravity = mass * radius;
}
public double getMass() {
return mass;
}
public double getRadius() {
return radius;
}
public double getSurfaceGravity() {
return surfaceGravity;
}
public double surfaceWeight(){
return mass*surfaceGravity;
}
}
代碼二:
public class WeightTable {
public static void main(String[] args) {
for (Planet p:Planet.values()) {
System.out.printf("Weight on %s is %f%n",p,p.surfaceWeight());
}
}
}
執行結果:
Weight on MERCURY is 18.000000
Weight on VENUS is 2.000000
Weight on EARTH is 48.000000
此處可通過枚舉對象的執行個體,獲得火星,地球等星球的表面積等屬性。PS:當然,這些對象屬性是一經定義,不可變的常量。
三、枚舉運用的一個恰當方式:舉例算數電腦
代碼三:(基礎版本)此版本存在一個問題,維護起來比較麻煩。比如我添加了一個OTHER枚舉。忘了實作具體實作,編譯沒出錯,但是運作錯誤了。這事情就比較大了。
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE,
// 額外添加的,未做具體的操作,會有運作時異常
OTHER;
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("Unknown op:" + this);
}
}
代碼四:
@Test
public void test2(){
System.out.println(Operation.OTHER.apply(3.0, 2.0));
}
運作結果:(運作時錯誤)
java.lang.AssertionError: Unknown op:OTHER
代碼五:(改進版本)可維護性強的安全版本。
public enum Operation2 {
PLUS {
@Override
double apply(double x, double y) {return x+y;}
},
MINUS {
@Override
double apply(double x, double y) {return x-y;}
},
TIMES {
@Override
double apply(double x, double y) {return x*y;}
},
DIVIDE {
@Override
double apply(double x, double y) {return x/y;}
},
OTHER {
@Override
double apply(double x, double y) {return x+y+1;}
};
//抽象方法,每個具體的執行個體都必須實作,如果不實作,編譯錯誤
abstract double apply(double x, double y);
}
代碼說明:此處添加了一個抽象的apply方法,而每個枚舉可以了解為是一個具體的對象。是以必須實作自身的抽象方法。編譯器會限制開發人員編寫。
PS:這種代碼寫法适合這種固定的個數的參數,但是操作不同的方法的整合。可以在寫代碼的時候試試。
代碼六:
@Test
public void test3(){
System.out.println(Operation2.OTHER.apply(3.0, 2.0));
}
執行結果:
6.0
看看一個新增一個構造方法。構造方法的作用是什麼呢。可以傳遞參數進去(固定的,不對外)。但是這個參數當然是必須在枚舉裡面寫的。比如下面這個代碼,加減乘除算法符号,可以通過這種方式顯示。
代碼七:
public enum Operation3 {
PLUS("+") {
@Override
double apply(double x, double y) {return x+y;}
},
MINUS("-") {
@Override
double apply(double x, double y) {return x-y;}
},
TIMES("*") {
@Override
double apply(double x, double y) {return x*y;}
},
DIVIDE("-") {
@Override
double apply(double x, double y) {return x/y;}
},
OTHER("?") {
@Override
double apply(double x, double y) {return x+y+1;}
};
//抽象方法,每個具體的執行個體都必須實作,如果不實作,編譯錯誤
abstract double apply(double x, double y);
private final String simbol;
private Operation3(String simbol) {
this.simbol = simbol;
}
@Override
public String toString() {
return this.simbol;
}
}
代碼八:
@Test
public void test4(){
double x = 3.0;
double y = 2.0;
String s = Operation3.PLUS.toString();
System.out.printf("%f%s%f=%f",x,s,y,Operation3.PLUS.apply(x, y));
}
執行結果:ps:這樣的日志是不是很漂亮?優雅!
3.000000+2.000000=5.000000
四、不要用枚舉的ordinal()方法
先看一個例子:
代碼九:(反例代碼)
public enum Ensemble {
SOLO,
DUET,
TRIO,
QUARTET;
public int numberOfMusic(){
return ordinal()+1;
}
}
代碼十:(反例代碼)
@Test
public void test5(){
for (Ensemble e:Ensemble.values()) {
System.out.println(e.numberOfMusic());
}
}
運作結果:
1
2
3
4
PS:似乎很優雅,但是不利于維護。例如我颠倒一下順序,那麼我代碼需要處處修改。腦補一下SOLO和DUET換了之後。如果我使用這個枚舉排序方法,我對應需要修改每一個操作的順序。例如我運作結果1代表走路,2代表吃飯。我改了之後,對應的1,2的操作也需要同時修改。就是這麼個意思。
代碼十一:(改進版本)
public enum Ensemble2 {
SOLO(2),
DUET(1),
TRIO(3),
QUARTET(4);
private final int size;
private Ensemble2(int size) {
this.size = size;
}
public int numberOfMusic(){
return size;
}
}
代碼十二:
@Test
public void test6(){
for (Ensemble2 e:Ensemble2.values()) {
System.out.println(e.toString()+e.numberOfMusic());
}
}
運作結果:PS:這樣子就不用怕修改對應的操作了,因為我SOLO就是SOLO做的事,改了排序,也隻是需要修改枚舉内的構造參數,而不需要改變對應的操作。
SOLO2
DUET1
TRIO3
QUARTET4
五、用EnumSet代替位域
看看位域的例子吧。
代碼十三:(反例代碼)
public class Text {
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 1;
public static final int STYLE_UNDERLINE = 1 << 2;
public static final int STYLE_STRIKETHROUGH = 1 << 3;
public void applyStyles(int styles){
}
}
代碼十四:(反例代碼)
@Test
public void test7() {
Text text = new Text();
text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC);
System.out.println(Text.STYLE_BOLD | Text.STYLE_ITALIC);
}
運作結果:
3
PS:其實這麼操作是完全沒有問題的。其實原因很簡單,還是和最開始說的問題一樣,列印日志不便,因為編譯後是數字。還有就是EnumSet集合的優點,可以周遊。上面這個位域是完全無法做到的。
代碼十五:(改進版本)
public class Text2 {
public enum Style {
BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
}
public void applyStyles(Set<Style> styles) {
for (Style s:styles) {
System.out.println(s.toString());
}
}
}
代碼十六:(改進版本)
@Test
public void test8() {
Text2 text = new Text2();
Set<Style> sets = EnumSet.of(Text2.Style.BOLD,Text2.Style.ITALIC);
text.applyStyles(sets);
System.out.println("-----------");
System.out.println(EnumSet.of(Text2.Style.BOLD,Text2.Style.ITALIC));
}
運作結果:當然主要是集合用起來比較友善呀。性能相當,更友善。
BOLD
ITALIC
-----------
[BOLD, ITALIC]
六、用EnumMap代替序數索引
看看序數索引的例子:
代碼十七:(反例)(其中Herb類見代碼十九)
@Test
public void test9() {
Herb[] garden = {new Herb("bobo", Type.ANNUAL),new Herb("hehe", Type.BIENNIAL),new Herb("gangan", Type.PERENIAL),new Herb("xionxion", Type.ANNUAL)} ;
//建立一個集合數組(數組大小為Type的長度)
Set<Herb>[] herbs = new Set[Herb.Type.values().length];
//集合數組初始化
for (int i = 0; i < herbs.length; i++) {
herbs[i] = new HashSet<>();
}
//集合數組中hashset添加資料
for (Herb h :garden) {
herbs[h.type.ordinal()].add(h);
}
for (int i = 0; i < herbs.length; i++) {
System.out.printf("%s:%s%n",Herb.Type.values()[i],herbs[i]);
}
}
代碼十八:(反例)
@Test
public void test9() {
Herb[] garden = {new Herb("bobo", Type.ANNUAL),new Herb("hehe", Type.BIENNIAL),new Herb("gangan", Type.PERENIAL),new Herb("xionxion", Type.ANNUAL)} ;
Set<Herb>[] herbs = new Set[Herb.Type.values().length];
for (int i = 0; i < herbs.length; i++) {
herbs[i] = new HashSet<>();
}
for (Herb h :garden) {
herbs[h.type.ordinal()].add(h);
}
for (int i = 0; i < herbs.length; i++) {
System.out.printf("%s:%s%n",Herb.Type.values()[i],herbs[i]);
}
}
運作結果:原因:ordinal方法原本就不推薦使用,因為通過位置去判斷,修改維護成本高。加上herbs數組本身不具備編譯檢測功能,此處可看我上篇文章泛型限制。缺點也是數組不如泛型的缺點。結果上倒是無問題。
ANNUAL:[bobo, xionxion]
PERENIAL:[gangan]
BIENNIAL:[hehe]
代碼十九:(改進版本)
public class Herb {
public enum Type{ANNUAL,PERENIAL,BIENNIAL}
private final String name;
public final Type type;
public Herb(String name,Type type) {
this.name = name;
this.type = type;
}
@Override
public String toString() {
return name;
}
}
代碼二十:(改進版本)
@Test
public void test10() {
Herb[] garden = {new Herb("bobo", Type.ANNUAL),new Herb("hehe", Type.BIENNIAL),new Herb("gangan", Type.PERENIAL),new Herb("xionxion", Type.ANNUAL)} ;
Map<Herb.Type, Set<Herb>> herbs = new EnumMap<>(Herb.Type.class);
for (Herb.Type t:Herb.Type.values()) {
herbs.put(t, new HashSet<>());
}
for (Herb h:garden) {
herbs.get(h.type).add(h);
}
System.out.println(herbs);
}
運作結果:PS:此處限制是通過Type判斷,維護成本降低。加上通過集合,安全性有保證。
{ANNUAL=[xionxion, bobo], PERENIAL=[gangan], BIENNIAL=[hehe]}
七、用接口模拟可伸縮的枚舉
之前提到的算數方法,如今可擴充性增加了,主要是為了對外提供API時對其他人能有一定限制。
之前提到的Operation操作代碼:(改進前)
public enum Operation{
PLUS("+") {
@Override
double apply(double x, double y) {return x+y;}
},
MINUS("-") {
@Override
double apply(double x, double y) {return x-y;}
},
TIMES("*") {
@Override
double apply(double x, double y) {return x*y;}
},
DIVIDE("-") {
@Override
double apply(double x, double y) {return x/y;}
};
//抽象方法,每個具體的執行個體都必須實作,如果不實作,編譯錯誤
abstract double apply(double x, double y);
private final String simbol;
private Operation(String simbol) {
this.simbol = simbol;
}
@Override
public String toString() {
return this.simbol;
}
}
代碼一:(改進後)
public interface Operation {
double apply(double x, double y);
}
代碼二:(改進後)
public enum BaseOperation implements Operation {
PLUS("+") {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
@Override
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("-") {
@Override
public double apply(double x, double y) {
return x / y;
}
};
private final String simbol;
private BaseOperation(String simbol) {
this.simbol = simbol;
}
@Override
public String toString() {
return this.simbol;
}
}
代碼三: (改進後)PS:其他人用起來也必須符合我的限制apply。基本上就沒問題了。
public enum ExtendedOperation implements Operation{
EXP("^"){
@Override
public double apply(double x, double y) {
return Math.pow(x, y);
}
};
private final String symbol;
private ExtendedOperation(String symbol) {
this.symbol =symbol;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return symbol;
}
}
代碼四:
public class JunitTest {
@Test
public void test() {
double x = 2.0;
double y = 3.0;
func(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T>&Operation> void func(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));
}
}
}
運作結果:
2.000000^3.000000=8.000000
總結:
寫這個讀書筆記已經寫了三篇,發現其實effective java這本書是站在一個很高的角度去寫的。
首先,代碼安全性。不僅僅是實作,可以說是更是處處謹慎。例如,多用泛型少用數組,數組不好麼?不,數組是基礎。數組非常好。但是為什麼不用呢?因為泛型有限制,安全。編譯時能發現錯誤,而數組要運作時發現錯誤。
再則,多用枚舉少用常量标志位,為什麼多用枚舉呢?我們寫代碼,其他人閱讀起來使用的時候,例如列印枚舉日志和列印常量日志,自己或者别人閱讀起來的感覺不可同日而語。還有,枚舉是面向對象的,那麼可以使用一些面向對象的特性,例如周遊,泛型限制,常量就不可以。
最後,就是性能,我所了解的一個程式員而不是一個碼農的自我修養:相同性能條件下,用一種更安全更具有擴充性可讀性的代碼,才是一個優雅的代碼。
感謝閱讀~