天天看点

【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这本书是站在一个很高的角度去写的。

首先,代码安全性。不仅仅是实现,可以说是更是处处谨慎。例如,多用泛型少用数组,数组不好么?不,数组是基础。数组非常好。但是为什么不用呢?因为泛型有约束,安全。编译时能发现错误,而数组要运行时发现错误。

再则,多用枚举少用常量标志位,为什么多用枚举呢?我们写代码,其他人阅读起来使用的时候,例如打印枚举日志和打印常量日志,自己或者别人阅读起来的感觉不可同日而语。还有,枚举是面向对象的,那么可以使用一些面向对象的特性,例如遍历,泛型约束,常量就不可以。

最后,就是性能,我所理解的一个程序员而不是一个码农的自我修养:相同性能条件下,用一种更安全更具有扩展性可读性的代码,才是一个优雅的代码。

感谢阅读~