All About Annotations in Java All About Annotations in Java

Page content

In this tutorial, we’ll learn basics of Java Annotations, Built-in Java Annotations, How to create single-value, multi-value, repeated and type annotations and How to use them at runtime.

Overview

Annotations are very powerful feature of Java. You must have come across @Override, @Deprecated, @SuppressWarnings, which are built-in annotations in Java.

Most of the Java-based framework heavily use annotations. For instance, Spring Framework provides @Autowired, @Controller, @Component, … . Similarly Hibernate Framework provides @Entity, @Table, @Column, … . Annotations make these frameworks easier to use and at the same time make the source code more readable.

What are Annotations?

Annotations are used to add some marker or data at class, method, or field level. This data is used at compile-time or run-time to perform some actions.

It is important to note that Annotations doesn’t have any direct effect on the behavior of class, method, or field, where it is applied. For example, adding @Override, @Deprecated or @SuppressWarnings at method level doesn’t change the output of that method.

However frameworks such as Spring and Hibernate use these annotations to perform some actions at runtime. For example, adding @Autowired at field level instruct the Spring framework to inject the dependency of this field at runtime.

Let’s understand two important concepts of annotations now - Target and Retention Policy

Target

Target defines where the annotation can be applied for example, at class, method or field level. This information is required while creating an annotation. There are the types of possible targets:-

@Target(ElementType.*)Annotation can be applied on …
ElementType.TYPEclass, interface or enum
ElementType.METHODmethod
ElementType.FIELDfield
ElementType.CONSTRUCTORconstructor
ElementType.PARAMETERparameter of a method or constructor
ElementType.PACKAGEpackage

Note that some annotation like @Override can be applied at method level only while others like @Deprecated, @SuppressWarnings can be applied at any level - class, method, or field.

Retention Policy

Retention policy defines the life-cycle of the annotation. This information is also required while creating an annotation. There are three types of retention policies:-

@Retention(RetentionPolicy.*)Description
RetentionPolicy.SOURCEAnnotations are retained only in source code but ignored in .class file by the compiler
RetentionPolicy.CLASSAnnotations are retained in .class file by the compiler but ignored by the JVM at run time. This is the default behavior
RetentionPolicy.RUNTIMEAnnotations are retained in .class file by the compiler and also retained by the JVM at run time, so they can be used by java reflection

Note that you normally use RetentionPolicy.RUNTIME while creating your own custom annotation because you may want to use the data from your annotation at runtime using java reflection. We will talk more about it later in the post while creating custom annotations.

Java Built-in Annotations

Now we understand that What is Target and Retention Policy, which are two required information to create any annotation, Let’s look at the java built-in annotations:-

@Override

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

We see that @Override annotation can be applied at methods only (target) and part of the source code only (retention policy).

@Override
public String toString() { }

When we use @Override annotation at method level, it informs the complier that this method is meant to override a method declared in a superclass. It assures that method is overridden with correct signature. If not so, complier throws compilation error.


@Deprecated

@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Deprecated {
}

We see that @Deprecated annotation can be applied anywhere such as class, field, method, constructor, package, or method arguments level. Also it is retained at runtime means information can be extracted from this annotation using java reflection at runtime.

@Deprecated
static void deprecatedMethod() { }
}

When we use @Deprecated annotation at method level, it informs the compiler that this method is deprecated and should no longer be used. The compiler generates a warning whenever a program uses this method.


@SuppressWarnings

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
  String[] value();
}

We see that @SuppressWarnings annotation can be applied anywhere and retained in the source code only. We also see that this annotations takes comma separated String values. Few possible values are “rawtypes”, “unused”, “unchecked” and “deprecation”.

@SuppressWarnings({ "rawtypes", "unused", "unchecked", "deprecation" })
@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
    // suppress the deprecation warning while calling a deprecated method
    objectOne.deprecatedMethod();
}

@SuppressWarnings({ "rawtypes", "unchecked" })
void useRawCollection() {
    // suppress the rawtypes warning when initialize non-parameterized (raw) ArrayList 
    // suppress the unchecked warning when adding an integer to raw ArrayList
    ArrayList list=new ArrayList();   
    list.add(1);
}

When we use @SuppressWarnings annotation at method level, it informs the compiler to suppress the warnings which it would generate otherwise.

How to create Custom Annotation?

Let’s create a custom Annotation @ClassInfo

  • Annotations are created by using @interface, followed by annotation name which is ClassInfo in our case.
  • An annotation class can have one or more elements. They look like methods. For example in the below code, we have six elements namely author, date, currentRevision, lastModified, lastModifiedBy, and reviewers.
  • Default values to element can be assigned using default keyword
@Documented
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface ClassInfo {

	String author();

	String date();

	int currentRevision() default 1;

	String lastModified() default "N/A";

	String lastModifiedBy() default "N/A";

	// Note use of array
	String[] reviewers();
}

Meta-Annotations

You see that we have applied few annotations to our custom annotation ClassInfo. These are called as meta-annotations, which provide additional information about our custom annotation. They are as follows:-

  1. @Documented indicates to include the @ClassInfo annotation in JavaDoc. By default annotations are not included in JavaDoc.
  2. @Inherited indicates to inherit the @ClassInfo annotation in subclasses. By default annotations are not inherited.
  3. @Target(ElementType.TYPE) indicates that @ClassInfo annotation can be apply at class level only
  4. @Retention(RetentionPolicy.RUNTIME) indicates that @ClassInfo annotation elements can be accessed during runtime using java reflection.

How to use Custom Annotation?

Now we have created our custom annotation @ClassInfo, Let’s use it on the class

@ClassInfo (
   author = "John Doe",
   date = "3/17/2002",
   // Note array notation
   reviewers = {"Alice", "Bob", "Cindy"}
)
class MyClass {

}

Note that we have skipped currentRevision, lastModified, lastModifiedBy elements while using @ClassInfo annotation. Default values are assigned to these elements.

Let’s see how to get the information from our custom @ClassInfo annotation at runtime

public class AnnotationsTest {

	public static void main(String[] args) {
		printClassInfo(new MyClass());
	}

	public static void printClassInfo(Object object) {
		Class<?> clazz = object.getClass();
		if (clazz.isAnnotationPresent(ClassInfo.class)) {
			ClassInfo classInfo = clazz.getAnnotation(ClassInfo.class);
			System.out.println("author: " + classInfo.author());
			System.out.println("date: " + classInfo.date());
			System.out.println("currentRevision: " + classInfo.currentRevision());
			System.out.println("lastModified: " + classInfo.lastModified());
			System.out.println("lastModifiedBy: " + classInfo.lastModifiedBy());
			System.out.println("reviewers: " + Arrays.toString(classInfo.reviewers()));
		} else {
			System.out.println("Class Info is not available");
		}
	}
}
Output
author: John Doe date: 3/17/2002 currentRevision: 1 lastModified: N/A lastModifiedBy: N/A reviewers: [Alice, Bob, Cindy]

@Inherited

class MyChildClass extends MyClass {}

Remember, We applied @Inherited meta-annotation on our custom annotation class ClassInfo while creation. That means, @ClassInfo info applied at MyClass will be inherited by its child class MyChildClass as well resulting both produce same output when passed in printClassInfo method.

public static void main(String[] args) {
  printClassInfo(new MyClass());
  printClassInfo(new MyChildClass()); // same output as: new MyClass()
}

Repeated Annotation

Java 8 has introduced the Repeated annotation where same annotation can be used multiple times at class, method or field level. Before Java 8, this result into compilation error.

Let’s create @Author annotation which can be used repeatedly at class level. It requires two steps:-

  1. First create a repeatable annotation @Author. We need to apply @Repeatable meta-annotation on our custom annotation to use it multiple time. Note that we need to pass a container annotation class in @Repeatable.
  2. Second create a container annotation @Authors which holds all repeatable Author values. This container annotation must have an element named value() and of type array Author[]
@Repeatable(Authors.class)
@interface Author {
	String name();
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Authors {
	Author[] value(); //must have this signature
}

Now we have created our repeatable @Author annotation. Let’s use it on MyClass multiple times and get the list of all the authors at runtime.

@Author(name = "Author 1")
@Author(name = "Author 2")
@Author(name = "Author 3")
class MyClass {

}

Java 8 internally treats this Repeating Annotation as an instance of @Authors holding an array of @Author.

public class RepeatedAnnotationsTest {

	public static void main(String[] args) {	
		System.out.println(getListOfAuthors(new MyClass()));
	}

	public static List<String> getListOfAuthors(Object object) {
		Class<?> clazz = object.getClass();
		if (clazz.isAnnotationPresent(ClassInfo.class)) {
			Author[] authors = clazz.getAnnotationsByType(Author.class);
			return Arrays.stream(authors).map(author -> author.name()).collect(Collectors.toList());
		} else {
			System.out.println("Authors not available");
		}
		return null;
	}
}
Output
[Author 1, Author 2, Author 3]

Type Annotation

Java 8 has introduced Type annotation by adding two new target types. Let’s use them to create @NotNull annotation.

@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
}

The ElementType.TYPE_PARAMETER means the annotation can be applied on the declaration of a type variable (e.g., class MyClass {…}).
The ElementType.TYPE_USE means the annotation can be applied on any use of a type (e.g., types appearing in declarations, generics, and type casts)

Let’s use the @NotNull annotation on the type declaration of a field and use it to validate the null value. Throw exception if any field declared with @NotNull annotation contains null value.

class MyClass {
	private @NotNull String key;
}
public class TypeAnnotationsTest {

	public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
		validateProperties(new MyClass()); // value of field key is null
	}

	public static void validateProperties(Object object) throws IllegalArgumentException, IllegalAccessException {
		Class<?> clazz = object.getClass();
		Field[] fields = clazz.getDeclaredFields();
		for(Field field: fields) {
			field.setAccessible(true);
			if(field.getAnnotatedType().isAnnotationPresent(NotNull.class)) {
				if(field.get(object) == null) {
					throw new NullPointerException("@NotNull field \"" + field.getName() + "\" can not be null");
				}
			}
		}
	}
}
Output
Exception in thread "main" java.lang.NullPointerException: @NotNull field "key" can not be null at com.example.core.TypeAnnotationsTest.validateProperties(TypeAnnotationsTest.java:14) at com.example.core.TypeAnnotationsTest.main(TypeAnnotationsTest.java:4)

Some of the example usage of type annotations are as follows:-

// declaration
@Encrypted String data;

// generics
List<@NonNull String> strings;

// type cast
String myString = (@NonNull String) myObject;

// constructor
new @Interned MyObject();
new @NonEmpty @Readonly List<String>(myNonEmptyStringSet);

// implements clause
class UnmodifiableList<T> implements @Readonly List<T> { 
	... 
}

// throw exception declaration
void monitorTemperature() throws @Critical TemperatureException { 
	... 
}

Conclusion

We see that Java Annotations are widely used by Java and its popular framework such as Spring and Hibernate. Most of the time, we as a developer are consumers of these annotations, rather than their creator. Though it is good to have knowledge of how custom annotations are created and used behind the scene.