The last in our ‘Improvements in Java 8’ series, in this article we’ll be looking at both 'Type' and 'Repeatable' annotations in detail and explaining their benefits.
Annotations
Before we go into the details of 'Type' and 'Repeatable' annotations, let’s first look at what annotations are and how they work.
What are Annotations?
The concept of annotations was introduced in Java 5. Used to specify metadata, they don’t change how a program works and instead provide additional information and help to increase the overall quality of the code provided.
Java provides some in-built annotations, including '@Override', '@Deprecated' and more. In-built annotations are analysed by the compiler and work by altering the way the program is treated. With the help of annotations, the compiler can enforce some checks and catch errors early on. For example, if a method has an '@Override' annotation, this means that this is an overridden method and the superclass needs to have a method with the same name; otherwise the compiler will cause an error.
In addition to in-built annotations you can also create your own custom annotations. Custom annotations are not checked by the compiler, so you’ll need to write your own code using the Reflection API in order to be able to do something meaningful with the annotation.
Finally, frameworks like JPA and Spring also provide some annotations that can be used by tools to generate or modify code. For example, JPA provides some annotations like '@Id', and '@Entity', which can be used to generate the database tables.
Creating a custom annotation
It’s fairly easy to create your own custom annotations. As an example, Java 5 has added an 'Enum' called 'java.lang.annotation.ElementType' which can be used to create custom annotations. The 'Enum' constants specify where the annotation can be applied, yet prior to Java 8, annotations could only be applied to constructors, fields, local variables, methods, packages or parameters.
The following code demonstrates creating a custom annotation:
@Target(ElementType.FIELD)
public @interface Email {
}
Above, an annotation called 'Email' is created. The 'ElementType.FIELD' is used, so this annotation can only be applied to a field in a class as follows:
public class Person {
private String name;
private int age;
private String email;
}
In the example above, the email field is designated with the ‘@Email’ annotation.
As mentioned earlier, the compiler does not process custom annotations, meaning you’d need to write your own code to process this annotation and do something meaningful with it.
Type Annotations
As we mentioned earlier, before Java 8, custom annotations could only be applied on declarations such as constructors, fields, local variables, methods, packages or parameters.
'Type' annotations introduced by Java 8 can be applied to any type (not just declarations), such as generic type arguments with the 'implements keyword' for typecasts and more. Overall, these help to increase the quality of code by enabling stronger checks, but it’s important to note that 'Type' annotations are not just annotations that run out of the box in Java 8. Instead, they simply provide the ability to create custom annotations that can be applied to any type.
Prior to Java 8
It’s crucial to first understand the limitations of annotations before Java 8. Suppose you have a custom annotation like so:
@Target(ElementType.FIELD)
public @interface NotNull {
}
Consider the following code:
List<@NotNull Person> personList = new ArrayList<Person>();
Above, the annotation is applied to a generic type. However, this wasn't allowed prior to Java 8, so the code would result in a compilation error.
Creating a Type Annotation
Java 8 has added two new constants to the 'ElementType' 'Enum' in order to support type annotations:
ElementType.TYPE_USE: Specifies that an annotation can be applied on any type
ElementType.TYPE_PARAMETER: Specifies that the annotation can be applied on type variables
The above 'Enum' values need to be used to create a 'Type' annotation as demonstrated below:
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface NotNull {
}
In the example above, a 'Type' annotation has been created called 'NotNull' which can now be applied to additional types like generics, type casts and more.
Using a Type Annotation
As we mentioned earlier, 'Type' annotations can be used on a number of additional types like constructors, generic type arguments, with the 'implements keyword', for type casts. as demonstrated below:
With a Generic:
List<@NotNull Person> personList = new ArrayList<Person>();
With a Type cast:
Object obj = new Object();
Person person = (@NotNull Person) obj;
With a constructor:
Person p = new @NotNull Person();
With inheritance:
class MyFile extends @Readable File {
}
OR
class Person implements @Ascending Comparator{
}
With exceptions:
public void login() throws @Critical AccessDeniedException{
}
With method references:
@NotNull String::toUpperCase();
With nested types:
Map.@NotNull Entry
Repeatable Annotations
Before Java 8, annotations couldn't be repeated meaning that if the same annotation was applied more than once on the same class, method or field, it would result in a compilation error.
Repeatable annotations help to overcome this limitation by allowing the same one to be applied more than once.
Prior to Java 8
Consider the following code that creates a new annotation called 'Season':
@Target(ElementType.FIELD)
public @interface Season {
String value();
}
Now look at the following code which uses this annotation:
@Season("summer")
@Season("winter")
private String sports;
A field called sports is defined with the '@Season' annotation. This code would have caused a compilation error prior to Java 8 since the annotation '@Season' is present twice. It would need to be re-written it as follows:
@Season1("summer")
@Season2("winter")
private String sports;
Essentially, you’d need to create separate annotations corresponding to each season.
Creating a Repeatable Annotation
Java 8 has added a new annotation called '@Repeatable' in the 'java.lang.annotation' package to support annotations that can be repeated.
There are two steps in creating a 'Repeatable' annotation:
Step 1 – Defining the annotation
First you need to define the annotation as follows:
@Repeatable(Seasons.class)
public @interface Season {
String value();
}
Following the example above will define the 'Season' annotation with the '@Repeatable' annotation specified. For the value attribute of the 'Repeatable' annotation, you need to specify the name of the container annotation - you’ll be able to do this in step 2. You’ll also need to declare a value property within the annotation.
Step 2 – Creating the container annotation
The 'Container' annotation should be created as follows:
public @interface Seasons {
Season[] value();
}
The above example defines a 'Container' annotation to store the 'Season' annotation. The 'Container' annotation should have a value specified and this value should be an array of the 'Repeatable' annotation type. In this case, it’s an array of 'Season'.
Using a Repeatable annotation
Once a 'Repeatable' annotation is created, you can use it any number of times on a target type as demonstrated below:
@Season("summer")
@Season("winter")
private String sports;
This code will no longer cause a compilation error.
In this article, we saw how both 'Type' and 'Repeating' annotations work and why they’re so useful when it comes to increasing the quality of your code overall. Like what you’ve read? Make sure to check out our entire series on improvements in Java 8.