<

I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

In this article, we’ll talk about a core feature of the Java language – the default annotations available in the JDK.

2. What an Annotation Is

Simply put, annotations are Java types that are preceded by an “@” symbol.

Java has had annotations ever since the 1.5 release. Since then, they’ve shaped the way we’ve designed our applications.

Spring and Hibernate are great examples of frameworks that rely heavily on annotations to enable various design techniques.

Basically, an annotation assigns extra metadata to the source code it’s bound to. By adding an annotation to a method, interface, class, or field, we can:

  1. Inform the compiler about warnings and errors
  2. Manipulate source code at compilation time
  3. Modify or examine behavior at runtime

3. Java Built-in Annotations

Now that we’ve reviewed the basics, let’s take a look at some annotations that ship with core Java. First, there are several that inform compilation:

  1. @Override
  2. @SuppressWarnings
  3. @Deprecated
  4. @SafeVarargs
  5. @FunctionalInterface

These annotations generate or suppress compiler warnings and errors. Applying them consistently is often a good practice since adding them can prevent future programmer error.

3.1. @Override

In a subclass, we can override or overload instance methods. Overriding indicates that the subclass is replacing inherited behavior. Overloading is when a subclass is adding new behavior.

Sometimes, we’ll overload by accident when we actually intended to override. It’s easy to make this mistake in Java:

public class Machine {
    public boolean equals(Machine obj) {
        return true;
    }

    @Test
    public void whenTwoDifferentMachines_thenReturnTrue() {
        Object first = new Machine();
        Object second = new Machine();
        assertTrue(first.equals(second));
    }
}

Surprisingly, the test above fails. This is because this equals method is overloading Object#equals, not overriding it.

We can use the @Override annotation on inherited methods to protect us from this mistake.

In this example, we can add the @Override annotation above the equals method:

@Override
public boolean equals(Machine obj) {
    return true;
}

At this point, the compiler will raise an error, informing us that we aren’t overriding equals like we think.

Then, we can correct our mistake:

@Override
public boolean equals(Object obj) {
    return true;
}

Because of how easy it’s to accidentally overload, it’s a common recommendation to use the @Override annotation on all inherited methods.

3.2. @SuppressWarnings

Compiler warning messages are usually helpful. Sometimes warnings can get noisy, though.

Especially when we can’t or don’t want to address them:

public class Machine {
    private List versions;

    public void addVersion(String version) {
        versions.add(version);
    }
}

The compiler will issue a warning about this method. It’ll warn that we’re using a raw-typed collection. If we don’t want to fix the warning, then we can suppress it with the @SuppressWarnings annotation.

This annotation allows us to say which kinds of warnings to ignore. While warning types can vary by compiler vendor, the two most common are deprecation and unchecked.

deprecation tells the compiler to ignore when we’re using a deprecated method or type.

unchecked tells the compiler to ignore when we’re using raw types.

So, in our example above, we can suppress the warning associated with our raw type usage:

public class Machine {
    private List versions;

    @SuppressWarnings("unchecked")
    // or
    @SuppressWarnings({"unchecked"})
    public void addVersion(String version) {
        versions.add(version);
    }
}

To suppress a list of multiple warnings, we set a String array containing the corresponding warning list:

@SuppressWarnings({"unchecked", "deprecated"})

3.3. @Deprecated

As a project evolves its API changes. Over time, there’re certain constructors, fields, types or methods that we don’t want people to use anymore.

Instead of breaking the backward compatibility of the project’s API, we can tag these elements with the @Deprecated annotation.

@Deprecated tells other developers that the marked element should no longer be used. It’s common to have a Javadoc accompany the @Deprecated annotation to explain what would be a better alternative that serves the right behavior:

public class Worker {
    /**
     * Calculate period between versions
     * @deprecated
     * This method is no longer acceptable to compute time between versions.
     * <p> Use {@link Utils#calculatePeriod(Machine)} instead.
     *
     * @param machine instance
     * @return computed time
     */
    @Deprecated
    public int calculate(Machine machine) {
        return machine.exportVersions().size() * 10;
    }
}

Remember that a compiler only displays the deprecated API warning if the given Java element is used somewhere in the code. So, in this case, it’d only show if there was code that called the calculate method.

Also, we can communicate deprecated status in documentation as well by using the Javadoc @deprecated tag in the method description.

3.4. @SafeVarargs

Java 5 introduced the concept of varargs, or a method parameter of variable length, as well as parameterized types.

Combining these can cause problems for us:

public static <T> T[] unsafe(T... elements) {
    return elements; // unsafe! don't ever return a parameterized varargs array
}

public static <T> T[] broken(T seed) {
    T[] plant = unsafe(seed, seed, seed); // broken! This will be an Object[] no matter what T is
    return plant;
}

public static void plant() {
   String[] plants = broken("seed"); // ClassCastException
}

These problems are tricky for a compiler to confirm, and so it gives warnings whenever the two are combined, like in the case of unsafe:

warning: [unchecked] Possible heap pollution from parameterized vararg type T
  public static <T> T[] unsafe(T... elements) {

This method, if used incorrectly, like in the case of broken, will pollute an Object[] array into the heap instead of the intended type b.

To squash this warning, we can add the @SafeVarargs annotation on final or static methods and constructors.

@SafeVarargs is like @SupressWarnings in that it allows us to declare that a particular compiler warning is a false positive. Once we ensure our actions are safe, we can add this annotation:

public class Machine<T> {
    private List<T> versions = new ArrayList<>();

    @SafeVarargs
    public final List<T> safe(T... toAdd) {
        for (T version : toAdd) {
            versions.add(version);
        }
    }
}

Safe use of varargs is a tricky concept in and of itself. For more information, Josh Bloch has a great explanation in his book, Effective Java.

3.5. @FunctionalInterface

Java 8 allows us to write code in a more functional way.

Single Abstract Method interfaces are a big part of this. If we intend a SAM interface to be used by lambdas, we can optionally mark it as such with @FunctionalInterface:

@FunctionalInterface
public interface Adder {
    int add(int a, int b);
}

Like @Override with methods, @FunctionalInterface declares our intentions with Adder.

Now, whether we use @FunctionalInterface or not, we can still use Adder in the same way:

Adder adder = (a,b) -> a + b;
int result = adder.add(4,5);

But, if we add a second method to Adder, then the compiler will complain:

@FunctionalInterface
public interface Adder { 
    // compiler complains that the interface is not a SAM
    
    int add(int a, int b);
    int div(int a, int b);
}

Now, this would’ve compiled without the @FunctionalInterface annotation. So, what does it give us?

Like @Override, this annotation protects us against future programmer error. Even though it’s legal to have more than one method on an interface, it isn’t when that interface is being used as a lambda target. Without this annotation, the compiler would break in the dozens of places where Adder was used as a lambda. Now, it just breaks in Adder itself.

4. Meta-Annotations

Next, meta-annotations are annotations that can be applied to other annotations.

For example, these meta-annotations are used for annotation configuration:

  1. @Target
  2. @Retention
  3. @Inherited
  4. @Documented
  5. @Repeatable

4.1. @Target

The scope of annotations can vary based on the requirements. While an annotation is only used with methods, another annotation can be consumed with constructor and field declarations.

To determine the target elements of a custom annotation, we need to label it with a @Target annotation.

@Target can work with eight different element types. If we look at the source code of @SafeVarargs, then we can see that it must be only attached to constructors or methods:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SafeVarargs {
}

4.2. @Retention

Some annotations are meant to be used hints for the compiler, while others are used at runtime.

We use the @Retention annotation to say where in our program’s lifecycle our annotation applies.

To do this, we need to configure @Retention with one of three retention policies:

  1. RetentionPolicy.SOURCE – visible by neither the compiler nor the runtime
  2. RetentionPolicy.CLASS – visible by the compiler
  3. RetentionPolicy.RUNTIME – visible by the compiler and the runtime

@Retention defaults to RetentionPolicy.SOURCE.

If we have an annotation that should be accessible at runtime:

@Retention(RetentionPolicy.RUNTIME)
@Target(TYPE)
public @interface RetentionAnnotation {
}

Then, if we add some annotations to a class:

@RetentionAnnotation
@Deprecated
public class AnnotatedClass {
}

Now we can reflect on AnnotatedClass to see how many annotations are retained:

@Test
public void whenAnnotationRetentionPolicyRuntime_shouldAccess() {
    AnnotatedClass anAnnotatedClass = new AnnotatedClass();
    Annotation[] annotations = anAnnotatedClass.getClass().getAnnotations();
    assertThat(annotations.length, is(1));
}

The value is 1 because @RetentionAnnotation has a retention policy of RUNTIME while @Deprecated doesn’t.

4.3. @Inherited

In some situations, we may need a subclass to have the annotations bound to a parent class.

We can use the @Inherited annotation to make our annotation propagate from an annotated class to its subclasses.

If we apply @Inherited to our custom annotation and then apply it to BaseClass:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InheritedAnnotation {
}

@InheritedAnnotation
public class BaseClass {
}

public class DerivedClass extends BaseClass {
}

Then, after extending the BaseClass, we should see that DerivedClass appears to have the same annotation at runtime:

@Test
public void whenAnnotationInherited_thenShouldExist() {
    DerivedClass derivedClass = new DerivedClass();
    InheritedAnnotation annotation = derivedClass.getClass()
      .getAnnotation(InheritedAnnotation.class);
 
    assertThat(annotation, instanceOf(InheritedAnnotation.class));
}

Without the @Inherited annotation, the above test would fail.

4.4. @Documented

By default, Java doesn’t document the usage of an annotation in Javadocs.

But, we can use the @Documented annotation to change Java’s default behavior.

If we create a custom annotation that uses @Documented:

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelCell {
    int value();
}

And, apply it to the appropriate Java element:

public class Employee {
    @ExcelCell(0)
    public String name;
}

Then, the Employee Javadoc will reveal the annotation usage:

4.5. @Repeatable

Sometimes it can be useful to specify the same annotation more than once on a given Java element.

Before Java 7, we had to group annotations together into a single container annotation:

@Schedules({
    @Schedule(time = "15:05"),
    @Schedule(time = "23:00")
})
void scheduledAlarm() {
}

However, Java 7 brought a cleaner approach. With the @Repeatable annotation, we can make an annotation repeatable:

@Repeatable(Schedules.class)
public @interface Schedule {
    String time() default "09:00";
}

To use @Repeatable, we need to have a container annotation, too. In this case, we’ll reuse @Schedules:

public @interface Schedules {
    Schedule[] value();
}

Of course, this looks a lot like what we had before Java 7. But, the value now is that the wrapper @Schedules isn’t specified anymore when we need to repeat @Schedule:

@Schedule
@Schedule(time = "15:05")
@Schedule(time = "23:00")
void scheduledAlarm() {
}

Because Java requires the wrapper annotation, it was easy for us to migrate from pre-Java 7 annotation lists to repeatable annotations.

5. Conclusion

In this article, we’ve talked about Java built-in annotations that every Java developer should be familiar with.

As always, all the examples of the article can be found over on GitHub.

I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE LESSONS