《Effective Java》第六章:枚举和注解
第30条:用枚举代替int常量
在编程语言中还没有引入枚举类型之前,表示枚举类型的常用模式就是声明一组具名的int
常量1
2
3public static final int APPLE_FUJI =0;
public static final int APPLE_PIPPIN =1;
public static final int APPLE_GRANNY_SMITH =2;
但是它又诸多的缺点:
- 在类型安全性和使用方便性方面没有任何帮助。
- 如果关联的
int
放生了变化,客户端必须得重新编译。 - 打印调试的时候只能打印数字
上述int
常量如何使用枚举类实现1
public enum APPLE{FUJI,PIPPIN,GRANNY_SMITH};
除了编码简单之外,枚举的特点还有:
- 枚举是类型安全的(所有可以作为单例的泛型化来使用)。
- 可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码。
- 可以通过
toString
方法来打印可视的字符串。
枚举类型还可以添加任意的方法和域,并实现任意的接口。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35interface Calc{
double apply(double x,double y);
}
/**
* 具体继承了接口 并重新实现了
* @author yanyl
*
*/
public enum Operation implements Calc{
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("/"){
@Override
public double apply(double x,double y){return x/y;}
};
private final String symbol;
Operation(String symbol)
{
this.symbol=symbol;
}
@Override
public String toString()
{
return this.symbol;
}
}
上述枚举可以这么使用1
2Calc calc=Operation.PLUS;
System.out.println(calc.apply(1, 2));
是不是超级方便~
总之而言,与
int
常量对比,枚举类型的优势是不言而喻的,(当然,当初枚举的出现就是为了解决int
常量问题的^_^,所以大家还是多使用枚举吧)
第31条:用实例域代替序数
什么是枚举的序数,你知道吗?1
2
3
4
5
6
7
8
9public enum Ensemble{
SOLO,DUET,TRIO,QUARTET,QUITTET,
SEXTET,SEPTET,OCTET,NONET,DECTET;
public int numberOfMusicians()
{
return ordinal()+1;
}
}
每个枚举都有一个ordinal()
方法,用于返回当前枚举值的序数,这个见都没见过的方法用起来看似很方便,但是如果你要修改常量的顺序,那么维护起来就是一场噩梦。
第30条说过了,枚举里面可以有字段,所以这条就是推荐使用实例域代替序数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public enum Ensemble{
SOLO(1),DUET(2),TRIO(3),QUARTET(4),QUITTET(5),
SEXTET(6),SEPTET(7),OCTET(8),NONET(9),DECTET(10);
/**
* 使用自定义的一个实例域
*/
private final int numberOfMusicians;
Ensemble(int numberOfMusicians)
{
this.numberOfMusicians=numberOfMusicians;
}
public int numberOfMusicians()
{
return numberOfMusicians+1;
}
}
这个方法实现的修过虽好,然是感觉写起来好麻烦。。。维护起来会很简单吗???
第32条:用EnumSet代替位域
这个小节讲的其实就是和位图法相关。
如果一个枚举类型的元素主要用于在集合中,一般就使用int
枚举类型,将2的不同倍数赋予每个常量。1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Text
{
public static final int STYLE_BOLD =1<<0;//1
public static final int STYLE_ITALIC =1<<1;//2
public static final int STYLE_UNDERLINE =1<<2;//4
public static final int STYLE_STRIKETHROUGH =1<<3;//8
private int styles=-1;
public void applyStyles(int styles)
{
this.styles=styles;//这里其实就是用位图法来保存,取值要用位移来取 int这里只能存32个值
}
}
这种表示方法让你用OR位运算将几个常量合并到一个集合中,称作位域。
然后你可能会使用这或操作种方法来保持状态:1
new Text().applyStyles(Text.STYLE_BOLD|Text.STYLE_ITALIC);
这种做法一个是打印是来比较难看懂,还有编译表示所有元素时也没好好的办法。
还好有EnumSet
的出现,它实现了Set
接口,如果底层枚举类型少于64个,那么这个EnumSet
就是用一个long
类型来表示,所以性能上也是很高的,上面这个例子用EnumSet
来实现就是:1
2
3
4
5
6
7
8
9
10static class Text
{
public enum Style{BOLD,ITALIC,UNDERLINE,STRIKETHROUGH}
private EnumSet<Style> styles=null;
public void applyStyles(EnumSet<Style> styles)
{
this.styles=styles;
}
}
调用方法就用1
new Text().applyStyles(EnumSet.of(Text.Style.BOLD, Text.Style.ITALIC));
这是调用之后你打印styles
这个变量可以看到
[BOLD, ITALIC]
这种打印显示的总比单纯的数字要美好的多。
总而言之,正式因为枚举类型要用集合,所以没有理由用位域来表示它。
其实我觉得用EnumSet
应该没有位域快啊,估计高手还是会去用位域的把
第33条:用EnumMap代替序数索引
1 | public class Herb{ |
当你需要将上面类的实例根据Type
枚举类型来存储起来是,它推荐这么做:1
Map<Herb.Type,Set<Herb>> enumMap=new EnumMap<Herb.Type,Set<Herb>>(Herb.Type.class);
但是我觉得何必这么麻烦,用下面的写得表示也很方便嘛,也还不需要记那么多东西1
Map<Integer,Set<Herb>> map=new HashMap<Integer,Set<Herb>>();
每次使用Type.ordinal()
的索引即可。
第34条:用接口模拟可伸缩的枚举
前面几条讲了使用Enum
的便利性,安全性,但是它的可伸缩性比较弱,因为枚举无法再继承类或者枚举,比较幸运地时枚举支持接口的实现。
例如第30条中的四则运算我需要扩展其他的运算:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public enum ExtendOperation implements Calc{
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;
ExtendOperation(String symbol)
{
this.symbol=symbol;
}
@Override
public String toString()
{
return this.symbol;
}
}
然后你就可以这么使用1
2
3
4
5
6
7
8
9
10public static void main(String[] args)
{
System.out.println(test(Operation.PLUS,2,3));//调用加法
System.out.println(test(ExtendOperation.EXP,2,3));//调用求幂
}
public static <T extends Enum<T> & Calc> double test(T opt,double x,double y)
{
return opt.apply(x, y);
}
最终会输出
5.0
8.0
总而言之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。
第35条:注解优于命名模式
以JUnit
为例,命名模式的3大缺陷:
- 文字拼写错误会导致失败,并且没有任何提示
- 无法确保他们只用于相应的程序元素上
- 它们都没有提供将参数值与程序元素关联起来的好方法
你这通过这种方式简单的来创建一个自己的注解1
2
3
4
5
6
7import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
}
MyTest
注解类型的声明就是它自身的Retention
和Targer
注解进行了注解,这些注解类型称为元注解,其中:
@Retention(RetentionPolicy.RUNTIME)
表示MyTest
注解应该在运行时保留@Target(ElementType.METHOD)
表示该注解仅用于方法上
我们有了这个注解之后可以这么使用它1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class HelloWorld{
@MyTest
public void test1(){}
public void test2(){}
@MyTest
public void test3(){}
@MyTest
public void test4() throws Exception
{
throw new Exception("i am error test4");
}
}
那我们如何调用这个注解呢?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws Exception
{
int tests=0;
int passed=0;
Class testClass=Class.forName("yyl.java.study.test.HelloWorld");
Object testClassInstance=testClass.newInstance();
for(Method m:testClass.getDeclaredMethods())
{
if(m.isAnnotationPresent(MyTest.class))
{
tests++;
try
{
m.invoke(testClassInstance,null);//对实例的方法进行调用
passed++;
System.out.println("PASSED:"+m.getName());
}catch(Exception e)
{
System.out.println("FAILD:"+m.getName());
}
}
}
System.out.println(String.format("PASSED:%s,FAILED:%s",passed,tests-passed));
}
}
启动这个RunTests
会得到
PASSED:test3
FAILD:test4
PASSED:test1
PASSED:2,FAILED:1
一个简易版的测试框架就完成了,该书中的其他内容是讲你如何完善这个自制的测试框架,比如添加异常的注解,最终判断失败的机制等,我个人觉得只要把上述的简易框架看懂其他都不是问题。^_^
既然有了注解,那么完全没有理由再使用命名模式了。
第36条:坚持使用Override注解
估计Override
注解是在Java
中最常见也是常用的一种注解了吧,它标志方法被重写,但是在java
中你重写方法时不加Override
也是可以正常运行的,所以我相信很多人会懒掉这个注解,关于这个注解书中有下面几个推荐理由:
- 你再重写得定义上出错时如果有
Override
,编译器就会帮你检查错误 - IDE具有自动检查功能,当你没有用
Override
但是却覆盖了超类的方法时,IDE就是产生一条警告,提醒确认。 - 还可以让程序员清晰地看到这条方式是否是重写方法,在看代码的找起来比较方便啊(我自己觉得的)
总而言之,如果在你想要的每个方法声明中使用Override
注解来覆盖超类声明,编译器就可以替你防止大量的错误(在继承抽象类时可以不加。。但是我觉得还是加的好)
第37条:用标记接口定义类型
标记接口是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口,比如Serializable
关于标记接口与标记注解的争论:
标记接口的优势:
- 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型
- 标记接口可以更加精确地进行锁定
标记注解的优势:
- 它可以通过默认的方式添加一个或者多个注解类型的元素
- 标记注解作为变成元素之一的框架中同样具有一致性。
总而言之,标记接口和标记注解都各有用处,如果想要定义一个任何新的方法都不会与之关联的类型,标记接口是最好的选择。