Effective Java! Prefer Annotations to Naming Patterns

Effective Java Review (90 Part Series)

1 Effective Java Tuesday! Let’s Consider Static Factory Methods
2 Effective Java Tuesday! The Builder Pattern!
86 more parts…
3 Effective Java Tuesday! Singletons!
4 Effective Java Tuesday! Utility Classes!
5 Effective Java Tuesday! Prefer Dependency Injection!
6 Effective Java Tuesday! Avoid Creating Unnecessary Objects!
7 Effective Java Tuesday! Don’t Leak Object References!
8 Effective Java Tuesday! Avoid Finalizers and Cleaners!
9 Effective Java Tuesday! Prefer try-with-resources
10 Effective Java Tuesday! Obey the `equals` contract
11 Effective Java Tuesday! Obey the `hashCode` contract
12 Effective Java Tuesday! Override `toString`
13 Effective Java Tuesday! Override `clone` judiciously
14 Effective Java Tuesday! Consider Implementing `Comparable`
15 Effective Java Tuesday! Minimize the Accessibility of Classes and Member
16 Effective Java Tuesday! In Public Classes, Use Accessors, Not Public Fields
17 Effective Java Tuesday! Minimize Mutability
18 Effective Java Tuesday! Favor Composition Over Inheritance
19 Effective Java Tuesday! Design and Document Classes for Inheritance or Else Prohibit It.
20 Effective Java Tuesday! Prefer Interfaces to Abstract Classes
21 Effective Java! Design Interfaces for Posterity
22 Effective Java! Use Interfaces Only to Define Types
23 Effective Java! Prefer Class Hierarchies to Tagged Classes
24 Effective Java! Favor Static Members Classes over Non-Static
25 Effective Java! Limit Source Files to a Single Top-Level Class
26 Effective Java! Don’t Use Raw Types
27 Effective Java! Eliminate Unchecked Warnings
28 Effective Java! Prefer Lists to Array
29 Effective Java! Favor Generic Types
30 Effective Java! Favor Generic Methods
31 Effective Java! Use Bounded Wildcards to Increase API Flexibility
32 Effective Java! Combine Generics and Varargs Judiciously
33 Effective Java! Consider Typesafe Heterogenous Containers
34 Effective Java! Use Enums Instead of int Constants
35 Effective Java! Use Instance Fields Instead of Ordinals
36 Effective Java! Use EnumSet Instead of Bit Fields
37 Effective Java! Use EnumMap instead of Ordinal Indexing
38 Effective Java! Emulate Extensible Enums With Interfaces.
39 Effective Java! Prefer Annotations to Naming Patterns
40 Effective Java! Consistently Use the Override Annotation
41 Effective Java! Use Marker Interfaces to Define Types
42 Effective Java! Prefer Lambdas to Anonymous Classes
43 Effective Java! Prefer Method References to Lambdas
44 Effective Java! Favor the Use of Standard Functional Interfaces
45 Effective Java! Use Stream Judiciously
46 Effective Java! Prefer Side-Effect-Free Functions in Streams
47 Effective Java! Prefer Collection To Stream as a Return Type
48 Effective Java! Use Caution When Making Streams Parallel
49 Effective Java! Check Parameters for Validity
50 Effective Java! Make Defensive Copies When Necessary
51 Effective Java! Design Method Signatures Carefully
52 Effective Java! Use Overloading Judiciously
53 Effective Java! Use Varargs Judiciously
54 Effective Java! Return Empty Collections or Arrays, Not Nulls
55 Effective Java! Return Optionals Judiciously
56 Effective Java: Write Doc Comments For All Exposed APIs
57 Effective Java: Minimize The Scope of Local Variables
58 Effective Java: Prefer for-each loops to traditional for loops
59 Effective Java: Know and Use the Libraries
60 Effective Java: Avoid Float and Double If Exact Answers Are Required
61 Effective Java: Prefer Primitive Types to Boxed Types
62 Effective Java: Avoid Strings When Other Types Are More Appropriate
63 Effective Java: Beware the Performance of String Concatenation
64 Effective Java: Refer to Objects By Their Interfaces
65 Effective Java: Prefer Interfaces To Reflection
66 Effective Java: Use Native Methods Judiciously
67 Effective Java: Optimize Judiciously
68 Effective Java: Adhere to Generally Accepted Naming Conventions
69 Effective Java: Use Exceptions for Only Exceptional Circumstances
70 Effective Java: Use Checked Exceptions for Recoverable Conditions
71 Effective Java: Avoid Unnecessary Use of Checked Exceptions
72 Effective Java: Favor The Use of Standard Exceptions
73 Effective Java: Throw Exceptions Appropriate To The Abstraction
74 Effective Java: Document All Exceptions Thrown By Each Method
75 Effective Java: Include Failure-Capture Information in Detail Messages
76 Effective Java: Strive for Failure Atomicity
77 Effective Java: Don’t Ignore Exceptions
78 Effective Java: Synchronize Access to Shared Mutable Data
79 Effective Java: Avoid Excessive Synchronization
80 Effective Java: Prefer Executors, Tasks, and Streams to Threads
81 Effective Java: Prefer Concurrency Utilities Over wait and notify
82 Effective Java: Document Thread Safety
83 Effective Java: Use Lazy Initialization Judiciously
84 Effective Java: Don’t Depend on the Thread Scheduler
85 Effective Java: Prefer Alternatives To Java Serialization
86 Effective Java: Implement Serializable With Great Caution
87 Effective Java: Consider Using a Custom Serialized Form
88 Effective Java: Write readObject Methods Defensively
89 Effective Java: For Instance Control, Prefer Enum types to readResolve
90 Effective Java: Consider Serialization Proxies Instead of Serialized Instances

Historically library creators would use specific naming paterns to signify where functionality should be extended and as a signal for how we would like the library to interact with our code. A great example of this is prior to JUnit 4 in order to signify that a particular method was a test method; it would be prefixed by the word test. This method was not without its issues. For example a spelling mistake could lead to silent failures. If you named your method tsetSuperImportantStuff JUnit wouldn’t throw any errors and happily just ignore your test. If you weren’t paying close attention you wouldn’t notice either. With this system there is no way to indicate where a particular naming pattern is valid. For example if a user thought if they called their class TestAllTheThings it would pick up the whole class there is no way for the library to indicate to the user that this is not how to use this pattern. The final issue we are going to highlight is that there is no simple way to pass parameters to the consumer of the pattern. Imagine trying to pass an expected exception type? Something like testRunTimeExceptionThrownIsUsefulTest. Parsing out the exception type would be extremely error prone and leads to tests with not ideal names.

So what is the alternative? Annotations, that is what we can replace these patterns with. JUnit also decided to do this with the release of version 4. Let’s pretend we are making the simplest test runner possible. We would want to start with something to signify what methods should be tested.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

Enter fullscreen mode Exit fullscreen mode

We use the @interface keyword to signify that we are creating an annotation. Then we annotate our annotation with meta-annotations. The first of these @Retention(RetentionPolicy.RUNTIME) signifies that this annotation should be readable at runtime. If we didn’t add this annotation the annotation would disappear before we could read it at runtime. The second annotation @Target(ElementType.METHOD) tells the annotation that it can only be put on a method, not a class or member variable. Let’s see what this would look like in practice.

public class Sample {
  @Test
  public static void m1() { }

  public static void m2() { }

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

  @Test
  public void m4() {
    // invalid usage, not static
  }
}

Enter fullscreen mode Exit fullscreen mode

Now let’s look at an example of how to process this annotation.

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 wrappedException) {
          Throwable exception = wrappedException.getCause();
          System.out.println(m + " failed: " + exception);
        } catch (Exception exec) {
          System.out.println("Invalid @Test: " + m);
        }
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Not super straightforward code but high level this code takes in a fully-qualified class path to the test class. It then iterates over the methods of the class and checks which ones have the @Test annotation. It then invokes that method and sees if an exception is thrown. If not it iterates the passed count, if so it prints out the cause of the exception. If there is an issue with invoking the test method that is also logged out.

Now let’s consider that we want to be able to create a test that has an expected exception. We will want a new annotation to drive this.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
  Class<? extends Throwable> value();
}

Enter fullscreen mode Exit fullscreen mode

This is very similar to our previous @Test annotation but it adds the ability to pass a parameter. In this case the parameter takes a class of a class that extends from Throwable. Let’s look at an example of it’s usage.

@ExceptionTest(ArithmeticException.class)
public static void m1() {
  int i = 1 / 0;
}

Enter fullscreen mode Exit fullscreen mode

Now let’s look at how we can use this.

if (m.isAnnotationPresent(ExceptionTest.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.printf("Test %s failed: no exception%n", m);
  } catch (InvocationTargetException wrappedException) {
    Throwable exception = wrappedException.getCause();
    Class<? extends Throwable> exceptionType = m.getAnnotation(ExceptionTest.class).value();
    if(exceptionType.isInstance(exception)) {
      passed++;
    } else {
      System.out.printf("Test %s failed: expected %s, got %s%n", m, exceptionType.getName, exception);
    }
  } catch (Exception exec) {
    System.out.println("Invalid @Test: " + m);
  }
}

Enter fullscreen mode Exit fullscreen mode

This code is very similar to our @Test code runner with just some light changes to the logic to behave correctly. We do see the use of getAnnotation which allows us to grab the annotation and retrieve the value from it. Let’s say we wanted to take this code to the next level and allow any of multiple exception types to be thrown. Our annotation would only require a slight change.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
  Class<? extends Throwable>[] value();
}

Enter fullscreen mode Exit fullscreen mode

Really all we did was add the [] to signify that the parameter type is an array. The cool thing about this is that all our existing code would continue to work with this change. This is because Java will create a one element array if we just pass in a value directly like we do above. If we want to pass multiple values into a parameter that takes an array we can do so by putting them inside of { }. Let’s look at what our above example would look like:

@ExceptionTest({ArithmeticException.class, NullPointerException.class})
public static void m1() {
  int i = 1 / 0;
}

Enter fullscreen mode Exit fullscreen mode

We can also update our processing code to handle the updates pretty simply as well. Basically, inside the catch that we wrote above we would replace it’s contents with:

Throwable exception = wrappedException.getCause();
int oldPassed = passed;
Class<? extends Exception>[] exceptionTypes = m.getAnnotation(ExceptionTest.class).value();
for (Class<? extends Exception> exceptionType : exceptionTypes) {
  if (exceptionType.isInstance(exception) {
    passed++;
    break;
  }
}
if (passed == oldPassed) {
  System.out.println("Test %s failed: %s %n", m, exception);
}

Enter fullscreen mode Exit fullscreen mode

This works pretty well. However, in Java 8 another option was introduced to allow the ability of multiple values for a particular annotation. Instead of having the annotation take an array. We can put the annotation on the method multiple times. This is only possible with annotations annotated with the @Repeatable annotation that points to a container annotation type that simply holds a collection of those annotations. Let’s look at this in practice.

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

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

Enter fullscreen mode Exit fullscreen mode

And our multiple exception example would change to look like the following:

@ExceptionTest(ArithmeticException.class)
@ExceptionTest(NullPointerException.class)
public static void m1() {
  int i = 1 / 0;
}

Enter fullscreen mode Exit fullscreen mode

As you can see the user of the repeated annotation type does not see the container annotation type, that is simply for use by the processing code. The tricky part of using repeated annotations is that the code that queries the state of the annotations doesn’t always respond how you would expect. The isAnnotationPresent function will respond with true when checking against the collection annotation type when the contained repeatable annotation is used multiple times, however if it is not, it will respond with false and will only return true if you check against the contained type. Because of this we need to check for both the collection type as well as the single, contained annotation. The relevant part of our processing code would look something like:

if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.println("Test %s failed: no exception%n", m);
  } catch (Throwable wrappedException) {
    Throwable exception = wrappedException.getCause();
    int oldPassed = passed;
    ExceptionTest[] exceptionTests = m.getAnnotationByType(ExceptionTest.class);
    for (ExceptionTest exceptionTest : exceptionTests) {
      if (exceptionTest.value().isInstance(exception)) {
        passed++;
        break;
      }
    }
    if (passed == oldPassed) {
      System.out.println("Test %s failed: %s %n", m, exception);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Repeatable annotations can add a level of readability to your code but can complicate the annotation processing code you need to write. It’s up to you to know which tradeoff is worth it.

The testing framework we have started to write in this code is very much a toy but it does demonstrate some of the benefits of using annotations vs naming patterns. Anyone that has developed with me knows that I do love to use annotations. I find them powerful and a great way to add behavior and mark classes and methods. You may not find yourself writing a lot of annotations in your code but even if you don’t, understanding how they work can help you when you use them in your own code.

Effective Java Review (90 Part Series)

1 Effective Java Tuesday! Let’s Consider Static Factory Methods
2 Effective Java Tuesday! The Builder Pattern!
86 more parts…
3 Effective Java Tuesday! Singletons!
4 Effective Java Tuesday! Utility Classes!
5 Effective Java Tuesday! Prefer Dependency Injection!
6 Effective Java Tuesday! Avoid Creating Unnecessary Objects!
7 Effective Java Tuesday! Don’t Leak Object References!
8 Effective Java Tuesday! Avoid Finalizers and Cleaners!
9 Effective Java Tuesday! Prefer try-with-resources
10 Effective Java Tuesday! Obey the `equals` contract
11 Effective Java Tuesday! Obey the `hashCode` contract
12 Effective Java Tuesday! Override `toString`
13 Effective Java Tuesday! Override `clone` judiciously
14 Effective Java Tuesday! Consider Implementing `Comparable`
15 Effective Java Tuesday! Minimize the Accessibility of Classes and Member
16 Effective Java Tuesday! In Public Classes, Use Accessors, Not Public Fields
17 Effective Java Tuesday! Minimize Mutability
18 Effective Java Tuesday! Favor Composition Over Inheritance
19 Effective Java Tuesday! Design and Document Classes for Inheritance or Else Prohibit It.
20 Effective Java Tuesday! Prefer Interfaces to Abstract Classes
21 Effective Java! Design Interfaces for Posterity
22 Effective Java! Use Interfaces Only to Define Types
23 Effective Java! Prefer Class Hierarchies to Tagged Classes
24 Effective Java! Favor Static Members Classes over Non-Static
25 Effective Java! Limit Source Files to a Single Top-Level Class
26 Effective Java! Don’t Use Raw Types
27 Effective Java! Eliminate Unchecked Warnings
28 Effective Java! Prefer Lists to Array
29 Effective Java! Favor Generic Types
30 Effective Java! Favor Generic Methods
31 Effective Java! Use Bounded Wildcards to Increase API Flexibility
32 Effective Java! Combine Generics and Varargs Judiciously
33 Effective Java! Consider Typesafe Heterogenous Containers
34 Effective Java! Use Enums Instead of int Constants
35 Effective Java! Use Instance Fields Instead of Ordinals
36 Effective Java! Use EnumSet Instead of Bit Fields
37 Effective Java! Use EnumMap instead of Ordinal Indexing
38 Effective Java! Emulate Extensible Enums With Interfaces.
39 Effective Java! Prefer Annotations to Naming Patterns
40 Effective Java! Consistently Use the Override Annotation
41 Effective Java! Use Marker Interfaces to Define Types
42 Effective Java! Prefer Lambdas to Anonymous Classes
43 Effective Java! Prefer Method References to Lambdas
44 Effective Java! Favor the Use of Standard Functional Interfaces
45 Effective Java! Use Stream Judiciously
46 Effective Java! Prefer Side-Effect-Free Functions in Streams
47 Effective Java! Prefer Collection To Stream as a Return Type
48 Effective Java! Use Caution When Making Streams Parallel
49 Effective Java! Check Parameters for Validity
50 Effective Java! Make Defensive Copies When Necessary
51 Effective Java! Design Method Signatures Carefully
52 Effective Java! Use Overloading Judiciously
53 Effective Java! Use Varargs Judiciously
54 Effective Java! Return Empty Collections or Arrays, Not Nulls
55 Effective Java! Return Optionals Judiciously
56 Effective Java: Write Doc Comments For All Exposed APIs
57 Effective Java: Minimize The Scope of Local Variables
58 Effective Java: Prefer for-each loops to traditional for loops
59 Effective Java: Know and Use the Libraries
60 Effective Java: Avoid Float and Double If Exact Answers Are Required
61 Effective Java: Prefer Primitive Types to Boxed Types
62 Effective Java: Avoid Strings When Other Types Are More Appropriate
63 Effective Java: Beware the Performance of String Concatenation
64 Effective Java: Refer to Objects By Their Interfaces
65 Effective Java: Prefer Interfaces To Reflection
66 Effective Java: Use Native Methods Judiciously
67 Effective Java: Optimize Judiciously
68 Effective Java: Adhere to Generally Accepted Naming Conventions
69 Effective Java: Use Exceptions for Only Exceptional Circumstances
70 Effective Java: Use Checked Exceptions for Recoverable Conditions
71 Effective Java: Avoid Unnecessary Use of Checked Exceptions
72 Effective Java: Favor The Use of Standard Exceptions
73 Effective Java: Throw Exceptions Appropriate To The Abstraction
74 Effective Java: Document All Exceptions Thrown By Each Method
75 Effective Java: Include Failure-Capture Information in Detail Messages
76 Effective Java: Strive for Failure Atomicity
77 Effective Java: Don’t Ignore Exceptions
78 Effective Java: Synchronize Access to Shared Mutable Data
79 Effective Java: Avoid Excessive Synchronization
80 Effective Java: Prefer Executors, Tasks, and Streams to Threads
81 Effective Java: Prefer Concurrency Utilities Over wait and notify
82 Effective Java: Document Thread Safety
83 Effective Java: Use Lazy Initialization Judiciously
84 Effective Java: Don’t Depend on the Thread Scheduler
85 Effective Java: Prefer Alternatives To Java Serialization
86 Effective Java: Implement Serializable With Great Caution
87 Effective Java: Consider Using a Custom Serialized Form
88 Effective Java: Write readObject Methods Defensively
89 Effective Java: For Instance Control, Prefer Enum types to readResolve
90 Effective Java: Consider Serialization Proxies Instead of Serialized Instances

原文链接:Effective Java! Prefer Annotations to Naming Patterns

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容