以下为我在《Effective Java》中留下的读书笔记,对一些重要的知识点提取出了摘要.
30、用enum代替int常量
补充:编译时常量
int枚举模式的缺点:
1、类型安全性(int枚举模式与普通的int类型并没有实质性的区别).
2、使用脆弱.因为它是编译时常量,如果与枚举常量关联的int发生了变化,客户端就必须重新编译,如果没有重新编译,程序还可以运行,但是它们的行为已经不确定.
3、int枚举常量无法打印出具有详细意义的字符串.
String枚举模式的:
1、存在性能问题,因为依赖于字符串的比较.
2、字符串常量硬编码会存在隐含的错误问题.
枚举类型避免了这些缺点,并且提供许多额外的好处:
1、编译时的类型安全
2、单例的泛型化
3、有自己的命名空间
4、导出常量的域在枚举类型和它的客户端之间提供了一个隔离层(常量值并没有被编译到客户端代码中)
5、还可以添加任意的方法和域,并实现任意的接口
为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器
枚举类有一个静态的values方法,按照声明顺序返回它的值数组.
for(Planet p : Planet.values()){
}
如果一个枚举具有普遍适用性,它就应该成为一个顶层类,如果它只是被用在一个特定的顶层类中,它就应该成为该顶层类的一个成员类.例如java.math.RoundingMode枚举表示十进制小数的舍入.(顶层类)
在枚举类中添加方法时,
每个枚举类型的子项需要关联不同的方法,特定于常量的方法实现如下:
public enum Operation {
PLUS{double apply(double x, double y){return x + y;} } ,
MINUS{double apply(double x, double y){return x - y;} };
abstract double apply(double x, double y);
}
在枚举类型中声明一个抽象的方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象方法
如此,在给Operation添加新常量时,就不会忘记提供apply方法.
可以与特定于常量的数据结合:
public enum Operation {
PLUS("+") ,
MINUS("-");
private final String symbol;
private Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
枚举类型有一个自动产生的valueOf(String) 方法 ,它将常量的名字转变成常量本身.
如果覆盖了toString,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举类型,如下面片段代码:
private static final Map<String, Operation> stringToEnum = new HashMap<>();
static{
for(Operation op : values())
stringToEnum.put(op.toString(), op);
}
public static Operation fromString(String symbol){
return stringToEnum.get(symbol);
}
此时注意,枚举构造器不可以访问枚举的静态域,除了编译时常量域之外.
当枚举受控时,用策略枚举代替switch语句更好.
switch语句适合对于不受控制的枚举类型的外部.
什么时候用枚举:
1、天然的枚举类型.例如,行星、一周的天数以及棋子的数目等等;
2、在编译时就知道其所有可能值得其他集合.例如菜单的选项、操作代码以及命令行标记等.
补充: switch用法 case SATURDAY: case SUNDAY: ..... 多个case可以写在一起
31、用实例域代替序数
所有的枚举都有一个ordinal()方法.
返回每个枚举常量在类型中的数字位置,它是设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构的.
永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中.
32、用EnumSet代替位域
补充: << 移位运算 1<<0 = 1; 1<<1 = 2; 1<<2 = 4; 1<<3 = 8;
位域,这种表示法让你用OR位运算将几个常量合并到一个集合中.
java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合.
如果底层的枚举类型有64个或者更少的元素,整个EnumSet用单个long表示,因此它的性能比得上位域的性能.
批处理时,使用removeAll 和 retainAll,可以避免手工位操作时容易出现的错误以及不太雅观的代码.
EnumSet提供了丰富的静态工厂来创建集合,其中一个:
EnumSet.of(Style.BOLD,Style.ITALIC);
33、用EnumMap代替序数索引
最好不要用序数来索引数组,而要使用EnumMap
例子:
一次索引
package enumTest;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class EnumMapTest {
static class Herb{
public enum Type {ANNUAL, PERENNIAL, BIENNTAL}
private final String name;
private final Type type;
Herb(String name, Type type){
this.name = name;
this.type = type;
}
}
public static void main(String[] args) {
Herb[] garden = ...;
Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<>(Herb.Type.class);
for(Herb.Type t : Herb.Type.values())
herbsByType.put(t, new HashSet<Herb>());
for(Herb h : garden)
herbsByType.get(h.type).add(h);
}
}
多维:
enum Phase{
SOLID, LIQUID, GAS;
public enum Transition{
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase src;
private final Phase dst;
Transition(Phase src, Phase dst){
this.src = src;
this.dst = dst;
}
private static final Map<Phase, Map<Phase, Transition>> map = new EnumMap<>(Phase.class);
static{
for(Phase p : Phase.values())
map.put(p, new EnumMap<>(Phase.class));
for(Transition trans : Transition.values())
map.get(trans.src).put(trans.dst, trans);
}
public static Transition from(Phase src, Phase dst){
return map.get(src).get(dst);
}
}
34、用接口模拟可伸缩的枚举
枚举类型可以用接口进行扩展,但是不能被继承
传递完整的扩展枚举类型
interface Operation{
double apply(double x, double y);
}
enum ExtendedOperation implements Operation{
EXP{
@Override
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER{
@Override
public double apply(double x, double y) {
return x % y;
}
};
}
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(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));
}
<T extends Enum<T> & Operation> 既表示枚举又表示Operation的子类型
另一种方法:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void test(Collection<? extends Operation> opSet, double x, double y){
for(Operation op : opSet)
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x,y));
}
虽然无法编写可拓展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟.
35、注解优先于命名模式
Java1.5之前,一般用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理.
命名模式的缺点:
1、文字拼写错误会导致失败,且没有任何提示.
2、无法确保它们只用于相应的程序元素上.
3、它们没有提供将参数值与程序元素关联起来的好方法.
注解类型声明中的这种注解叫 元注解 :
@Retention(RetentionPolicy.RUNTIME) //表明注解应该在运行时保留
@Target(ElementType.METHOD) //表明只在方法声明中才是合法的
用元注解组合实现的称为 标记注解
反射机制中将异常封装在
InvocationTargetException 中,通过getCause方法将异常信息提取出来:
catch(InvocationTargetException wrappedExc){
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
}
Class<?> class.isInstance();判断是否是此类型
36、坚持使用Override注解
37、用标记接口定义类型
标记接口 是 没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口.例如,Serializable
与之相对的还有 标记注解.
标记接口相比标记注解的优势:
1、标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型.这个类型允许在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误.
2、可以被更精确地进行锁定.
标记注解相比标记接口的优势:
1、它可以通过默认的方式添加一个或者多个注解类型元素,给已被使用的注解类型添加更多的信息.
2、它们是更大的注解机制的一部分.
如果发现自己在编写的是目标为ElementType.TYPE的标记注解类型,就要花点时间考虑清楚,它是否真的应该为注解类型,想想标记接口是否会更加合适呢.