准则一 用枚举代替常量
Java 枚举类型背后的基本思想很简单:它们是通过 public static final 修饰的字段为每个枚举常量导出一个实例的类。枚举类型实际上是 final 类型,因为没有可访问的构造函数。之所以强大是因为我们还可以这样使用:
public enum FourArithmeticOperations {
ADD("+") {
public double apply(double x, double y) {
this.printLog();
return x + y;
}
},
SUBTRACT("-") {
public double apply(double x, double y) {
this.printLog();
return x - y;
}
},
MULTIPLY("*") {
public double apply(double x, double y) {
this.printLog();
return x * y;
}
},
DIVIDE("/") {
public double apply(double x, double y) {
this.printLog();
return x / y;
}
};
private final String desc;
FourArithmeticOperations(String desc) {
this.desc = desc;
}
abstract double apply(double x, double y);
public String getDesc() {
return desc;
}
public void printLog() {
System.out.println(this.desc);
}
public static void main(String[] args) {
System.out.println(FourArithmeticOperations.ADD.apply(1, 2));
}
}
本质上枚举也是一个类,只不过这个类是不可变的,也就是final的,所以怎么使用枚举类全靠你的想象力。
那么再使用这些枚举类型的时候我们也有一个技巧,来看一个例子:
private static final Map<String, FourArithmeticOperations> stringToEnum = Arrays.stream(FourArithmeticOperations.values()).collect(Collectors.toMap(Objects::toString, e -> e));
public static FourArithmeticOperations fromString(String symbol) {
return stringToEnum.get(symbol);
}
这样的话可以实现快速查找,但是我个人认为如果这个枚举被被访问的非常频繁可以这样做,因为系统中有很多枚举,我们不可能为每个枚举都生成这样的Map会浪费空间,这样做的本质其实就是空间换时间。
还提到了一个技巧:
// 如果需要对某个操作进行拓展(这里的拓展是我们针对这个操作本身拓展一定的功能,比如下面的我们进行了取反操作),我们可以采用下面这种方式非常的
// 简洁易读性非常强
public static Operation inverse(Operation op) {
switch(op) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new AssertionError("Unknown op: " + op);
}
}
准则二 使用实例字段替代序数
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
我们可以得到每个枚举值的位置,这样做有什么不好呢?如果枚举类的使用与顺序无关则不打紧,但是一旦使用这个枚举和顺序建立关联那就比较的不好办了,因为在中间插入一个值就会影响顺序。更好的做法是把这个值存到字段里:
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 替代位字段
public 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
// Parameter is bitwise OR of zero or more STYLE_ constants
public void applyStyles(int styles) { ... }
}
这种表示方式称为位字段,允许你使用位运算的 OR 操作将几个常量组合成一个 Set,也就是一个数字表示一个集合。位字段表示方式允许使用位运算高效地执行 Set 操作,如并集和交集。但是位字段具有 int 枚举常量所有缺点,甚至更多。当位字段被打印为数字时,它比简单的 int 枚举常量更难理解。没有一种简单的方法可以遍历由位字段表示的所有元素。最后,你必须预测在编写 API 时需要的最大位数,并相应地为位字段(通常是 int 或 long)选择一种类型。一旦选择了一种类型,在不更改 API 的情况下,不能超过它的宽度(32 或 64 位)。
随意推荐使用EnumSet:
EnumSet<FourArithmeticOperations> fourArithmeticOperations = EnumSet.allOf(FourArithmeticOperations.class);
这个类实现了 Set 接口,提供了所有其他 Set 实现所具有的丰富性、类型安全性和互操作性。但在内部,每个 EnumSet 都表示为一个位向量。如果底层枚举类型有 64 个或更少的元素(大多数都是),则整个 EnumSet 用一个 long 表示,因此其性能与位字段的性能相当。批量操作(如 removeAll 和 retainAll)是使用逐位算法实现的,就像手动处理位字段一样。
准则四 使用 EnumMap 替换序数索引
好来看一个例子学习这个准则:
public enum Animal {
DOG,CAT,BIRD;
}
比如我们有一个这样的,枚举类,我们需要为每个类创建很多对象放到Set里。我们可以采用下面这种写法:
private static final Set<Animal>[] sets = new Set[Animal.values().length];
public static void init() {
for (int i = 0; i < sets.length; i++) {
sets[i] = new HashSet<>();
}
}
这样确实没有问题确实能够满足大部分时候的需求,但是也有满足不小的时候,因为数组有限制,数组使用不了泛型。
所以书中推荐我们使用:
Map<Animal, Set<Animal>> enumMap = new EnumMap(Animal.class);
Map<Animal, Set<Animal>> collect = Arrays.stream(Animal.values()).collect(Collectors.groupingBy(e -> e, Collectors.toSet()));
上面两种写法又有所区别,因为第一种类型是全的,但是第二种如果某一种类型空缺了,则不会有对应的Set。
对于嵌套的类型:
public enum Animal {
DOG,CAT,BIRD;
public enum Color {
RED,BLUE,GREEN;
}
public static void main(String[] args) {
// 同样的这里 也可以换成流式表达式
EnumMap<Animal, Set<Color>> animalSetEnumMap = new EnumMap<>(Animal.class);
}
}
准则五 使用接口模拟可扩展枚举
一个枚举类型扩展另一个枚举类型,一般来说不需要这样操作,但是如果要拓展我们可以通过接口的方式, 枚举可以实现接口。
// Emulated extensible enum using an interface
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 symbol;
}
}
使用接口来模拟可扩展枚举的一个小缺点是实现不能从一个枚举类型继承到另一个枚举类型。如果需要共享操作,可以在接口默认实现,然后实现这个接口。例如:
public enum LinkOption implements OpenOption, CopyOption {
NOFOLLOW_LINKS;
}
准则六 注解优于命名模式
如果可以使用注解,那么就没有理由使用命名模式。
准则七 坚持使用@Override
要覆盖超类声明的每个方法声明上使用@Override注解。
准则八 使用标记接口定义类型
你可能听过一个说法:标记接口已经过时,更好的方式是标记注解(Item-39)。这个言论是错误的。与标记注解相比,标记接口有两个优点。首先,标记接口定义的类型由标记类的实例实现;标记注解不会。 标记接口类型的存在允许你在编译时捕获错误,如果你使用标记注解,则在运行时才能捕获这些错误。
例如序列化是个标记接口,这样如果流在写对象的时候,可以判断这个是否实现了Serializable,这个是在编译的时候就能做到的。
ObjectOutputStream#writeObject()
对比:
使用标记注解的情况
应用范围广泛:当需要标记的不仅仅是类或接口,还包括方法、字段、构造函数等其他程序元素时,只能使用标记注解。
不需要类型检查:如果标记的目的仅仅是提供元数据信息,而不打算利用其进行类型检查或强制某些行为,则标记注解更为合适。
框架集成:如果正在使用的框架或库大量依赖于注解(如 Spring 或 JPA),则使用标记注解可以更好地与这些框架集成。
元数据需求:当需要在编译时或运行时捕获额外信息(例如,用于代码生成或反射操作),但不需要在运行时强制任何行为时,标记注解是更好的选择。
使用标记接口的情况
类型检查:如果您希望利用标记来实现类型安全的编程,即在编译时就能确保某些类型具有特定的标记,则应使用标记接口。
行为约束:当标记不仅仅是为了提供元数据,而且还暗示了一定的行为或约定时,使用标记接口可以更好地表达这种意图。
API 设计:如果您计划编写一个或多个方法,这些方法只接受实现了特定标记接口的对象,那么使用标记接口可以提供更强大的类型系统支持。