使用接口模拟可扩展的枚举

This commit is contained in:
尉勇强2 2020-04-16 20:14:52 +08:00
parent 242c7c8d58
commit 85d02fd51a
12 changed files with 435 additions and 0 deletions

View File

@ -0,0 +1,36 @@
## 使用 EnumSet 替代位属性
如果枚举类型的元素主要用于集合中:
- [Item36Example01.java](EnumsAnnotations/src/main/java/com/jueee/item36/Item36Example01.java):传统上使用 int 枚举模式,将 2 的不同次幂赋值给每个常量。
这种表示方式允许你使用按位或or运算将几个常量合并到一个称为位域的集合中。
位域表示还允许你使用按位算术有效地执行集合运算,如并集和交集。
但是位域具有 `int` 枚举常量等的所有缺点:
- 当打印为数字时,解释位域比简单的 `int` 枚举常量更难理解。
- 没有简单的方法遍历所有由位域表示的元素。
- 必须预测在编写 API 时需要的最大位数,并相应地为位域(通常为 `int``long`)选择一种类型。
- [Item36Example02.java](EnumsAnnotations/src/main/java/com/jueee/item36/Item36Example02.java):使用枚举而不是 `int` 常量。
`java.util` 包提供了 `EnumSet` 类来有效地表示从单个枚举类型中提取的值集合。
这个类实现了 `Set` 接口,提供了所有其他 `Set` 实现的丰富性,类型安全性和互操作性。
请注意,`applyStyles` 方法采用`Set<Style>``EnumSet<Style>`
尽管所有客户端都可能会将 `EnumSet` 传递给该方法,但接受接口类型而不是实现类型通常是很好的做法。
这允许一个不寻常的客户端通过其他 Set 实现的可能性。
总之,**仅仅因为枚举类型将被用于集合中,就没有理由用位域来表示它。**
`EnumSet` 类结合了位域的简洁性和性能以及枚举类型的所有优点。
截至 Java 9 `EnumSet` 的一个真正缺点是不能创建一个不可变的 `EnumSet`,但是在之后的发行版本中这可能会得到纠正。
同时,你可以用 `Collections.unmodifiableSet` 封装一个 `EnumSet`,但是简洁性和性能会受到影响。

View File

@ -0,0 +1,47 @@
## 使用 EnumMap 替代序数索引
- [Item37Example01.java](EnumsAnnotations/src/main/java/com/jueee/item37/Item37Example01.java):使用 `ordinal` 方法来索引到数组或列表的代码。
这种方法是有效的,但充满了问题。
因为数组不兼容泛型,程序需要一个未经检查的转换,并且不会干净地编译。
由于该数组不知道索引代表什么,因此必须手动标记索引输出。
但是这种技术最严重的问题是,当你访问一个由枚举序数索引的数组时,你有责任使用正确的 `int` 值; `int` 不提供枚举的类型安全性。
如果你使用了错误的值,程序会默默地做错误的事情,如果你幸运的话,抛出一个 `ArrayIndexOutOfBoundsException` 异常。
- [Item37Example02.java](EnumsAnnotations/src/main/java/com/jueee/item37/Item37Example02.java):使用 `java.util.EnumMap`
这段程序更简短,更清晰,更安全,运行速度与原始版本相当。
没有不安全的转换; 无需手动标记输出,因为 `map` 键是知道如何将自己转换为可打印字符串的枚举; 并且不可能在计算数组索引时出错。
`EnumMap` 与序数索引数组的速度相当,其原因是 `EnumMap` 内部使用了这样一个数组,但它对程序员的隐藏了这个实现细节,将 Map 的丰富性和类型安全性与数组的速度相结合。
请注意,`EnumMap` 构造方法接受键类`Class`型的 Class 对象这是一个有限定的类型令牌bounded type token它提供运行时的泛型类型信息。
数组索引(两次)的数组,用序数来表示从两个枚举值的映射:
- [Item37Example03.java](EnumsAnnotations/src/main/java/com/jueee/item37/Item37Example03.java):使用一个数组来映射两个阶段到一个阶段转换。
编译器无法知道序数和数组索引之间的关系。
如果在转换表中出错或者在修改 `Phase``Phase.Transition` 枚举类型时忘记更新它,则程序在运行时将失败。
- [Item37Example04.java](EnumsAnnotations/src/main/java/com/jueee/item37/Item37Example04.java):用 `EnumMap` 做得更好。
因为每个阶段转换都由一对阶段枚举来索引所以最好将关系表示为从一个枚举from 阶段到第二个枚举to 阶段)到结果(阶段转换)的 `map`
初始化阶段转换的 `map` 的代码有点复杂。`map` 的类型是 `Map<Phase, Map<Phase, Transition>>`,意思是「从(源)阶段映射到从(目标)阶段到阶段转换映射。」这个 `map``map` 使用两个收集器的级联序列进行初始化。
第一个收集器按源阶段对转换进行分组,第二个收集器使用从目标阶段到转换的映射创建一个 `EnumMap`。 第二个收集器 `((x, y) -> y))` 中的合并方法未使用;仅仅因为我们需要指定一个 `map` 工厂才能获得一个 `EnumMap`,并且 `Collectors` 提供伸缩式工厂,这是必需的。
**总之,使用序数来索引数组很不合适:改用 EnumMap。**
如果你所代表的关系是多维的,请使用 `EnumMap <...EnumMap <... >>`
应用程序员应该很少使用 `Enum.ordinal`,如果使用了,也是一般原则的特例。

View File

@ -0,0 +1,29 @@
## 使用接口模拟可扩展的枚举
利用枚举类型可以通过为 `opcode` 类型定义一个接口,并实现任意接口。
- [Operation.java](EnumsAnnotations/src/main/java/com/jueee/item38/Operation.java):可扩展版本的 `Operation` 类型。
- [BasicOperation.java](EnumsAnnotations/src/main/java/com/jueee/item38/BasicOperation.java):虽然枚举类型(`BasicOperation`)不可扩展,但接口类型(`Operation`)是可以扩展的,并且它是用于表示 API 中的操作的接口类型。
- [ExtendedOperation.java](EnumsAnnotations/src/main/java/com/jueee/item38/ExtendedOperation.java):定义前面所示的操作类型的扩展,包括指数运算和余数运算。
定义另一个实现此接口的枚举类型,并使用此新类型的实例来代替基本类型。
- [Item38Example01.java](EnumsAnnotations/src/main/java/com/jueee/item38/Item38Example01.java):传入整个扩展枚举类型,并使用其元素。
扩展的操作类型的类字面文字(`ExtendedOperation.class`)从 `main` 方法里传递给了 `test` 方法,用来描述扩展操作的集合。
这个类的字面文字用作限定的类型令牌。
`opEnumType` 参数中复杂的声明`<T extends Enum<T> & Operation> Class<T>`)确保了 `Class` 对象既是枚举又是 `Operation` 的子类,这正是遍历元素和执行每个元素相关联的操作时所需要的。
- [Item38Example02.java](EnumsAnnotations/src/main/java/com/jueee/item38/Item38Example02.java):传递一个 `Collection<? extends Operation>`,这是一个限定通配符类型,而不是传递了一个 `class` 对象。
使用接口来模拟可扩展枚举的一个小缺点是,实现不能从一个枚举类型继承到另一个枚举类型。
如果实现代码不依赖于任何状态,则可以使用默认实现将其放置在接口中。
该条目中描述的模式在 `Java` 类库中有所使用。例如,`java.nio.file.LinkOption` 枚举类型实现了 `CopyOption``OpenOption` 接口。
**总之,虽然不能编写可扩展的枚举类型,但是你可以编写一个接口来配合实现接口的基本的枚举类型,来对它进行模拟。** 这允许客户端编写自己的枚举(或其它类型)来实现接口。如果 `API` 是根据接口编写的,那么在任何使用基本枚举类型实例的地方,都可以使用这些枚举类型实例。

View File

@ -0,0 +1,62 @@
package com.jueee.item37;
import java.awt.List;
import java.util.HashSet;
import java.util.Set;
/**
*
* @author hzweiyongqiang
*/
public class Item37Example01 {
/**
*
* @author hzweiyongqiang
*/
public enum LifeCycle {
ANNUAL("一年生"), PERENNIAL("多年生"), BIENNIAL("双年生");
private String action;
LifeCycle(String action) {
this.action = action;
}
public String getAction() {
return action;
}
}
public class Plant {
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
public static void main(String[] args) {
// Using ordinal() to index into an array - DON'T DO THIS!
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
// Print the results
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n", LifeCycle.values()[i], plantsByLifeCycle[i]);
}
}
}

View File

@ -0,0 +1,59 @@
package com.jueee.item37;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
*
* @author hzweiyongqiang
*/
public class Item37Example02 {
/**
*
* @author hzweiyongqiang
*/
public enum LifeCycle {
ANNUAL("一年生"), PERENNIAL("多年生"), BIENNIAL("双年生");
private String action;
LifeCycle(String action) {
this.action = action;
}
public String getAction() {
return action;
}
}
public class Plant {
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
public static void main(String[] args) {
// Using an EnumMap to associate data with an enum
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
for (LifeCycle lc : LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
System.out.println(plantsByLifeCycle);
}
}

View File

@ -0,0 +1,36 @@
package com.jueee.item37;
import java.awt.List;
import java.util.HashSet;
import java.util.Set;
/**
*
* @author hzweiyongqiang
*/
public class Item37Example03 {
/**
* Using ordinal() to index array of arrays - DON'T DO THIS!
* @author hzweiyongqiang
*/
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// Rows indexed by from-ordinal, cols by to-ordinal
private static final Transition[][] TRANSITIONS = {
{ null, MELT, SUBLIME },
{ FREEZE, null, BOIL },
{ DEPOSIT, CONDENSE, null }
};
// Returns the phase transition from one phase to another
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
}

View File

@ -0,0 +1,45 @@
package com.jueee.item37;
import java.util.EnumMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
*
* @author hzweiyongqiang
*/
public class Item37Example04 {
/**
* Using a nested EnumMap to associate data with enum pairs
*
* @author hzweiyongqiang
*/
public 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 from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
.collect(Collectors.groupingBy(t -> t.from, () -> new EnumMap<>(Phase.class),
Collectors.toMap(t -> t.to, t -> t, (x, y) -> y, () -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
}

View File

@ -0,0 +1,43 @@
package com.jueee.item38;
/**
*
* @author hzweiyongqiang
*/
public enum BasicOperation implements Operation {
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;
}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}

View File

@ -0,0 +1,26 @@
package com.jueee.item38;
//Emulated extension enum
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;
}
}

View File

@ -0,0 +1,19 @@
package com.jueee.item38;
public class Item38Example01 {
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));
}
}
public static void main(String[] args) {
String[] testArgs = new String[] {"3.6", "4.9"};
double x = Double.parseDouble(testArgs[0]);
double y = Double.parseDouble(testArgs[1]);
test(ExtendedOperation.class, x, y);
}
}

View File

@ -0,0 +1,26 @@
package com.jueee.item38;
import java.util.Arrays;
import java.util.Collection;
/**
*
* @author hzweiyongqiang
*/
public class Item38Example02 {
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));
}
}
public static void main(String[] args) {
String[] testArgs = new String[] {"3.6", "4.9"};
double x = Double.parseDouble(testArgs[0]);
double y = Double.parseDouble(testArgs[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
}

View File

@ -0,0 +1,7 @@
package com.jueee.item38;
public interface Operation {
double apply(double x, double y);
}