Effective Java(第三版) 学习笔记 - 第六章 枚举和注解 Rule34~Rule41
目录
感觉除了枚举的基础用法,其他的一些内容日常开发中并不常用,仅做拓展了解就足够了。
Rule34 用Enum代替int常量
II The int enum pattern - severely deficient!
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public enum APPLE {
FUJI(0),
PIPPIN(1),
GRANNY_SMITH(2),
;
private int value;
APPLE(int value) {
this.value = value;
}
}
我相信大部分人都会选择下面用枚举定义的方式,相同的业务字段定义放在一个业务属性枚举里面进行声明,而不是利用前缀人为的写在一起,从代码结构上就可以看出当前具有哪些业务属性枚举,而不是需要在大的常量类中自己搜索。
同时,也可以让开发者清楚的知道该业务属性一共有几种业务定义,有一个全貌的认知(往往特别是数据库类型的定义尝尝会遇到没有注释或者注释不全面)。
也可以避免硬编码带来的不便(当然就算定义了枚举,也需要运用到每一处的使用中,不然还是用硬编码的话,枚举的意义就缺失了)。
下面介绍一下书中提到的,除了基础定义之外的枚举用法:
1、将枚举定义与某种行为关联起来
举例:提供一个运算枚举,并支持计算功能
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
public 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("Unknow op:" + this);
}
}
public enum Operation {
// PLUS { @Override public double apply(double x, double y) { return applyPlus(x, y);}},
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;}},
;
public abstract double apply(double x, double y);
private final static double applyPlus(double x, double y) {
return x + y;
}
}
下面的这种用抽象方法来进行运算定义,各自实现在枚举的声明中进行重写最大的好处就是不需要防范光新增了枚举类型,但是没有漏写了Switch分歧。也不用防范枚举之外的操作。
如果代码段中逻辑比较多或者需要共通话,可以用本地私有方法来进行调用。
这种手法称之为:特定于常量的方法实现
2、多个枚举共享相同行为
举例:计算加班费场景
// The strategy enum pattern (Page 166)
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
SATURDAY(WEEKEND), SUNDAY(WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// The strategy enum type
enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 :
(minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
public static void main(String[] args) {
for (PayrollDay day : values())
System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
}
}
这种设计方式称之为:策略枚举
与Switch相比,策略枚举看起来代码量更多,结构没有Switch简洁。但是带来的好处是更加安全,也更加灵活。
Rule35 用实例域代替序数
// Abuse of ordinal to derive an a ssoci a ted value - DON ’T DO THIS
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, DOUBLE_QUARTET,
NONET, DECTET, TRIPLE_QUARTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
// Enum with integer data stored in an instance field (Page 168)
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
简单来说就是,永远不要根据枚举的序数导出与它关联的值,而是要把它保存在一个实例域中。大多数程序员都不需要这个方法,它是设计像EnumSet和EnumMap这种基于枚举的通用数据结构的。这条仅做了解就行。
※ordinal()不了解是什么的,可以看下Enum这个类。
Rule36 用EnumSet代替位域
位域这个词是第一次听到。
简单描述的话,位域就是利用二进制数,其中每一位代表一种业务数据,整体看是一串二进制数,可以转成整型,但是其实每一位二进制的1都是独立数据含义的。
举个例子:
用一个7位的二进制保存颜色信息,其中每一位代表一种颜色。 0000 0001代表赤,0000 0010代表橙,以此类推。同时,0001 0001代表【赤、青】,0111 1111代表所有定义的颜色。
这个时候位域的优势就体现出来了,用一个值,就可以表示数组或列表的传参,占用内存更小,相比数组或者列表可能性能更高,但是可以一般的业务开发运用这种方式的话,可能项目管理起来会比较困难,需要有机制来约定并在实际的使用中都有一些高的要求标准,而且如果定义内容比较多,二进制长度就需要越长,直接看时不容易理解业务代表含义。一般不太常见,仅做了解就好。
而且,Java的整数类型最大是8字节,64位。如果遇到64位不够时,可以考虑用BitMap来存储。利用数组来弥补基础类型的64位长度限制。
同时,BitMap有可能出现:
- 数据碰撞 :比如将字符串映射到 BitMap 的时候会有碰撞的问题,那就可以考虑用 Bloom Filter 来解决,Bloom Filter 使用多个 Hash 函数来减少冲突的概率。
- 数据稀疏 :又比如要存入(10,8887983,93452134)这三个数据,我们需要建立一个 99999999 长度的 BitMap ,但是实际上只存了3个数据,这时候就有很大的空间浪费,碰到这种问题的话,可以通过引入 Roaring BitMap 来解决。
EnumSet用泛型限制了类型必须是枚举,同时继承了AbstractSet,具备Set的一些特性。EnumSet有两个子类,RegularEnumSet和JumboEnumSet,有兴趣的可以了解一下。
为什么说用EnumSet代替位域,因为EnumSet的底层也是用二进制操作提速的,但是相比纯二进制,EnumSet更贴合业务开发者的业务定义和个人理解,同时又封装好了系列二进制操作,所以推荐使用。
※目前业务开发中,暂还未用过EnumSet。一般都是把枚举里面的实例属性单独放入一个list或set中
Rule37 用EnumMap代替序数索引
之前Rule35中说过,不要用ordinal来做一些业务上的处理逻辑。如果需要用到一些遍历情况时,建议用EnumMap来进行操作,封装内部也是用数组来进行操作,但是相比较之下更加安全,而不是用序数来索引数组。
※目前业务开发中,也暂未用过EnumMap。
Rule38 用接口模拟可扩展枚举
可扩展枚举基本不太常用,一般运用枚举就是为了在编程的时候就确定了改枚举所属的业务属性及其含义,一般遇到需要追加类型时,代码都是自己公司维护的,直接在原枚举类中新增就是。
可能作为基础组件给外部项目提供Jar包时需要运用到扩展枚举吧。我们只需要知道枚举是不可扩展的,如果需要扩展,就需要利用接口的特性来进行模拟。看个书中用例很容易理解,有所了解就行。
// Emulated extensible enum using an interface (Page 176)
public interface Operation {
double apply(double x, double y);
}
// Emulated extensible enum using an interface - Basic implementation (Page 176)
public enum BasicOperation implements Operation {
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("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
// Emulated extensible enum (Pages 176-9)
public enum ExtendedOperation implements Operation {
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;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
// // Using an enum class object to represent a collection of extended enums (page 178)
// 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> opEnumType, double x, double y) {
// for (Operation op : opEnumType.getEnumConstants())
// System.out.printf("%f %s %f = %f%n",
// x, op, y, op.apply(x, y));
// }
// Using a collection instance to represent a collection of extended enums (page 178)
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));
}
}
Rule39 注解优先于命名模式
感觉没啥好说的,相比于人为的约定项目内部方法名要符合某种自定义规范,用注解的形式更加方便、代码也更明确。
自定义注解相关请看《Java注解》
Rule40 坚持使用@Override注解
没啥好说的,Idea也会自动检查。需要知道的是,@Override注解的声明周期是代码级别,生成class文件就废除了。
Rule41 用标记接口定义类型
标记接口:声明的接口并没有实际的方法提供,仅仅是一种标记作用,代表当前类或接口符合某一种规范之类的。例如,Serializable、Cloneable,打开源码就是一个空的接口。相反,Comparable这种虽然也可以标明实现它的类具备比较特性,但是由于实现类需要实装compareTo方法,所以并不是纯意义上的标记接口。
标记注解:与标记接口一样,注解中不包含任何成员。判断的时候运用isAnnotationPresent()来进行判断行。
※isAnnotationPresent()方法是定义在AnnotatedElement接口中。部分结构图如下
两种标记方式都具有各自优势,如果仅用于类或接口,可以优先考虑用接口标记的方式。如果需要作用于成员或方法,那只能使用注解标记。
本文技术菜鸟个人学习使用,如有不正欢迎指出修正。xuweijsnj