How to create Repeatable Annotations in Java

dimitrilc
Introduction

In Java 8, @Repeatable was introduced and this is the recommended way to create repeating annotations. We still have to create a holder Annotation(annotation that holds an array of other annotations), but we no longer have to declare the holder Annotation at the callsite.

This tutorial aims to show you the difference between the old way and the new way using @Repeatable.

Goals

At the end of this tutorial, you would have learned:

  1. How to create your own custom Annotations.
  2. How to create repeatable Annotations.
Prerequisite Knowledge
  1. Basic Java
  2. Basic knowledge of Reflection API.
  3. Basic understanding of Annotations.
Tools Required
  1. A Java IDE with at least JDK 8 support.
Project Setup

To follow along with this tutorial, perform the steps below:

  1. Create a new Java project.
  2. Create a package com.example.
  3. Create a Java class called Entry.java. This is where our main() method lives.
  4. Under the com.example package, create 3 public classes:
    a. Banana
    b. Cat
    c. Bike
Custom Annotation Review

Most Java developers I met have told me that they never had to create their own custom Annotations, even though they use Annotations every day(@Override). It is usually the job of frameworks to provide out-of-the-box Annotations for other developers to use.

This section of the tutorial provides a quick overview of how to create custom Annotations for those who are not familiar with the syntax or as a review for those who have forgotten.

Copy and paste the code below into your Cat.java file.

    package com.example;

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

    @Retention(RetentionPolicy.RUNTIME)
    @interface CatAttribute { //1
       String value(); //2
    }

    @CatAttribute("Cute") //3
    public class Cat { } //Single custom Annotation

In the code snippet above, we declared an empty class Cat and an Annotation called CatAttribute.

Annotations are similar to interfaces. They are implicitly abstract and cannot be instantiated. That is why the chosen syntax for declaring an annotation is also the keyword “interface”, but prefixed with an @ symbol to differentiate between the two.

Annotations that we see in code might not “survive” compile-time or runtime. Whether they are stripped or preserved at different stages is determined by the enum RetentionPolicy(CLASS, RUNTIME, SOURCE).

You are not required to annotate your custom Annotations with @RetentionPolicy; I only do that for this code snippet because later we are going to retrieve the annotation in main(). If you do not annotate your custom Annotation with @RetentionPolicy, then your annotation will just follow the default behavior, which is specified by RetentionPolicy.CLASS.

Pay attention to the value() method at line 2. The method value() is special, and the terminology changes a little bit. The official documentation calls them elements instead of methods. If the special element value exists in an Annotation, then we do not have to specify the word “value” again at the callsite, hence why we were able to pass “Cute” directly to @CatAttribute at line 3.

The old way of creating repeating Annotations

For us to understand how to use @Repeatable, we need to see how the old way works first .

Copy and paste the code below into Banana.java.

    package com.example;

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

    @interface Benefit { //1
       String value();
    }

    @Retention(RetentionPolicy.RUNTIME)
    @interface Benefits { //2
       Benefit[] value(); //3
    }

    @Benefits({ //4
           @Benefit("Nutritious"), //5
           @Benefit("Healthy") //6
    })
    public class Banana { } //The old way to doing repeatable annotation

In the code snippet above, we have created two different Annotations. The Benefit Annotation is what contains interesting information for the class Banana, but the Benefits(plural) Annotation contains an array to hold the other Benefit Annotations.

At callsite at line 4, the code becomes a little bit hard to read because we have to declare the holder annotation and the holder array as well. Wouldn’t it be nice to just be able to apply multiple Benefit Annotations directly to Banana instead?

@Repeatable Annotation

The @Repeatable Annotation allows you to do just that. After setting it up, we will be able to apply the same Annotation multiple times.

In Bike.java, copy and paste the code below:

    package com.example;

    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;

    @Repeatable(BikeAttributes.class) //1
    @interface BikeAttribute { //2
       String value();
    }

    @Retention(RetentionPolicy.RUNTIME)
    @interface BikeAttributes { //3
       BikeAttribute[] value(); //4
    }

    @BikeAttribute("Agile") //5
    @BikeAttribute("Affordable") //6
    public class Bike { } //Repeatable annotation

To use @Repeatable, we have to perform the almost exact steps as when we were using the holder Annotation. The only extra step that we have to do is to annotate the Annotation that needs repeating with @Repeatable and pass in the class of the holder Annotation as its value. You can see that at line 1.

Even though it is just sugar syntax, the benefit here is that we no longer have to declare the holder Annotation anymore, and can annotate the @BikeAttribute directly multiple times.

What do Annotations look like at runtime?

Since we annotated all of the Annotations that we created in the previous steps with @Retention(RetentionPolicy.RUNTIME), we are now able to retrieve them at runtime. In your Entry.java class, copy and paste the code below.

    package com.example;

    import java.lang.annotation.Annotation;

    public class Entry {
       public static void main(String[] args){
           Annotation[] catAnnotations = Cat.class.getAnnotations(); //1 getting cat annotations via reflection
           for (Annotation annotation : catAnnotations) {
               System.out.println(annotation);
           }

           Annotation[] bananaAnnotations = Banana.class.getAnnotations(); //2 getting banana annotations via reflection
           for (Annotation annotation : bananaAnnotations){
               System.out.println(annotation);
           }

           Annotation[] bikeAnnotations = Bike.class.getAnnotations(); //3 getting bike annotations via reflection
           for (Annotation annotation : bikeAnnotations){
               System.out.println(annotation);
           }
       }
    }

All the code does is to get the Annotations that were preserved at runtime for our previous classes and print them out.

For the Cat class that was only annotated once, we get:

    @com.example.CatAttribute("Cute")

But for both Banana and Bike classes, they both get the holder Annotations, which further proves that @Repeatable is just sugar syntax.

    @com.example.Benefits({
    @com.example.Benefit("Nutritious"), 
    @com.example.Benefit("Healthy")
    })
    @com.example.BikeAttributes({
    @com.example.BikeAttribute("Agile"),
     @com.example.BikeAttribute("Affordable")
    })
Solution Code

You can find the full source code for the project here https://github.com/dmitrilc/DaniwebJavaRepeatable

Summary

We have learned how to create repeatable Annotations. Its sugar syntax will make our code a lot more readable.

The @Repeatable syntax also makes it very easy to convert current holder Annotations. All you have to do is to add @Repeatable to child Annotation and pass in the class object of the holder Annotation.

28 Views
About the Author

My name is Dimitri Nguyen. I am a Java Developer specializing in backend development on the Java/Spring/MySQL stack.

I can also work on the frontend using Angular/Typescript/JS/HTML/CSS and native Android with Kotlin.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts learning and sharing knowledge.