[JAVA] Item 39: Prefer annotations to naming patterns

39. Choose annotations from naming patterns

Tools and frameworks have often changed their treatment depending on the ** naming pattern **. For example, in JUnit prior to version 4, the test method name must start with test. This method has some drawbacks. If you make a typo, you will implicitly fail. For example, if you type tsetSafetyOverride instead of testSafetyOverride, JUnit3 will not fail, but the test will not run. Also, there is no way to guarantee that the naming pattern is used only for the proper program elements. Another drawback is that there is no good way to associate the argument value with the program element.

Annotations eliminate these drawbacks (annotations have been introduced since JUnit 4). In this chapter, we'll create a simple testing framework to see how annotations work. First, let's look at the annotation that works automatically and fails if an error is sent.

/**
 * Indicates that the annotated method is a test method.
 * Use only on parameterless static methods.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

In the definition of Test annotation, `@ Retention``` and `@ Target``` are added, but these are called meta annotations.

@Retention(RetentionPolicy.RUNTIME)Indicates that the Test annotation is retained at run time.



#### **`@Target(ElementType.METHOD)Indicates that the Test annotation is valid only in method declarations (not applicable to fields or classes).`**
```METHOD)Indicates that the Test annotation is valid only in method declarations (not applicable to fields or classes).

 The comment in the Test annotation declaration says "Use only on parameterless static methods", but this cannot be detected by the compiler unless you write an annotation processor. See the `` `javax.annotation.processing``` documentation for more information.
 The following shows how the Test annotation is actually used.

```java
// 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() { }
}

Test annotations are called ** marker annotations ** because they have no arguments and simply mark the element. If you type Test or use Test annotations other than method declarations, you will get a compile error. sampleOf the classes, m1 succeeds, m3 and m7 fail, m5 is invalid because it is not a static method, and the other four methods are not annotated with the test and are ignored by the testing framework. The Test annotation does not directly affect the semantics of the Sample class, it only gives information to programs that use the Test annotation. That is, annotations do not affect the semantics of the given code, but the processing of tools such as the sample test runner below.

package tryAny.effectiveJava;

//Program to process marker annotations
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

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);
    }

}

The test runner tool takes the FQDN as an argument and executes the method with the Test annotation by calling `Method.invoke```. If the test method throws an exception, the reflection facility wraps the exception and throws a ```InvocationTargetException```. This tool catches this exception and gets the cause exception with the getCause``` method. The second catch clause catches the exception due to the abuse of the Test annotation and prints out the appropriate message. The output when the `` Sample``` class is applied to the test tool is as follows.

public static void tryAny.effectiveJava.Sample.m3() failed: java.lang.RuntimeException: Boom
Invalid @Test: public void tryAny.effectiveJava.Sample.m5()
public static void tryAny.effectiveJava.Sample.m7() failed: java.lang.RuntimeException: Crash
Passed: 1, Failed: 3

Now, let's create a new annotation that will succeed if a specific exception is thrown.

package tryAny.effectiveJava;

//Annotation type with a parameter
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 *  * 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();
}

A bounded token (Item33) called Class <? Extends Throwable>` `` is used as an argument of this annotation, and a class that inherits `` `Throwable can be taken as an argument. Means. The following are specific examples of use.

package tryAny.effectiveJava;

//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)
}

In addition, the test runner tool will be modified as follows to match this.

package tryAny.effectiveJava;

//Program to process marker annotations
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

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);
                }
            }

            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);
                }
            }

        }

        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }

}

This code extracts the arguments of the ExceptionTest annotation and checks if the exception raised is of the same type. Compiling the test program indicates that the exception type specified as an annotation argument is valid. However, the test runner throws a `` `TypeNotPresentException``` if the particular exception type that was at compile time is not at runtime.

We've further improved the code for this exception test to allow you to test if any of the specified exceptions are thrown. This improvement can be easily done by making the argument type of ExceptionTest an array.

// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}

Now the ExceptionTest annotation can take either one argument or multiple arguments. When describing multiple exceptions, it is as follows.

// 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);
}

exceptiontestWith the new version, the test runner tool looks like this:

package tryAny.effectiveJava;

//Program to process marker annotations
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

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);
                }
            }

            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();
                    int oldPasses = passed;
                    Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
                    for (Class<? extends Throwable> excType : excTypes) {
                        if (excType.isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPasses) {
                        System.out.printf("Test %s failed: %s%n", m, exc);
                    }
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }

        }

        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }

}

In Java8, annotations that take multiple arguments can be implemented in different ways. Instead of declaring annotations with array arguments, use `` `@ Repeatablemeta annotations so that one element can be annotated multiple times. This @Repeatable``` meta annotation takes one argument. Its argument is a class object that has one array called containing annotation type as an argument. The following is an example.

package tryAny.effectiveJava;

//Annotation type with a parameter
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 *  * Indicates that the annotated method is a test method that  * must throw
 * the designated exception to succeed.  
 */

// Repeatable annotation type
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Exception> value();

}
package tryAny.effectiveJava;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}

The method of annotating is as follows.

// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }

@repeatableCare must be taken when processing. Annotations that are used repeatedly are created in combination with annotations that store them. getannotationsbytypeThe method hides this and gives access to both annotations that are used repeatedly and annotations that are not. On the other handisannotationspresentThe method determines that the annotation that is used repeatedly is the annotation that stores it. That is, the type of annotation that is used repeatedlyisannotationspresentThe method can't determine that it's a recurring type. In order to detect both annotations that are used repeatedly and annotations that are not used, it is necessary to check both the stored annotation and the element annotation. The following is a rewrite to withstand repeated use of annotations.

// 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);
    }
}

@repeatableYou should use it if you feel that using it enhances readability, but you should keep in mind that using it complicates the processing of repetitive annotations.

Although the testing framework in this chapter is simple, it has shown an advantage over the naming pattern of using annotations.

Most programmers do not define annotations themselves. You should use standard Java annotations and annotations provided by IDEs and tools.

Recommended Posts

Item 39: Prefer annotations to naming patterns
Item 28: Prefer lists to arrays
Item 65: Prefer interfaces to reflection
Item 43: Prefer method references to lambdas
Item 42: Prefer lambdas to anonymous classes
Item 85: Prefer alternatives to Java serialization
Item 68: Adhere to generally accepted naming conventions
Item 58: Prefer for-each loops to traditional for loops
Item 23: Prefer class hierarchies to tagged classes
Item 61: Prefer primitive types to boxed primitives
Item 81: Prefer concurrency utilities to wait and notify
Item 80: Prefer executors, tasks, and streams to threads
Item 47: Prefer Collection to Stream as a return type
Introduction to design patterns (introduction)
Introduction to Design Patterns (Builder)
Introduction to Design Patterns (Composite)
Introduction to design patterns (Flyweight)
Introduction to design patterns Prototype
Introduction to Design Patterns (Iterator)
Introduction to Design Patterns (Strategy)