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:
- How to create your own custom Annotations.
- How to create repeatable Annotations.
Prerequisite Knowledge
- Basic Java
- Basic knowledge of Reflection API.
- Basic understanding of Annotations.
Tools Required
- A Java IDE with at least JDK 8 support.
Project Setup
To follow along with this tutorial, perform the steps below:
- Create a new Java project.
- Create a package
com.example
. - Create a Java class called
Entry.java
. This is where ourmain()
method lives. - 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.