《Effective Java》笔记35-37:注解

第35条:注解优先于命名模式

没有注解之前,一般使用命名模式表明程序需要进行某些特殊处理,如:JUnit 测试框架原本要求要用 test 作为测试方法名称的开头。这种方法虽然可行,但有很多的弊端。

简单的测试

注解很好地解决了所有的问题。定义一个注解类型来指定简单的测试,它们自动运行,并在抛出异常时失败。

import java.lang.annotation.*;

// 只用于无参的静态方法,编译器无法限制,可以让测试工具解决
@Retention(RetentionPolicy.RUNTIME)  // 注解应该在运行时保留
@Target(ElementType.METHOD)  // Test 注解只在方法上声明才是合法的
public @interface Test {
}
// 测试类
public class Sample {
    @Test
    public static void m1() { }

    public static void m2() { }

    @Test
    public static void m3() { throw new RuntimeException("Boom"); }

    public static void m4() { }

    @Test
    public void m5() { }

    public static void m6() { }

    @Test
    public static void m7() { throw new RuntimeException("Crash"); }

    public static void m8() { }
}
// 测试工具
public class RunTests {
    public static void main(String[] args) throws ClassNotFoundException {
        int tests = 0;
        int passed = 0;
        Class testClass = Class.forName("Sample");
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Test.class)) {  // 指定类型的注释存在于此元素上
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException e) {
                    // 如果测试方法出现异常,会在这里
                    System.out.println(m + " failed: " + e.getCause());
                } catch (Exception e) {
                    System.out.println("INVALID @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}
// public static void Sample.m3() failed: java.lang.RuntimeException: Boom
// INVALID @Test: public void Sample.m5()
// public static void Sample.m7() failed: java.lang.RuntimeException: Crash
// Passed: 1, Failed: 3

抛出指定异常才成功的测试

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception> value();  // 某个扩展 Exception 的类的 Class 对象
}
public class Sample2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1() {
        int i = 1 / 0;
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m2() {
        int[] a = new int[0];
        int i = a[1];  // 不是指定的异常
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m3() { }  // 没有异常
}
public class RunTests2 {
    public static void main(String[] args) throws ClassNotFoundException {
        int tests = 0;
        int passed = 0;
        Class testClass = Class.forName("Sample2");
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("%s,测试失败,没有异常%n", m);
                } catch (InvocationTargetException e) {
                    Throwable exc = e.getCause();
                    Class<? extends Exception> 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 e) {
                    System.out.println("INVALID @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}
// public static void Sample2.m3(),测试失败,没有异常
// Test public static void Sample2.m2() failed: expected java.lang.ArithmeticException, got java.lang.ArrayIndexOutOfBoundsException: 1
// Passed: 1, Failed: 2

抛出任一异常

测试可以在抛出任何一种异常时都得到通过。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();  // 修改为数组形式
}
@ExceptionTest({IndexOutOfBoundsException.class, NullPointerException.class})  // 多个
public static void doublyBad() {
    List<String> list = new ArrayList<>();
    list.addAll(5, null);
}
if (m.isAnnotationPresent(ExceptionTest.class)) {
    tests++;
    try {
        m.invoke(null);
        System.out.printf("%s,测试失败,没有异常%n", m);
    } catch (InvocationTargetException e) {
        Throwable exc = e.getCause();
        Class<? extends Exception>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
        int oldPassed = passed;
        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);
        }

    } catch (Exception e) {
        System.out.println("INVALID @Test: " + m);
    }
}

第36条:坚持使用 Override 注解

可以防止错误

第37条:用标记接口定义类型

标记接口是指没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口。例如:Serializable。

标记接口优点一

标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。这样可以在编译时期捕捉更多的错误。

使用 Serializable 标记接口来指示某个类型是可序列化的。对传递给它的对象进行序列化的 ObjectOutputStream.writeObject 方法要求其参数可序列化。如果此方法的参数是 Serializable 类型,则在编译时会检测到序列化不适当对象的尝试(通过类型检查)。编译时错误检测是标记接口的意图,但不幸的是,ObjectOutputStream.writeObject API 没有利用 Serializable 接口:它的参数被声明为 Object 类型,所以尝试序列化一个不可序列化的对象直到运行时才会失败。

标记接口优点二

可以更精确地定位目标。

如果注解类型使用 @Target(ElementType.TYPE) 声明,它就可以被应用到任何类或者接口。假设有一个标记只适用于特殊接口的实现。如果将它定义成一个标记接口,就可以用它将唯一的接口扩展成它适用的接口

Set 接口可以说就是这种有限制的标记接口。它只适用于 Collection 子类型,但是它不会添加除了 Collection 定义之外的方法。一般情况下,不把它当作时标记接口,因为它改进了几个 Collection 方法的契约,包括 add,equals 和 hashCode。但是很容易想到只适用于某种特殊接口的子类型的标记接口,它没有改进接口的任何方法的契约。这种标记接口可以描述整个对象的某个约束条件,或者表明实例能够利用其它某个类的方法进行处理(就像 Serializable 接口表明实例可以通过 ObjectOutputStream 进行处理一样。

标记注解优点

可以通过默认的方式添加一个或多个注解类型元素,给已被使用的注解类型添加更多的信息。

另一个优点在于,它们是更大的注解机制的一部分。因此,标记注解允许在基于注解的框架中保持一致性。

总结

如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接口可以用来实现或者扩展接口。

如果标记只应用给类或者接口,那么问自己问题:「我想编写一个或多个只接受具有此标记的对象的方法呢?」如果是这样,则应该优先使用标记接口而不是注解。这将使你可以将接口用作相关方法的参数类型,这将带来编译时类型检查的好处。

如果永远不会想写一个只接受带有标记的对象的方法,那么最好使用标记注解。另外,如果标记是大量使用注解的框架的一部分,则标记注解是明确的选择。

  总之,标记接口和标记注释都有其用处。如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一种可行的方法。如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。如果发现自己正在编写目标为 ElementType.TYPE 的标记注解类型,那么请花时间弄清楚究竟应该用注解类型,还是标记接口更合适。

  从某种意义来说,本条目与条目 22 的的意思正好相反,条目 22 的意思是:「如果你不想定义一个类型,不要使用接口」。本条目的意思是:「如果想定义一个类型,一定要使用接口。」


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Effective Java》笔记35-37:注解

文章字数:1.9k

本文作者:Bin

发布时间:2019-06-16, 15:47:16

最后更新:2019-08-06, 00:43:22

原始链接:http://coolview.github.io/2019/06/16/Effective-Java/%E3%80%8AEffective%20Java%E3%80%8B%E7%AC%94%E8%AE%B035-37%EF%BC%9A%E6%B3%A8%E8%A7%A3/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录