38. 使用接口模拟可扩展的枚举
在几乎所有方面,枚举类型都优于本书第一版中描述的类型安全模式[Bloch01]。 从表面上看,一个例外涉及可扩展性,这在原始模式下是可能的,但不受语言结构支持。 换句话说,使用该模式,有可能使一个枚举类型扩展为另一个; 使用语言功能特性,它不能这样做。 这不是偶然的。 大多数情况下,枚举的可扩展性是一个糟糕的主意。 令人困惑的是,扩展类型的元素是基类型的实例,反之亦然。 枚举基本类型及其扩展的所有元素没有好的方法。 最后,可扩展性会使设计和实现的很多方面复杂化。
也就是说,对于可扩展枚举类型至少有一个有说服力的用例,这就是操作码( operation codes),也称为opcodes。 操作码是枚举类型,其元素表示某些机器上的操作,例如条目 34中的Operation
类型,它表示简单计算器上的功能。 有时需要让API的用户提供他们自己的操作,从而有效地扩展API提供的操作集。
幸运的是,使用枚举类型有一个很好的方法来实现这种效果。基本思想是利用枚举类型可以通过为opcode类型定义一个接口,并实现任意接口。例如,这里是来自条目 34的Operation
类型的可扩展版本:
// 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; } } 复制代码
虽然枚举类型(BasicOperation
)不可扩展,但接口类型(Operation
)是可以扩展的,并且它是用于表示API中的操作的接口类型。 你可以定义另一个实现此接口的枚举类型,并使用此新类型的实例来代替基本类型。 例如,假设想要定义前面所示的操作类型的扩展,包括指数运算和余数运算。 你所要做的就是编写一个实现Operation
接口的枚举类型:
// 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; } } 复制代码
只要API编写为接口类型(Operation
),而不是实现(BasicOperation
),现在就可以在任何可以使用基本操作的地方使用新操作。请注意,不必在枚举中声明apply
抽象方法,就像您在具有实例特定方法实现的非扩展枚举中所做的那样(第162页)。 这是因为抽象方法(apply
)是接口(Operation
)的成员。
不仅可以在任何需要“基本枚举”的地方传递“扩展枚举”的单个实例,而且还可以传入整个扩展枚举类型,并使用其元素。 例如,这里是第163页上的一个测试程序版本,它执行之前定义的所有扩展操作:
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)); } 复制代码
注意,扩展的操作类型的类字面文字(ExtendedOperation.class
)从main
方法里传递给了test
方法,用来描述扩展操作的集合。这个类的字面文字用作限定的类型令牌(条目 33)。opEnumType
参数中复杂的声明(<T extends Enum<T> & Operation> Class<T>
)确保了Class对象既是枚举又是Operation
的子类,这正是遍历元素和执行每个元素相关联的操作时所需要的。
第二种方式是传递一个Collection<? extends Operation>
,这是一个限定通配符类型(条目 31),而不是传递了一个class对象:
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)); } 复制代码
生成的代码稍微不那么复杂,tes
t方法灵活一点:它允许调用者将多个实现类型的操作组合在一起。另一方面,也放弃了在指定操作上使用EnumSe
t(条目 36)和EnumMap
(条目 37)的能力。
上面的两个程序在运行命令行输入参数4和2时生成以下输出:
4.000000 ^ 2.000000 = 16.000000 4.000000 % 2.000000 = 0.000000 复制代码
使用接口来模拟可扩展枚举的一个小缺点是,实现不能从一个枚举类型继承到另一个枚举类型。如果实现代码不依赖于任何状态,则可以使用默认实现(条目 20)将其放置在接口中。在我们的Operation
示例中,存储和检索与操作关联的符号的逻辑必须在BasicOperation
和ExtendedOperation
中重复。在这种情况下,这并不重要,因为很少的代码是冗余的。如果有更多的共享功能,可以将其封装在辅助类或静态辅助方法中,以消除代码冗余。
该条目中描述的模式在Java类库中有所使用。例如,java.nio.file.LinkOption
枚举类型实现了CopyOption
和OpenOption
接口。
总之,虽然不能编写可扩展的枚举类型,但是你可以编写一个接口来配合实现接口的基本的枚举类型,来对它进行模拟。这允许客户端编写自己的枚举(或其它类型)来实现接口。如果API是根据接口编写的,那么在任何使用基本枚举类型实例的地方,都可以使用这些枚举类型实例。
39. 注解优于命名模式
过去,通常使用命名模式( naming patterns)来指示某些程序元素需要通过工具或框架进行特殊处理。 例如,在第4版之前,JUnit测试框架要求其用户通过以test[Beck04]开始名称来指定测试方法。 这种技术是有效的,但它有几个很大的缺点。 首先,拼写错误导致失败,但不会提示。 例如,假设意外地命名了测试方法tsetSafetyOverride
而不是testSafetyOverride
。 JUnit 3不会报错,但它也不会执行测试,导致错误的安全感。
命名模式的第二个缺点是无法确保它们仅用于适当的程序元素。 例如,假设调用了TestSafetyMechanisms
类,希望JUnit 3能够自动测试其所有方法,而不管它们的名称如何。 同样,JUnit 3也不会出错,但它也不会执行测试。
命名模式的第三个缺点是它们没有提供将参数值与程序元素相关联的好的方法。 例如,假设想支持只有在抛出特定异常时才能成功的测试类别。 异常类型基本上是测试的一个参数。 你可以使用一些精心设计的命名模式将异常类型名称编码到测试方法名称中,但这会变得丑陋和脆弱(条目 62)。 编译器无法知道要检查应该命名为异常的字符串是否确实存在。 如果命名的类不存在或者不是异常,那么直到尝试运行测试时才会发现。
注解[JLS,9.7]很好地解决了所有这些问题,JUnit从第4版开始采用它们。在这个项目中,我们将编写我们自己的测试框架来显示注解的工作方式。 假设你想定义一个注解类型来指定自动运行的简单测试,并且如果它们抛出一个异常就会失败。 以下是名为Test
的这种注解类型的定义:
// 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 { } 复制代码
Test注解类型的声明本身使用Retention
和Target
注解进行标记。 注解类型声明上的这种注解称为元注解。 @Retention(RetentionPolicy.RUNTIME)
元注解指示Test注解应该在运行时保留。 没有它,测试工具就不会看到Test注解。@Target.get(ElementType.METHOD)
元注解表明Test注解只对方法声明合法:它不能应用于类声明,属性声明或其他程序元素。
在Test注解声明之前的注释说:“仅在无参静态方法中使用”。如果编译器可以强制执行此操作是最好的,但它不能,除非编写注解处理器来执行此操作。 有关此主题的更多信息,请参阅javax.annotation.processing
文档。 在缺少这种注解处理器的情况下,如果将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
类有七个静态方法,其中四个被标注为Test。 其中两个,m3和m7引发异常,两个m1和m5不引发异常。 但是没有引发异常的注解方法之一是实例方法,因此它不是注释的有效用法。 总之,Sample
包含四个测试:一个会通过,两个会失败,一个是无效的。 未使用Test注解标注的四种方法将被测试工具忽略。
Test注解对Sample类的语义没有直接影响。 他们只提供信息供相关程序使用。 更一般地说,注解不会改变注解代码的语义,但可以通过诸如这个简单的测试运行器等工具对其进行特殊处理:
// 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); } } 复制代码
测试运行器工具在命令行上接受完全限定的类名,并通过调用Method.invoke
来反射地运行所有类标记有Test注解的方法。 isAnnotationPresent
方法告诉工具要运行哪些方法。 如果测试方法引发异常,则反射机制将其封装在InvocationTargetException
中。 该工具捕获此异常并打印包含由test方法抛出的原始异常的故障报告,该方法是使用getCause
方法从InvocationTargetException
中提取的。
如果尝试通过反射调用测试方法会抛出除InvocationTargetException
之外的任何异常,则表示编译时未捕获到没有使用的Test注解。 这些用法包括注解实例方法,具有一个或多个参数的方法或不可访问的方法。 测试运行器中的第二个catch块会捕获这些Test使用错误并显示相应的错误消息。 这是在RunTests
在Sample
上运行时打印的输出:
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(); } 复制代码
此注解的参数类型是Class<? extends Throwable>
。毫无疑问,这种通配符是拗口的。 在英文中,它表示“扩展Throwable的某个类的Class对象”,它允许注解的用户指定任何异常(或错误)类型。 这个用法是一个限定类型标记的例子(条目 33)。 以下是注解在实践中的例子。 请注意,类名字被用作注解参数值:
// 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) } 复制代码
现在让我们修改测试运行器工具来处理新的注解。 这样将包括将以下代码添加到买呢方法中:
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); } } 复制代码
此代码与我们用于处理Test注解的代码类似,只有一个例外:此代码提取注解参数的值并使用它来检查测试引发的异常是否属于正确的类型。 没有明确的转换,因此没有ClassCastException
的危险。 测试程序编译的事实保证其注解参数代表有效的异常类型,但有一点需要注意:如果注解参数在编译时有效,但代表指定异常类型的类文件在运行时不再存在,则测试运行器将抛出TypeNotPresentException
异常。
将我们的异常测试示例进一步推进,可以设想一个测试,如果它抛出几个指定的异常中的任何一个,就会通过测试。 注解机制有一个便于支持这种用法的工具。 假设我们将ExceptionTest
注解的参数类型更改为Class对象数组:
// Annotation type with an array parameter @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Exception>[] value(); } 复制代码
注解中数组参数的语法很灵活。 它针对单元素数组进行了优化。 所有以前的ExceptionTest
注解仍然适用于ExceptionTest的新数组参数版本,并且会生成单元素数组。 要指定一个多元素数组,请使用花括号将这些元素括起来,并用逗号分隔它们:
// 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); } 复制代码
修改测试运行器工具以处理新版本的ExceptionTest
是相当简单的。 此代码替换原始版本:
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
元注解来标示注解的声明,而不用使用数组参数声明注解类型,以指示注解可以重复应用于单个元素。 该元注解采用单个参数,该参数是包含注解类型的类对象,其唯一参数是注解类型[JLS,9.6.3]的数组。 如果我们使用ExceptionTest
注解采用这种方法,下面是注解的声明。 请注意,包含注解类型必须使用适当的保留策略和目标进行注解,否则声明将无法编译:
// 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(); } 复制代码
下面是我们的doublyBad
测试用一个重复的注解代替基于数组值注解的方式:
// Code containing a repeated annotation @ExceptionTest(IndexOutOfBoundsException.class) @ExceptionTest(NullPointerException.class) public static void doublyBad() { ... } 复制代码
处理可重复的注解需要注意。重复注解会生成包含注解类型的合成注解。 getAnnotationsByType
方法掩盖了这一事实,可用于访问可重复注解类型和非重复注解。但isAnnotationPresent
明确指出重复注解不是注解类型,而是包含注解类型。如果某个元素具有某种类型的重复注解,并且使用isAnnotationPresent
方法检查元素是否具有该类型的注释,则会发现它没有。使用此方法检查注解类型的存在会因此导致程序默默忽略重复的注解。同样,使用此方法检查包含的注解类型将导致程序默默忽略不重复的注释。要使用isAnnotationPresent
检测重复和非重复的注解,需要检查注解类型及其包含的注解类型。以下是RunTests程序的相关部分在修改为使用ExceptionTest注解的可重复版本时的例子:
// 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); } } 复制代码
添加了可重复的注解以提高源代码的可读性,从逻辑上将相同注解类型的多个实例应用于给定程序元素。 如果觉得它们增强了源代码的可读性,请使用它们,但请记住,在声明和处理可重复注解时存在更多的样板,并且处理可重复的注解很容易出错。
这个项目中的测试框架只是一个演示,但它清楚地表明了注解相对于命名模式的优越性,而且它仅仅描绘了你可以用它们做什么的外观。 如果编写的工具要求程序员将信息添加到源代码中,请定义适当的注解类型。当可以使用注解代替时,没有理由使用命名模式。
这就是说,除了特定的开发者(toolsmith)之外,大多数程序员都不需要定义注解类型。 但所有程序员都应该使用Java提供的预定义注解类型(条目40,27)。 另外,请考虑使用IDE或静态分析工具提供的注解。 这些注解可以提高这些工具提供的诊断信息的质量。 但请注意,这些注解尚未标准化,因此如果切换工具或标准出现,可能额外需要做一些工作。
40. 始终使用Override注解
Java类库包含几个注解类型。对于典型的程序员来说,最重要的是@Override
。此注解只能在方法声明上使用,它表明带此注解的方法声明重写了父类的声明。如果始终使用这个注解,它将避免产生大量的恶意bug。考虑这个程序,在这个程序中,类Bigram
表示双字母组合,或者是有序的一对字母:
// Can you spot the bug? public class Bigram { private final char first; private final char second; public Bigram(char first, char second) { this.first = first; this.second = second; } public boolean equals(Bigram b) { return b.first == first && b.second == second; } public int hashCode() { return 31 * first + second; } public static void main(String[] args) { Set<Bigram> s = new HashSet<>(); for (int i = 0; i < 10; i++) for (char ch = 'a'; ch <= 'z'; ch++) s.add(new Bigram(ch, ch)); System.out.println(s.size()); } } 复制代码
主程序重复添加二十六个双字母组合到集合中,每个双字母组合由两个相同的小写字母组成。 然后它会打印集合的大小。 你可能希望程序打印26,因为集合不能包含重复项。 如果你尝试运行程序,你会发现它打印的不是26,而是260。它有什么问题?
显然,Bigram
类的作者打算重写equals
方法(条目 10),甚至记得重写hashCode
(条目 11)。 不幸的是,我们倒霉的程序员没有重写equals
,而是重载它(条目 52)。 要重写Object.equals,必须定义一个equals方法,其参数的类型为Object,但Bigram
的equals方法的参数不是Object类型的,因此Bigram
继承Object的equals方法,这个equals方法测试对象的引用是否是同一个,就像==运算符一样。 每个祖母组合的10个副本中的每一个都与其他9个副本不同,所以它们被Object.equals视为不相等,这就解释了程序打印260的原因。
幸运的是,编译器可以帮助你找到这个错误,但只有当你通过告诉它你打算重写Object.equals来帮助你。 要做到这一点,用@Override
注解Bigram.equals方法,如下所示:
@Override public boolean equals(Bigram b) { return b.first == first && b.second == second; } 复制代码
如果插入此注解并尝试重新编译该程序,编译器将生成如下错误消息:
Bigram.java:10: method does not override or implement a method from a supertype @Override public boolean equals(Bigram b) { ^ 复制代码
你会立刻意识到你做错了什么,在额头上狠狠地打了一下,用一个正确的(条目 10)来替换出错的equals实现:
@Override public boolean equals(Object o) { if (!(o instanceof Bigram)) return false; Bigram b = (Bigram) o; return b.first == first && b.second == second; } 复制代码
因此,应该在你认为要重写父类声明的每个方法声明上使用Override
注解。 这条规则有一个小例外。 如果正在编写一个没有标记为抽象的类,并且确信它重写了其父类中的抽象方法,则无需将Override
注解放在该方法上。 在没有声明为抽象的类中,如果无法重写抽象父类方法,编译器将发出错误消息。 但是,你可能希望关注类中所有重写父类方法的方法,在这种情况下,也应该随时注解这些方法。 大多数IDE可以设置为在选择重写方法时自动插入Override
注解。
大多数IDE提供了是种使用Override
注解的另一个理由。 如果启用适当的检查功能,如果有一个方法没有Override
注解但是重写父类方法,则IDE将生成一个警告。 如果始终使用Override
注解,这些警告将提醒你无意识的重写。 它们补充了编译器的错误消息,这些消息会提醒你无意识重写失败。 IDE和编译器,可以确保你在任何你想要的地方和其他地方重写方法,万无一失。
Override
注解可用于重写来自接口和类的方法声明。 随着default默认方法的出现,在接口方法的具体实现上使用Override
以确保签名是正确的是一个好习惯。 如果知道某个接口没有默认方法,可以选择忽略接口方法的具体实现上的Override
注解以减少混乱。
然而,在一个抽象类或接口中,值得标记的是你认为重写父类或父接口方法的所有方法,无论是具体的还是抽象的。 例如,Set接口不会向Collection接口添加新方法,因此它应该在其所有方法声明中包含Override
注解以确保它不会意外地向Collection接口添加任何新方法。
总之,如果在每个方法声明中使用Override
注解,并且认为要重写父类声明,那么编译器可以保护免受很多错误的影响,但有一个例外。 在具体的类中,不需要注解标记你确信可以重写抽象方法声明的方法(尽管这样做也没有坏处)。
41.使用标记接口定义类型
标记接口(marker interface),不包含方法声明,只是指定(或“标记”)一个类实现了具有某些属性的接口。 例如,考虑Serializable
接口(第12章)。 通过实现这个接口,一个类表明它的实例可以写入ObjectOutputStream
(或“序列化”)。
你可能会听说过标记注解(条目 39)标记一个接口是废弃过时的。 这个断言是不正确的。 标记接口与标记注解相比具有两个优点。 首先,标记接口定义了一个由标记类实例实现的类型;标记注解则不会。 标记接口类型的存在允许在编译时捕获错误,如果使用标记注解,则直到运行时才能捕获错误。
Java的序列化机制(第6章)使用Serializable
标记接口来指示某个类型是可序列化的。 对传递给它的对象进行序列化的ObjectOutputStream.writeObject
方法要求其参数可序列化。 如果此方法的参数是Serializable
类型,则在编译时会检测到序列化不适当对象的尝试(通过类型检查)。 编译时错误检测是标记接口的意图,但不幸的是,ObjectOutputStream.write
API没有利用Serializable
接口:它的参数被声明为Object类型,所以尝试序列化一个不可序列化的对象直到运行时才会失败。
标记接口对于标记注解的另一个优点是可以更精确地定位目标。 如果使用目标ElementType.TYPE
声明注解类型,它可以应用于任何类或接口。 假设有一个标记仅适用于特定接口的实现。 如果将其定义为标记接口,则可以扩展它适用的唯一接口,保证所有标记类型也是适用的唯一接口的子类型。
可以说,Set接口就是这样一个受限的标记接口。 它仅适用于Collection子类型,但不会添加超出Collection定义的方法。 它通常不被认为是标记接口,因为它改进了几个Collection方法的契约,包括add,equals和hashCode。 但很容易想象一个标记接口,它仅适用于某些特定接口的子类型,并且不会改进任何接口方法的契约。 这样的标记接口可以描述整个对象的一些约束条件(invariant),或者说明实例有资格被某个其他类的方法处理(就像Serializable
接口指示实例有资格被ObjectOutputStream
处理的方式)。
标记注解优于标记接口的主要优点是它们是较大的注解工具的一部分。因此,标记注解允许在基于注解的框架中保持一致性。
所以什么时候应该使用标记注解,什么时候应该使用标记接口?显然,如果标记适用于除类或接口以外的任何程序元素,则必须使用注解,因为只能使用类和接口来实现或扩展接口。如果标记仅适用于类和接口,那么问自己问题:“可能我想编写一个或多个只接受具有此标记的对象的方法呢?”如果是这样,则应该优先使用标记接口而不是注解。这将使你可以将接口用作所讨论方法的参数类型,这将带来编译时类型检查的好处。如果你能说服自己,永远不会想写一个只接受带有标记的对象的方法,那么最好使用标记注解。另外,如果标记是大量使用注解的框架的一部分,则标记注解是明确的选择。
总之,标记接口和标记注释都有其用处。 如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一种可行的方法。 如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。 如果发现自己正在编写目标为ElementType.TYPE
的标记注解类型,请花点时间确定它是否应该是注释类型,是不是标记接口是否更合适。
从某种意义来说,本条目与条目22的的意思正好相反,条目22的意思是:“如果你不想定义一个类型,不要使用接口”。本条目的意思是:如果想定义一个类型,一定要使用接口。