天天看點

【effective java讀書筆記】枚舉(一)一、用enum代替int常量二、枚舉中添加方法和屬性:三、枚舉運用的一個恰當方式:舉例算數電腦四、不要用枚舉的ordinal()方法五、用EnumSet代替位域六、用EnumMap代替序數索引

【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這本書是站在一個很高的角度去寫的。

首先,代碼安全性。不僅僅是實作,可以說是更是處處謹慎。例如,多用泛型少用數組,數組不好麼?不,數組是基礎。數組非常好。但是為什麼不用呢?因為泛型有限制,安全。編譯時能發現錯誤,而數組要運作時發現錯誤。

再則,多用枚舉少用常量标志位,為什麼多用枚舉呢?我們寫代碼,其他人閱讀起來使用的時候,例如列印枚舉日志和列印常量日志,自己或者别人閱讀起來的感覺不可同日而語。還有,枚舉是面向對象的,那麼可以使用一些面向對象的特性,例如周遊,泛型限制,常量就不可以。

最後,就是性能,我所了解的一個程式員而不是一個碼農的自我修養:相同性能條件下,用一種更安全更具有擴充性可讀性的代碼,才是一個優雅的代碼。

感謝閱讀~