【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执行结果:ps:这样的日志是不是很漂亮?优雅!
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));
}
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运行结果:PS:这样子就不用怕修改对应的操作了,因为我SOLO就是SOLO做的事,改了排序,也只是需要修改枚举内的构造参数,而不需要改变对应的操作。
public void test6(){
for (Ensemble2 e:Ensemble2.values()) {
System.out.println(e.toString()+e.numberOfMusic());
}
}
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运行结果:原因:ordinal方法原本就不推荐使用,因为通过位置去判断,修改维护成本高。加上herbs数组本身不具备编译检测功能,此处可看我上篇文章泛型约束。缺点也是数组不如泛型的缺点。结果上倒是无问题。
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]);
}
}
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运行结果:PS:此处约束是通过Type判断,维护成本降低。加上通过集合,安全性有保证。
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);
}
{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 {代码三: (改进后)PS:其他人用起来也必须符合我的约束apply。基本上就没问题了。
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;
}
}
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这本书是站在一个很高的角度去写的。
首先,代码安全性。不仅仅是实现,可以说是更是处处谨慎。例如,多用泛型少用数组,数组不好么?不,数组是基础。数组非常好。但是为什么不用呢?因为泛型有约束,安全。编译时能发现错误,而数组要运行时发现错误。
再则,多用枚举少用常量标志位,为什么多用枚举呢?我们写代码,其他人阅读起来使用的时候,例如打印枚举日志和打印常量日志,自己或者别人阅读起来的感觉不可同日而语。还有,枚举是面向对象的,那么可以使用一些面向对象的特性,例如遍历,泛型约束,常量就不可以。
最后,就是性能,我所理解的一个程序员而不是一个码农的自我修养:相同性能条件下,用一种更安全更具有扩展性可读性的代码,才是一个优雅的代码。
感谢阅读~