25、源文件仅限有单个顶层类
虽然Java编译器允许在单个源文件中定义多个顶层类,但是这样做没有任何好处,反而存在重大风险。请看下面的例子,这个源文件引用了另外两个顶层类(Utensil 和 Dessert):
public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}
}
然后你在一个名为 Utensil.java 的源文件中提供了 Utensil 类和 Dessert 类的实现:
// Two classes defined in one file. Don't ever do this!
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}
不料在你不知情的情况下,另一个人在名为 Dessert.java的源文件中定义了相同的两个类:
// Two classes defined in one file. Don't ever do this!
class Utensil {
static final String NAME = "pot";
}
class Dessert {
static final String NAME = "pie";
}
如果你使用 javac Main.java Dessert.java 命令编译程序,编译将失败,编译器将告诉你多重定义了 Utensil 和 Dessert。这是因为编译器将首先编译 Main.java,当它看到对 Utensil 的引用(在对 Dessert 的引用之前)时,它将在 Utensil.java 中查找这个类,并找到餐具和甜点。当编译器在命令行上遇到 Dessert.java 时,(编译器)也会载入该文件,导致(编译器)同时遇到 Utensil 和 Dessert 的定义。
如果你使用命令 javac Main.java 或 javac Main.java Utensil.java 编译程序,它将按我们的预期打印出pancake。但是如果你使用命令 javac Dessert.java Main.java 编译程序,它将按别人的实现打印出 potpie。因此,程序的行为受到源文件传递给编译器的顺序的影响,这显然是不可接受的。
要避免这种问题,只需将顶层类(Utensil和Dessert)分割到单独的源文件中,或者放弃将它们作为顶层类,转而使用静态成员类。如下所示:
// Static member classes instead of multiple top-level classes
public class Test {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}
private static class Utensil {
static final String NAME = "pan";
}
private static class Dessert {
static final String NAME = "cake";
}
}
泛型
26、 不要使用原始类型
每个泛型都定义了一个原始类型,它是没有任何相关类型参数的泛型的名称。例如,List 对应的原始类型是 List。原始类型的行为就好像所有泛型信息都从类型声明中删除了一样。它们的存在主要是为了与之前的泛型代码兼容。
使用原始类型是类型不安全的,编译器会发出警告,而且容易出现类型相关的运行时异常。 如下面的程序:
// Fails at runtime - unsafeAdd method uses a raw type (List)!
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0); // Has compiler-generated cast
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
因为使用了原始类型List,编译器会给出一个警告:
Test.java:10: warning: [unchecked] unchecked call to add(E) as a
member of the raw type List
list.add(o);
^
然后运行程序时,会在将strings.get(0)` 的结果强制转换为字符串的地方抛出一个 ClassCastException。
如果你想使用泛型,但不知道或不关心实际的类型参数是什么,那么可以使用问号代替。例如,泛型集合 Set<E> 的无界通配符类型是 Set<?> 。它是最通用的参数化集合类型,能够容纳任何集合:
// Uses unbounded wildcard type - typesafe and flexible
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }
无界通配符类型和原始类型之间的区别是:前者中不能放入任何元素(除了 null)。如果你这样做,会得到一个编译报错:
WildCard.java:13: error: incompatible types: String cannot be converted to CAP#1
c.add("verboten");
^ where CAP#1
is a fresh type-variable:
CAP#1 extends Object from capture of ?
为便于参考,本条目中介绍的术语(以及后面将要介绍的一些术语)总结如下:
27、消除 unchecked 警告
使用泛型编程时,很容易看到unchecked编译器警告。应该尽可能消除这些警告。消除所有这些警告后,就能确保代码是类型安全的。
有时unchecked警告很容易消除,例如下面不规范的代码会导致编译器警告:
Set<Lark> exaltation = new HashSet();
可以修改一下,取消警告:
Set<Lark> exaltation = new HashSet<>();
但有时警告无法消除,如果可以证明代码是类型安全的,可以通过SuppressWarnings("unchecked") 注解来抑制警告。
SuppressWarnings应该在尽可能小的范围内使用。如下例在一个变量上使用这个注解:
// Adding local variable to reduce scope of @SuppressWarnings
public <T> T[] toArray(T[] a) {
if (a.length < size) {
// This cast is correct because the array we're creating
// is of the same type as the one passed in, which is T[].
@SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
return result;
}
System.arraycopy(elements, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
每次使用注解时,要添加一条注释,说明这样做是安全的,以帮助他人理解代码。
28、列表优于数组
使用泛型时,优先考虑用list,而非数组。
数组和泛型有两个重要区别,这让它们在一起工作不那么协调。
区别一:数组是协变的,而泛型不是。如果 Sub 是 Super 的子类型,那么类型 Sub[] 就是类型 Super[] 的子类型,而**List<Sub>** 并非**List<Super>** 的子类型。
例如,下面这段代码是合法的:
// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
但这一段代码就不是:
// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");
两种方法都不能将 String 放入 Long 容器,但使用数组,会得到一个运行时错误;使用 list,你可以在编译时发现问题。后者当然是更加安全的。
区别二:数组是具体化的,而泛型通过擦除来实现。这意味着,数组在运行时知道并强制执行他们的元素类型,而泛型只在编译时执行类型约束,在运行时丢弃类型信息,这样做是为了与不使用泛型的老代码兼容。
由于这些差异,数组和泛型不能很好地混合。例如,创建泛型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都是非法的:new List<E>[] 、new List<String>[] 、new E[] 。所有这些行为都会在编译时报错,原因是它们并非类型安全。如果合法,那么类型错误可能延迟到运行时才出现,这违反了泛型系统的基本保证。
例如下面代码:
// Why generic array creation is illegal - won't compile!
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
如果第一行是合法的,那么会运行时到第5行才抛出运行时异常。
29、优先考虑泛型
应该尽量在自己编写的类型中使用泛型,这会保证类型安全,并使代码更易使用。
下面通过例子来看下如何对一个现有类做泛型化改造。
首先是一个简单的堆栈实现:
// Object-based collection - a prime candidate for generics
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
我们用适当的类型参数替换所有的 Object 类型,然后尝试编译修改后的程序:
// Initial attempt to generify Stack - won't compile!
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new E[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
} ... // no changes in isEmpty or ensureCapacity
}
这时生成一个错误:
Stack.java:8: generic array creation
elements = new E[DEFAULT_INITIAL_CAPACITY];
^
上一条目中讲到不能创建一个非具体化类型的数组。因此我们修改为:
// The elements array will contain only E instances from push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[]!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
另一种解决编译错误的方法是将字段元素的类型从 E[] 更改为 Object[]。这时会得到一个不同的错误:
Stack.java:19: incompatible types
found: Object, required: E
E result = elements[--size];
^
You can change this error into a warning by casting the element retrieved from the array to E, but you will get a warning:
通过将从数组中检索到的元素转换为 E,可以将此错误转换为警告。我们对警告做抑制:
// Appropriate suppression of unchecked warning
public E pop() {
if (size == 0)
throw new EmptyStackException();
// push requires elements to be of type E, so cast is correct
@SuppressWarnings("unchecked")
E result =(E) elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
30、优先使用泛型方法
应尽量使方法支持泛型,这样可以保证类型安全,并让代码更容易使用。例如下面代码:
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
有时,你需要创建一个对象,该对象是不可变的,但适用于许多不同类型,这时可以用泛型单例工厂模式来实现,如 Collections.emptySet。
下面的例子实现了一个恒等函数分发器:
// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}
然后是对这个恒等函数分发器的使用:
// Sample program to exercise generic singleton
public static void main(String[] args) {
String[] strings = { "jute", "hemp", "nylon" };
UnaryOperator<String> sameString = identityFunction();
for (String s : strings)
System.out.println(sameString.apply(s));
Number[] numbers = { 1, 2.0, 3L };
UnaryOperator<Number> sameNumber = identityFunction();
for (Number n : numbers)
System.out.println(sameNumber.apply(n));
}
递归类型限定允许类型参数被包含该类型参数本身的表达式限制,常见的场合是用在Comparable接口上。例如:
// Using a recursive type bound to express mutual comparability
public static <E extends Comparable<E>> E max(Collection<E> c);
31、使用限定通配符来增加 API 的灵活性
在泛型中使用有界通配符,可以让API更加灵活。
考虑第29条中的堆栈类。我们创建一个**Stack<Number>** 类型的堆栈,并在其中插入integer。
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);
这个例子在直觉上似乎是没问题的。然而实际执行的时候会报错:
StackTest.java:7: error: incompatible types: Iterable<Integer>
cannot be converted to Iterable<Number>
numberStack.pushAll(integers);
^
解决办法是使用带extends的有界通配符类型。下面代码表示泛型参数为E的子类型(包括E类型本身):
// Wildcard type for a parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
作为堆栈,还需要对外提供弹出元素的方法,以下是基础实现:
// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
但是这个实现在遇到下面场景时也会出现运行时报错,错误信息与前面的非常类似:Collection<Object> 不是 Collection<Number> 的子类型。
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ... ;
numberStack.popAll(objects);
解决办法是使用带super的有界通配符类型。下面例子表示泛型参数为E的超类(包括E类型本身)。
// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
while (!isEmpty()
dst.add(pop());
}
总结上面例子的经验,就是生产者用extends通配符,消费者用super通配符。可以简记为PECS原则:producer-extends, consumer-super。
32、合理地结合泛型和可变参数
可变参数方法和泛型在一起工作时不那么协调,因此需要特别注意。
可变参数方法的设计属于一个抽象泄漏,即当你调用可变参数方法时,将创建一个数组来保存参数;该数组本应是实现细节,却是可见的。因此会出现编译器警告。
下面是一个可变参数和泛型混用,造成类型错误的例子:
// Mixing generics and varargs can violate type safety!
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // Heap pollution
String s = stringLists[0].get(0); // ClassCastException
}
有人可能会问:为什么使用泛型可变参数声明方法是合法的,而显式创建泛型数组是非法的?因为带有泛型或参数化类型的可变参数的方法在实际开发中非常有用,因此语言设计人员选择忍受这种不一致性。
要保证可变参数和泛型混用时的类型安全,有以下两种方式:
- 为方法添加 SafeVarargs标记,这代表方法的编写者承诺这个方法是类型安全的,需要方法编写者自己保证。这时编译器警告会被消除。
- 如果方法内部保证可变参数只读,不做任何修改,那么这个方法也是类型安全的。
将数组传递给另一个使用 @SafeVarargs 正确注释的可变参数方法是安全的,将数组传递给仅计算数组内容的某个函数的非可变方法也是安全的。
33、优先考虑类型安全的异构容器
在前面的例子中,类型参数都是固定数量,例如**Map<K, V>** 就只有两个类型参数。如果我们需要无限数量的类型参数,可以通过将类型参数放置在键上而不是容器上。例如,可以使用 DatabaseRow 类型表示数据库行,并使用泛型类型 Column<T> 作为它的键。
下面的例子实现了以类型作为键的缓存,它是类型安全的,例如以String类型为键读取时,读到的对象肯定也是String类型,而不是Integer类型。
// Typesafe heterogeneous container pattern - implementation
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
// Typesafe heterogeneous container pattern - client
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString,favoriteInteger, favoriteClass.getName());
}
}
枚举和注解
34、用枚举类型代替 int 常量
枚举类型相比int常量有不少优点,如:能提供类型安全性,能提供 toString 方法打印字符串,还允许添加任意方法和字段并实现任意接口,使得枚举成为功能齐全的抽象(富枚举类型)。
一般来说,枚举在性能上可与 int 常量相比,不过加载和初始化枚举类型需要花费空间和时间,实际应用中这一点可能不太明显。
35、使用实例字段替代序数
所有枚举都有一个 ordinal 方法,该方法返回枚举类型中每个枚举常量的数值位置:
// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
这样写虽然可行,但难以维护。如果常量被重新排序,numberOfMusicians 方法将被破坏。 更好的办法是使用一个额外的字段来代表序数:
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; }
}
ordinal是为基于枚举的通用数据结构(EnumSet 和 EnumMap)设计的。除非你用到这些数据结构,否则最好完全避免使用这个方法。
36、用 EnumSet 替代位字段
如果枚举类型的元素主要在 Set 中使用,传统上使用 int 枚举模式,通过不同的 2 的平方数为每个常量赋值:
// Bit field enumeration constants - OBSOLETE!
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) { ... }
}
这种表示方式称为位字段,允许你使用位运算的或操作将几个常量组合成一个 Set:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
位字段具有 int 枚举常量所有缺点,例如:
- 被打印成数字时难以理解
- 没有简单的方法遍历位字段所有元素
- 一旦确定了int或long作为位字段的存储类型,就不能超过它的范围(32位或64位)
EnumSet类是一种更好的选择,它避免了以上缺点。而且由于它在底层实现上与位操作类似,因此与位字段性能相当。
将之前的示例修改为使用EnumSet的方法,更加简单清晰:
// EnumSet - a modern replacement for bit fields
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) { ... }
}
下面是将 EnumSet 实例传递给 applyStyles 方法的用户代码。EnumSet 类提供了一组丰富的静态工厂,可以方便地创建 Set:
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
37、使用 EnumMap 替换序数索引
用序数索引数组不如使用 EnumMap ,应尽量少使用 ordinal() 。
例如这个类表示一种植物:
class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override public String toString() {
return name;
}
}
假设有一个代表花园全部植物的 Plant 数组 plantsByLifeCycle,用于列出按生命周期(一年生、多年生或两年生)排列的植物:
有些程序员会将这些集合放到一个按照类型序号进行索引的数组实现这一点。
// 泛型数组
Set<Plant>[] plantsByLifeCycle =(Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++)
plantsByLifeCycle[i] = new HashSet<>();
for (Plant p : garden)
// Using ordinal() to index into an array - DON'T DO THIS!
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
// Print the results
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n",
Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
这种技术有如下问题:
数组与泛型不兼容,需要 unchecked 转换。
-
类型擦除:Java 的泛型是在编译时实现的,也就是说,在运行时,泛型类型信息会被擦除。而在Java中,数组是一个协变的、具有运行时类型信息的数据结构。由于泛型信息在运行时被擦除,因此在运行时无法知道具体的泛型参数类型,这和数组的运行时类型信息是相冲突的。
类型安全性:Java为了保证类型安全,禁止了泛型数组的创建。假设Java允许我们创建泛型数组,则有可能发生下面的情况:
List<String>[] stringLists = new List<String>[1];
Object[] objects = stringLists;
objects[0] = new ArrayList<Integer>();
这里,将一个**ArrayList<Integer>** 赋给了一个**Object[]** 引用,并没有引发任何警告或错误。但是,现在**stringLists[0]** 实际上就是一个Integer列表,如果我们试图在其中放入一个字符串,就会在运行时抛出**ClassCastException**。
数组不知道索引表示什么,必须手动标记输出。
int 不提供枚举的类型安全性,无法检验int值的正确性。
解决方案:
1、使用 java.util.EnumMap:
// Using an EnumMap to associate data with an enum
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);
这个程序比原来的版本更短,更清晰,更安全,速度也差不多。速度相当的原因是,EnumMap 在内部使用这样的数组,但是它向程序员隐藏了实现细节。
2、使用流可以进一步缩短程序:
// Naive stream-based approach - unlikely to produce an EnumMap!
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)));
这段代码的性能较差,因为底层不是基于 EnumMap,而是自己实现Map。
3、要改进性能,可以使用 mapFactory 参数指定 Map 实现:
// Using a stream and an EnumMap to associate data with an enum
System.out.println(
Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle,() -> new EnumMap<>(LifeCycle.class), toSet()))
);
下面例子用二维数组描述了气液固三台之间的装换,并使用序数索引读取二维数组的值:
// Using ordinal() to index array of arrays - DON'T DO THIS!
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()];
}
}
}
这个程序的问题与前面 garden 示例一样,编译器无法知道序数和数组索引之间的关系。如果你在转换表中出错,或者在修改 Phase 或 Phase.Transition 枚举类型时忘记更新,程序将在运行时失败。
使用 EnumMap 可以做得更好:
// Using a nested EnumMap to associate data with enum pairs
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 =
new EnumMap<Phase, Map<Phase ,Transition>>(Phase.class);
static{
for (Phase p : Phase. values())
m.put(p,new EnumMap<Phase,Transition (Phase.class));
for (Transition trans : Transition.values() )
m.get(trans. src).put(trans.dst, trans) ;
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
如果你想向系统中加入一种新阶段:等离子体。这个阶段只有两个变化:电离、去离子作用。修改基于EnumMap的版本要比基于数组的版本容易得多,而且更加清晰安全:
// Adding a new phase using the nested EnumMap implementation
public enum Phase {
SOLID, LIQUID, GAS, PLASMA;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),
IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
... // Remainder unchanged
}
}
38、使用接口模拟可扩展枚举
虽然不能编写可扩展枚举类型,但是可以通过编写接口来模拟它。
Java语言层面不支持可扩展枚举类型,但有时我们需要实现类似的需求,如操作码。操作码是一种枚举类型,其元素表示某些机器上的操作,例如第34条中的 Operation 类,它表示简单计算器上的函数。有时候,我们希望 API 的用户提供自己的操作,从而有效地扩展 API 提供的操作集。
一种思路是为操作码类型定义一个接口,并为接口的标准实现定义一个枚举:
// 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;
}
}
用这种方法可以轻松扩展自己的实现:
// 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;
}
}
通过传入扩展枚举类型到方法,可以编写一个上述代码的测试程序,它执行了前面定义的所有扩展操作:
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));
}
第二个选择是传递一个 Collection<? extends 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));
}
这种方法的优点是:代码更简单,且允许调用者组合来自多个实现类型的操作。缺点是:放弃了在指定操作上使用 EnumSet和EnumMap的能力。
接口模拟可扩展枚举的一个小缺点是实现不能从一个枚举类型继承到另一个枚举类型。如果实现代码不依赖于任何状态,则可以使用默认实现将其放置在接口中。
39、注解优于命名模式
如果可以使用注解,那么就没有理由使用命名模式。
历史上,使用命名模式来标明某些程序元素需要框架特殊处理是很常见的。例如,JUnit 4以前的版本要求其用户通过以字符 test 开头的名称来指定测试方法。这种技术是有效的,但是有几个很大的缺点:
- 拼写错误会导致没有提示的失败,导致一种正确执行了测试的假象。
- 无法在类的级别使用test命名模式。例如,命名一个类 为TestSafetyMechanisms,希望 JUnit 3 能够自动测试它的所有方法,是行不通的。
- 没有提供将参数值与程序元素关联的好方法。例如,希望支持只有在抛出特定异常时才成功的测试类别。如果将异常类型名称编码到测试方法名称中,那么代码将不好看且脆弱。
JUnit 从版本 4 开始使用注解解决了上述问题。在本条目中,我们将编写自己的示例测试框架来展示注解是如何工作的:
// Marker annotation type declaration
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
@Retention(RetentionPolicy.RUNTIME) 元注解表明测试注解在运行时生效。 @Target.get(ElementType.METHOD) 元注解表明测试注解仅对方法生效。
Test的注释说明它只能用于无参的静态方法,但实际上编译器并没有对此做强制。
下面是 Test 注解实际使用时的样子。如果程序员拼错 Test 或将 Test 注解应用于除方法声明之外的元素,程序将无法编译:
// Program containing marker annotations
public class Sample {
@Test
public static void m1() { } // Test should pass
public static void m2() { }
@Test
public static void m3() { // Test should fail
throw new RuntimeException("Boom");
}
public static void m4() { }
@Test
public void m5() { } // INVALID USE: nonstatic method
public static void m6() { }
@Test
public static void m7() { // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() { }
}
Sample 类有 7 个静态方法,其中 4 个被注解为 Test。其中两个方法 m3 和 m7 抛出异常,另外两个 m1 和 m5 没有抛出异常。但是,m5 不是静态方法,所以不是有效的用例。总之,Sample 包含四个测试用例:一个通过,两个失败,一个无效。
以下是解析并运行Test 注解标记的测试的例子:
// Program to process marker annotations
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n",passed, tests - passed);
}
}
如果在 Sample 上运行 RunTests,输出如下:
public static void Sample.m3() failed: RuntimeException: Boom
Invalid @Test: public void Sample.m5()
public static void Sample.m7() failed: RuntimeException: Crash
Passed: 1, Failed: 3
现在让我们添加一个只在抛出特定异常时才成功的测试支持。需要一个新的注解类型:
// Annotation type with a parameter
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method that
* must throw the designated exception to succeed.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
下面是这个注解的实际应用:
// Program containing annotations with a parameter
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // Should fail (no exception)
}
修改RunTests类来处理新的注解。向 main 方法添加以下代码:
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (InvocationTargetException wrappedEx) {
Throwable exc = wrappedEx.getCause();
Class<? extends Throwable> excType =m.getAnnotation(ExceptionTest.class).value();
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.printf("Test %s failed: expected %s, got %s%n",m, excType.getName(), exc);
}
}
catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
这段代码提取注解参数的值,并使用它来检查测试抛出的异常是否是正确的类型。
进一步修改异常测试示例,将允许的指定异常扩展到多个:
// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
以下是对应的测试用例:
// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class,NullPointerException.class })
public static void doublyBad() {
List<String> list = new ArrayList<>();
// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
修改RunTests类:
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
Class<? extends Exception>[] excTypes =m.getAnnotation(ExceptionTest.class).value();
for (Class<? extends Exception> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
Java 8 中可以在注解声明上使用 @Repeatable (重复注解)达到类似效果,提升程序可读性:
// Repeatable annotation type
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
使用重复注解代替数组值注解的测试用例:
// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }
为重复注解版本对应修改RunTests类:
// Processing repeatable annotations
if (m.isAnnotationPresent(ExceptionTest.class)|| m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionTest[] excTests =m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest excTest : excTests) {
if (excTest.value().isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
文章转载自: Seven