《Effective Java》阅读笔记-第六章
Effective Java 阅读笔记
第六章 枚举和注解
第 34 条 用 enum 代替 int 常量
int 类型常量或者 String 类型常量作为参数的可读性和可维护性都比较差,甚至 IDE 都不好提示。
Java 中的枚举是完全单例,并且可以有字段、方法,以及实现接口(因为编译之后就是个类,并且自动继承了java.lang.Enum
类)。
给枚举实现方法时,如果会根据枚举类型进行不同的处理,不要使用 switch 或者 if 进行判断:
这是一个反例:
枚举反例
public enum BadOperation {
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 UnsupportedOperationException("unknown op: " + this);
}
}
如果添加了新的操作方法枚举,很容易在switch时漏掉,这种情况可以在枚举上声明抽象方法,然后针对每个枚举实例去实现抽象方法:
枚举抽象方法
public enum GoodOperation {
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;
}
},
;
public abstract double apply(double x, double y);
}
在后续添加新字段/方法的时候就不会忘记处理(不处理就编译不过):
枚举添加新字段
public enum GoodOperation {
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;
GoodOperation(String symbol) {
this.symbol = symbol;
}
public abstract double apply(double x, double y);
@Override
public String toString() {
return "GoodOperation{" +
"symbol='" + symbol + '\'' +
'}';
}
}
如果枚举中只有部分是特定计算方法,比如周一到周五工资计算是工作日工资计算方式,周六和周日是加班工资计算方式,
那么在定义星期的时候,可以吧计算方式通过构造函数穿进去,计算方式通过内部枚举进行定义,这样虽然啰嗦一些,但是更安全,可以避免添加新的枚举常量时漏掉switch中的定义。
第 35 条 用字段代替枚举序号
每个枚举类的实例都有方法ordinal()
,可以获取枚举的序号,从0开始。但是这个序号是根据位置获得的,不要用这个序号作为业务中使用的序号,使用字段来代替这个东西,不然非常不好维护。永远不要根据序号获取值,如果有需要就保存到字段上。
字段代替序号
public enum Ensemble {
SOLO(1),
DUET(2),
// ...
;
private final int numberOfMusicians;
Ensemble(int numberOfMusicians) {
this.numberOfMusicians = numberOfMusicians;
}
public int getNumberOfMusicians() {
return numberOfMusicians;
}
}
第 36 条 用 EnumSet 代替位域
位域:位域(Bit fields)通常是指将一个或多个相关的布尔标志(或状态)打包到单个整数类型中的特定位上。这允许有效地使用内存,因为它避免了使用多个独立的布尔变量,而是将它们存储在一个整数类型中的不同位上。使用位域通常涉及到位操作,例如位与(&)、位或(|)、位取反(~)和位移(<<、>>)
位域例子
public class FlagsExample {
// 定义一些标志的位置
private static final int FLAG1 = 1; // 0001
private static final int FLAG2 = 2; // 0010
private static final int FLAG3 = 4; // 0100
private static final int FLAG4 = 8; // 1000
// 用一个整数类型的变量来表示一组标志
private int flags;
// 设置标志的方法
public void setFlag1(boolean value) {
if (value) {
flags |= FLAG1;
} else {
flags &= ~FLAG1;
}
}
public void setFlag2(boolean value) {
if (value) {
flags |= FLAG2;
} else {
flags &= ~FLAG2;
}
}
// 其他标志的设置方法类似...
// 检查标志的方法
public boolean isFlag1Set() {
return (flags & FLAG1) != 0;
}
public boolean isFlag2Set() {
return (flags & FLAG2) != 0;
}
// 其他标志的检查方法类似...
}
使用位域不仅可读性不好,容易出错,而且需要提前计算flag数量,确定使用int还是lang,而且超过64个之后也不再支持(int为32个)。
EnumSet可以很好的解决,例子:
Bad Example
public class Text {
public static final int STYLE_BLOD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
// ...
// 根据静态常量进行位运算获取最终style
// 比如 text.applyStytle(STYLE_BLOD | STYLE_ITALIC)
public void applyStytle(int styles) {
// ...
}
}
位运算非常容易出现错误,并且不美观,唯一的优点就是内存占用少。
Good Example
public class Text {
// 使用枚举代替
public static enum Style {BLOD, ITALIC, UNDERLINE;}
public void applyStytle(Set<Stytle> styles) {
// ...
}
}
修改成枚举之后可以通过EnumSet.of(Style.BLOD, Style.ITALIC)
来快速创建枚举 set 集合。
第 37 条 用 EnumMap 代替序号索引
道理和上一条类似,如非必要,永远不要使用ordinal()
。
如果要表达的关系是多维的就用EnumMap<..., EnumMap<...>>
表示。
第 38 条 用接口模拟可扩展的枚举
枚举不方便继承,如果需要扩展枚举,就需要通过接口来实现。
比如上面例子中的Operation
枚举,把抽象方法改为接口,可以更方便的进行扩展:
// 将抽象方法转移到接口中
public interface Operation {
double apply(double x, double y);
}
// 然后枚举类实现接口
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 "BasicOperation{" +
"symbol='" + symbol + '\'' +
'}';
}
}
这样扩展的枚举只需要实现Operation
接口就行。
接受枚举的地方也需要修改,第一种方式是通过传递 Class 对象,就可以获取该枚举下的所有示例:
public <T extends Enum<T> & Operation> void useEnumMethod(Class<T> enumClass, double x, double y) {
for (T enumConstant : enumClass.getEnumConstants()) {
enumConstant.apply(x, y);
}
}
或者仅传输一个实例:
public <T extends Enum<T> & Operation> void useEnumMethod(T opation, double x, double y) {
opation.apply(x, y);
}
泛型<T extends Enum<T> & Operation>
限定了只能是实现了 Operation 接口的枚举类。
第二种方式是传递 Operation 集合:
public void useEnumMethodByCollection(Collection<? extends Operation> operations, double x, double y) {
for (Operation operation : operations) {
operation.apply(x, y);
}
}
这种方式相对更灵活一些。
第 39 条 注解优先于命名模式
命名模式(naming pattern)就是根据名称进行处理,一般是工具或者框架需要进行特殊操作时使用。比如 Java 4 发行之前,JUnit 要求 test 作为方法名的开头。
这种方式的限制很多,注解显然是更好的解决方法,用注解进行操作可比用名字好多了。
第 40 条 坚持使用 Override 注解
重写父类方法时使用 Override 注解可以在编译期就发现很多错误,而且现在重写方法IDE中直接用快捷键就会自动加上,为什么不用呢?
第 41 条 用标记接口定义类型
标记接口(marker interface)就是不包含任何方法的接口,仅作为一个标记,比如Serializable
接口。
同事还有标记注解,比如弃用注解@Deprecated
。
标记接口相对标记注解而言优点:
- 接口的类型实例就是被标记的类型,枚举没有具体的实例类型,因此可以在编译期就发现错误,而不是推迟到运行时。
- 被接口标记的类型可以更精准的指定类型。因为接口可以继承,所以可以为特定接口进行标记,而注解如果要标记类型,那么所有的类和接口都允许被标记。
标记注解最大的优点就是:不仅可以标记类和接口,还可以标记方法、参数等。而且可以标记一次或多次,并且逐渐添加丰富的信息。
- 注解可以标记一次
标记接口和标记注解的选择:
- 如果标记类、接口、枚举,那就用接口
- 如果是方法、字段等非类、非接口的地方,就用注解
这本书的中文版真是狗屎翻译