Effective Java笔记第五章枚举和注解
第六节注解优先于命名模式
1.Java1.5发行之前,一般使用命名模式表示有些程序元素需要通过某种工具或者框架进行特殊处理。这种方法可行,但是有几个很严重的缺点。
1)文字拼写错误会导致失败,且没有任何提示。
2)无法确保他们只用于相应的程序元素上。
3)他们没有提供将参数值与程序元素关联起来的好方法。
2.注解很好的解决了所有这些问题,下面我们举个例子:
/**
* 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)元注解表明,Test注解应该在运行时保留。如果没有保留,测试工具就无法知道Test注解。@Target(ElementType.METHOD)元注解表明,Test注解只在方法声明中才是合法的:他不能运用到类声明,域声明或者其他程序元素上。
下面就是现实应用中的Test注解,称作标记注解,因为他没有参数,只是"标注"被注解的元素。如果程序员拼错了Test,或者将Test注解应用到程序元素而非方法声明,程序就无法编译:
public class Sample {
//无参静态方法
@Test
public static void m1() {
System.out.println("222222222");
}
public static void m2() {
}
@Test
public static void m3() {
throw new RuntimeException("Boom");
}
//无参方法
@Test
public void m4() {
System.out.println("111111111111");
}
//有参静态方法
@Test
public static void m5(Integer integer) {
integer=11;
System.out.println(integer);
}
}
一般来说,注解永远不会改变被注解代码的语义,但是使它可以通过工具进行特殊的处理,例如像这种简单的测试运行类:
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
//获取指定的类
Class<?> testClass = Class.forName("capter_five.day6.Sample");
//遍历指定类的所有方法
for (Method m : testClass.getDeclaredMethods()) {
// System.out.println(m);
//判断方法是否包含指定的注解
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
//反射运行方法
m.invoke(null);
passed++;
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
System.out.println(m + "failed" + cause);
} catch (Exception e) {
System.out.println("INVALID @Test:" + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
输出:
INVALID @Test:public void capter_five.day6.Sample.m4()
222222222
public static void capter_five.day6.Sample.m3()failedjava.lang.RuntimeException: Boom
INVALID @Test:public static void capter_five.day6.Sample.m5(java.lang.Integer)
Passed: 1, Failed: 3
如果尝试通过反射调用测试方法时抛出InvocationTargetException之外的任何异常,表明编译时没有捕捉到Test注解的无效用法。这种用法包括实例方法的注解,或者带有一个或者多个参数的方法的注解,或者不可访问的方法的注解。
3.现在我们要针对只在抛出特殊异常时才成功的测试添加支持。为此我们需要一个新的注解类型:
/**
*Indicates that the annotated method is a test method 指示带注释的方法是一个测试方法
* must throw the designated exception to succeed 只有抛出指定的异常才能成功
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
这个注解的参数类型是Class<? extends Exception>。他表示:某个扩展Exception的类的Class对象,它允许注解的用户指定任何异常类型。这种用法是有限制的类型令牌的一个示例。
下面我们对上面的注解进行应用:
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() {
int i = 0;
i = i / i;
System.out.println("1111111");
}
@ExceptionTest(ArithmeticException.class)
public static void m2() {
int[] a = new int[0];
int i = a[1];
System.out.println("222222");
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {
System.out.println("333333");
}
}
我们修改一下测试运行工具来处理新的注解:
public class RunTests2 {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("capter_five.day6.Sample2");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (InvocationTargetException e) {
Throwable exc = e.getCause();
Class<? extends Exception> value = m.getAnnotation(ExceptionTest.class).value();
if (value.isInstance(exc)) {
passed++;
} else {
System.out.printf("Test %s failed:expected %s ,got %s%n", m, value.getName(), exc);
}
} catch (Exception e) {
System.out.println("INVALID @Test:" + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
输出:
333333
Test public static void capter_five.day6.Sample2.m3() failed: no exception
Test public static void capter_five.day6.Sample2.m2() failed:expected java.lang.ArithmeticException ,got java.lang.ArrayIndexOutOfBoundsException: 1
Passed: 1, Failed: 2
4.将上面的异常测试示例再深入一点,想像测试可以在抛出多种指定异常时都能得到通过,假设我们将ExceptionTest 注解的参数类型改为Class对象的一个数组:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest2 {
Class<? extends Exception>[] value();
}
public class Sample3 {
@ExceptionTest2({IndexOutOfBoundsException.class,NullPointerException.class})
public static void m1() {
int i = 0;
i = i / i;
System.out.println("1111111");
}
@ExceptionTest2({IndexOutOfBoundsException.class,NullPointerException.class})
public static void m2() {
int[] a = new int[0];
int i = a[1];
System.out.println("222222");
}
@ExceptionTest2({IndexOutOfBoundsException.class,NullPointerException.class})
public static void m3() {
System.out.println("333333");
}
}
public class RunTests3 {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("capter_five.day6.Sample3");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest2.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (InvocationTargetException e) {
Throwable exc = e.getCause();
Class<? extends Exception>[] value = m.getAnnotation(ExceptionTest2.class).value();
int oldPassed = passed;
for (Class<? extends Exception> aClass : value) {
if (aClass.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed) {
System.out.printf("Test %s failed: %s %n",m,exc);
}
} catch (Exception e) {
System.out.println("INVALID @Test:" + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
输出:
333333
Test public static void capter_five.day6.Sample3.m3() failed: no exception
Test public static void capter_five.day6.Sample3.m1() failed: java.lang.ArithmeticException: / by zero
Passed: 1, Failed: 2
5.如果是在编写一个需要程序员给源文件添加信息的工具,就要定义一组适当的注解类型。既然有了注解,就完全没有理由再使用命名模式了。
所有的程序员都应该使用Java平台所提供的预定义的注解类型,还要考虑使用IDE或者静态分析工具所提供的任何注解。这种注解可以提升由这些工具所提供的诊断信息的质量。