Monday, July 5, 2021

Java Tutorial: Annotations

Chapters

Java Annotation

Annotations were introduced in java5 and their purpose is to provide metadata for our code. Annotations can also be used to set instructions to the compiler; build instructors for compile-time that can be used for generating code like XML; create metadata and instructions for our code that can be available during runtime.

Annotations don't directly affect the behavior of our code. In other words, They don't directly affect the functionality of our program. Annotations can be applied to methods, fields, classes and interfaces.

To annotate an element, write the annotation followed by the element.
e.g.
Annotation syntax
without value
@annotation-name
with value
@annotation-name("value")

Annotating fields/variables
@Deprecated int deprecation = 10;
or
@Deprecated
int deprecation = 10;

Annotating blocks(e.g. methods, constructors, etc.)
@Deprecated public void meth(){/**/}
or
@Deprecated
public void meth(){/**/}

Multiple annotations can be in any order
@FunctionalInterface
@Deprecated
interface InterfaceA{
  void meth();
}
or
@Deprecated
@FunctionalInterface
interface InterfaceA{
  void meth();
}

Built-in Annotations

Java has some built-in annotations. These are: @Deprecated, @FunctionalInterface, @Override, @SafeVarargs and @SuppressWarnings. These annotations are ready to use and give helpful instructions to the compiler.

@Deprecated

A program element(e.g. field,method,etc.) that is annotated as @Deprecated is an element that programmers are discouraged from using. An element may be deprecated if it's obsolete or a new version/alternative is implemented. Using deprecated element may cause errors, incompatibility, etc.

When we use a pre-deprecated element annotated with @Deprecated, The compiler will give us a warning.
public class SampleClass{

  public static void main(String[]args){
    
    //Integer constructor is deprecated
    //in java9 and above. The compiler
    //will give us warning if we use
    //Integer constructor
    Integer intOne = new Integer(10);
    
    //Integer.valueOf() is the Recommended
    //alternative by java for deprecated
    //Integer constructor with int parameter
    //Integer intOne = Integer.valueOf(10);
  }
}
We can use @Deprecated if we want a particular element of our code to be deprecated.
public class SampleClass{
  
  //make this method obsolete
  //by using @Deprecated annotation
  //
  //This implementation doesn't
  //consider the possibility of
  //two numbers being equal.
  //
  //@Deprecated can have value
  //like this: @Deprecated(forRemoval=true)
  //We will discuss annotation
  //values later
  //
  @Deprecated
  int findMax(int n1, int n2){
    
    if(n1 > n2)
      return n1;
    else return n2;
    
  }
  
   //new and improved version
   Integer findMaxInteger(Integer n1, Integer n2){
    Integer result = null;
    
    if(n1 > n2)
      result = n1;
    else if(n1 < n2)
      result = n2;
      
    return result;
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    
    //unlike pre-deprecated elements in
    //java, our user-deprecated elements
    //won't make the compiler to throw 
    //a warning regarding deprecation
    //
    //I'm not sure if this is compiler-specific
    //or not.
    //
    //As the time of this writing, I'm using
    //JDK11 of AdoptOpenJDK and my compiler
    //is not giving any warning regarding
    //user-deprecated elements
    //int myInt = findMax(10,15);
    
    Integer intOne = sc.findMaxInteger(10,15);
    if(intOne != null)
      System.out.println("Max: " + intOne);
    else System.out.println("intOne is null!");
    
  }
}
You might ask: "Why don't we remove the obsolete method instead?". Well, It's a bad idea if we do that. Imagine you created a library and some developers are using your created library. Then, You upgraded the version of your library and remove findMax() instead of depracating it. Now, the developers wanna use your library's new version.

Let's say they use your library's new version. Since you remove findMax(), the developers can't compile their codebase anymore because findMax() is missing. If the program is a huge one then their migration is a disaster and most likely, they will rollback to the previous version.

So, it's better to inform your users about the obsolete method first than removing it without any notification. This way, we're giving them time to adapt to your new version.

@FunctionalInterface

We use @FunctionalInterface to give an instruction to the compiler that an interface is a functional interface and the interface must follow the functional interface rule where only one abstract method is allowed.
public class SampleClass{

  public static void main(String[]args){
  }
}

@FunctionalInterface
interface ConcatString{

  void concat(String s1, String s2);
  
  //compiler will throw a compile-time
  //error 'cause we violate the 
  //functional interface rule if we 
  //another abstract method
  //void concatString(String[] s);
}
If we uncomment concatString() method, the compiler will throw a compile-time error. If we uncomment concatSting() and remove @FunctionalInterface then, the compiler won'y throw a warning regarding functional interface even ConcatString interface violates the functional interface rule.

If you intend an interface to be a functional interface then, putting @FunctionalInterface on top of the interface may be helpful.

@Override

Note: You need to have an understanding of method overriding to understand the usage of @Override.
We can use the @Override to give instructions to the compiler that a method annotated with @Override overrides a method in superclass.
public class SampleClass{
  
  public static void main(String[]args){
    ClassB b1 = new ClassB("SampleClass");
  }
}

class ClassA{

  void meth(String s){
    System.out.println("ClassA: " + s);
  }
}

class ClassB extends ClassA{

  @Override
  void meth(String s){
    System.out.println("ClassB: " + s);
  }
}
This example above will compile just fine. @Override ensures that we're overridng a method in a superclass. Try removing the meth() in ClassA or change the method signature of the overriding method and the compiler will throw a compile-time error.

@SafeVarargs

@SafeVarargs is solely used to suppress a warning that is related to variable arguments, which is the heap pollution warning. Use @SafeVarargs if you're confident that your generic varargs is safe to use.
public class SampleClass{
  
  //This method won't issue a warning regarding
  //heap pollution
  @SafeVarargs
  static <T>void displayString(T... obj){
    for(Object o : obj)
      System.out.println(o.toString());
  }
  
  public static void main(String[]args){
    SampleClass.displayString();
  }
}
Try removing @SafeVarargs and recompile the example above. This time, the compiler will throw a warning.

@SuppressWarnings

As the annotation name implies, @SuppressWarnings is used to suppress warnings. Unlike other built-in annotations, this annotation requires a value in array form. This annotation can suppress various warnings like deprecation, unchecked, etc.
General Form: @SuppressWarnings({"warning-to-suppress"})

public class SampleClass{
  
  public static void main(String[]args){
    
    //This instantiation won't issue a
    //deprecation warning
    //
    //Tip: Don't suppress deprecation
    //warning just because you want
    //to. Deprecated element may cause
    //errors so be careful using them.
    //
    //You may wanna suppress deprecation
    //if you're doing a test for 
    //the deprecated element
    @SuppressWarnings({"deprecation"})
    Integer intOne = new Integer(10);
    
    //suppressing raw types warning.
    //raw types warning is part of 
    //unchecked warnings
    //
    //Note: in modern java application,
    //raw types should be avoided
    @SuppressWarnings({"unchecked"})
    ClassA<T> classA = new ClassA("name");
  }
}

class ClassA<T>{
   T name;
   
   ClassA(T name){
     this.name = name;
   }
}
When we suppress a warning at a class level then, its members that throw the same warning will also be suppressed.
//This suppressWarnings annotation suppresses two warnings:
//
//"varargs" and "unchecked".
//"varargs" warning is issued when use generic varargs.
//Generic varargs cause heap pollution.
//
//"unchecked" warning is issued when the 
//compiler can't fully perform type checking
//Java doesn't allow us to create arrays of
//parameterized types. Consequently, elements
//of an array can't be generic. thus,
//the compiler can't fully peform type
//checking on arrays.
//varargs is considered as an array
@SuppressWarnings({"varargs","unchecked"})
public class SampleClass{
  
  //No need to put @SafeVarargs here. @SafeVarargs
  //is equivalent to @SuppressWarnings({"varargs","unchecked"})
  //@SafeVarargs
  static <T>void displayString(T... obj){
    for(Object o : obj)
      System.out.println(o.toString());
  }
  
  public static void main(String[]args){
    SampleClass.displayString();
  }
}
Creating Custom Annotation

We can create our custom annotation by using the @interface syntax. Don't be confused between @interface and interface keyword. @interface is used for custom annotation whereas interface keyword is used for creating interfaces.
General Form

access-modifier @interface annotation-name{

  //annotation body
}
We can put elements/values in our annotation and they look like methods. However, their functionalities are way different from methods and we shouldn't add implementation to these method-like elements in annotation. The purpose of annotation element is to store a value like string or number.

Also, all annotations including our custom annotations implicitly extend java.lang.annotation.Annotation interface and java doesn't allow annotations to explicitly extend any java class/interface/annotation.
public class SampleClass{
   //Annotation elements without default values
   //requires to be initialized when the annotation
   //where they reside is used
   //this are the formats for assigning values to
   //elements: 
   //element-name="value" for string element
   //element-name=number e.g. version=1.555.555 for number element
   //element-name{"value1",number1,"value2"} for array element
   //element-name{"value1"} for array element with single value
   @Author(authorName="Ghosts",authorEmail="MyAddress@yahoo.com")
   @Contributors(names={"Dan Brooks","David Muller"})
   //initializing values in annotation elements with default values
   //is optional. As we can see, @ComparisonAlgorithm annotation
   //elements are not required to be initialized
   @ComparisonAlgorithm
   Integer findMaxInteger(Integer n1, Integer n2){
    Integer result = null;
    
    if(n1 > n2)
      result = n1;
    else if(n1 < n2)
      result = n2;
      
    return result;
  }
  
  //initializing annotation elements
  //with default values is optional
  @ComparisonAlgorithm(dataType="Primitive",returnNull=false)
  int findMax(int n1, int n2){
    
    if(n1 > n2)
      return n1;
    else return n2;
    
  }
  
  public static void main(String[]args){
  }
  
  //custom annotation can be nested
  //just like regular interface
  @interface Author{
  
    //annotation elements
    //without default value
    String authorName();
    String authorEmail();
    
    //annotation elements
    //with default value
    //we will use the default
    //keyword to specify 
    //the default value of
    //an element
    //String authorName() default "Brainy Ghosts";
    //String authorEmail() default "SampleAddress@yahoo.com";
    
  }
}

@interface Contributors{
  
  //an array annotation
  //element without default
  //values
  String[] names();
  
  //an array annotation
  //element with default
  //values
  //String[] names() default {"Dan Brooks","David Muller"};
  
}

@interface ComparisonAlgorithm{

  int version() default 1;
  String dataType() default "Object";
  boolean returnNull() default true;
}
If our annotation has only one element, we can name that element as "value" then, we are not required to include the element name of that element if we initialize the annotation.
public class SampleClass{
   
   //We don't need to include element name
   //If each annotation has a single element
   //named as "value"
   @Author("Ghosts")
   @Contributors({"Dan Brooks","David Muller"})
   Integer findMaxInteger(Integer n1, Integer n2){
    Integer result = null;
    
    if(n1 > n2)
      result = n1;
    else if(n1 < n2)
      result = n2;
      
    return result;
  }
  
  public static void main(String[]args){
  }
}

@interface Author{
  String value();  
}

@interface Contributors{
  String[] value();
}
One of the usage of annotation is to provide metadata for our code. As we can see in the example above, we provide additional information for findMaxInteger() and findMax() methods. If we want to access annotation during runtime, we need to add @Retention annotation on our annotation block then, we can use java reflection to access annotation and its elements.

Annotations for Creating Annotation

Java has annotations that can be used to increase or reduce restrictions of declared annotation. These annotations are different from the built-in annotations. The annotations that we're going to discuss here is solely used for annotation type declaration/definition whereas built-in annotations are used for java elements like constructor,method,field, etc.

Though, There are built-in annotations that can be used for annotation declaration like @Deprecated. To use annotations used for annotation declaration, we need to import java.lang.annotation package.

@Target

@Target specifies where annotations can be used. @Target accepts ElementType as values. If we don't define @Target in our annotation declaration then, that annotation can be used in any java element.
import java.lang.annotation.*;
//valid
@Author(name="Ghosts")
//invalid
//@ComparisonAlgorithm
public class SampleClass{

  //valid
  @ComparisonAlgorithm
  Integer findMaxInteger(Integer n1, Integer n2){
    Integer result = null;
    
    if(n1 > n2)
      result = n1;
    else if(n1 < n2)
      result = n2;
      
    return result;
  }

  public static void main(String[]args){
  }
}

//This annotation doesn't have
//@Target annotation. Thus,
//this annotation can be used
//in any java element
@interface Author{
  String name();
}

//This annotation has @Target
//annotation. This annotation
//can only be used in method
//elements
//
//We can add multiple element
//types in @Target by separating
//them using comma(,)
//e.g. @Target({ElementType.METHOD,ElementType.FIELD})
//For more information about @Target
//visit the java documentation
@Target({ElementType.METHOD})
@interface ComparisonAlgorithm{

  int version() default 1;
  String dataType() default "Object";
  boolean returnNull() default true;
}
@Documented

If we're creating a documentation about our codebase and we want our annotations to be included in that documentation then, We need to annotate our annotation with @Documented annotation.
import java.lang.annotation.*;

@Author(name="Ghosts")
public class SampleClass{

Integer findMaxInteger(Integer n1, Integer n2){
  Integer result = null;
    
  if(n1 > n2)
    result = n1;
  else if(n1 < n2)
    result = n2;
      
  return result;
  }

  public static void main(String[]args){
  }
}

//This annotation will be included in
//a documentation, if we create one.
//Use javadoc tools to create a 
//documentation of your codebase
@Documented
@interface Author{
  String name();
}
@Retention

@Retention specifies the retention of annotations. @Retention accepts RetentionPolicy as value. There are three RetentionPolicy values that we can use. These are: RetentionPolicy.SOURCE, RetentionPolicy.CLASS and RetentionPolicy.RUNTIME.

RetentionPolicy.SOURCE means that annotations is only available in the source code.

RetentionPolicy.CLASS means that annotations is included in the .class file. Users can view annotations if they inspect .class file.

RetentionPolicy.RUNTIME means that annotations can be accessed during runtime. Use java reflection tools to access annotations and their elements.

If we don't annotate annotations with @Retention then, the RetentionPolicy value of those annotations is going to be RetentionPolicy.CLASS by default.
import java.lang.reflect.*;
import java.lang.annotation.*;

public class SampleClass{
   @Author(authorName="Ghosts",authorEmail="MyAddress@yahoo.com")
   @Contributors(names={"Dan Brooks","David Muller"})
   @ComparisonAlgorithm
   Integer findMaxInteger(Integer n1, Integer n2){
    Integer result = null;
    
    if(n1 > n2)
      result = n1;
    else if(n1 < n2)
      result = n2;
      
    return result;
  }
  
  //initializing annotation elements
  //with default values is optional
  @ComparisonAlgorithm(dataType="Primitive",returnNull=false)
  int findMax(int n1, int n2){
    
    if(n1 > n2)
      return n1;
    else return n2;
    
  }
  
  //display Annotation elements using reflection tools
  static void displayElements(Annotation[] annotations){
    for(Annotation anno : annotations){
      if(anno instanceof Author){
        Author author = (Author)anno;
        System.out.println(author.authorName());
        System.out.println(author.authorEmail());
      }
      else if(anno instanceof ComparisonAlgorithm){
        ComparisonAlgorithm ca = (ComparisonAlgorithm)anno;
        System.out.println(ca.version());
        System.out.println(ca.dataType());
        System.out.println(ca.returnNull());
      }
      //this block won't execute 'cause the 
      //Contributors annotation can't be accessed
      //at run-time
      else if(anno instanceof Contributors){
        Contributors cb = (Contributors)anno;
        for(String s : cb.names())
          System.out.println(s);
      }
      System.out.println();  
    }
    
  }
  
  public static void main(String[]args){
    //we're going to use java reflection tools
    //here to inspect annotations at runtime
    Class sc = SampleClass.class;
    Method[] meth = sc.getDeclaredMethods();
    
    for(Method m : meth)
      SampleClass.displayElements(m.getAnnotations());
      
  }
  
  //This annotation can be accessed at 
  //runtime
  @Retention(RetentionPolicy.RUNTIME)
  @interface Author{
    String authorName();
    String authorEmail(); 
  }
}

//This annotation has 
//RetentionPolicy.Class by default.
//Thus, It can't be accessed at
//runtime
@interface Contributors{
  String[] names();
}

//This annotation can be accessed at 
//runtime
@Retention(RetentionPolicy.RUNTIME)
@interface ComparisonAlgorithm{

  int version() default 1;
  String dataType() default "Object";
  boolean returnNull() default true;
}
@Inherited

If an annotation with @Inherited annotates an element and that element has subclasses then, the subclasses will be annotated with the same annotation of their superclass.
import java.lang.reflect.*;
import java.lang.annotation.*;
public class SampleClass{

  public static void main(String[] args){
    Class b1 = ClassB.class;
    
    //ClassB inherited the Author annotation of
    //ClassA. Try removing @Inherited in Author
    //declaration and the length of this array
    //will be zero
    Annotation[] annotations = b1.getAnnotations();
    System.out.println("# of Annotations: " + annotations.length);
    
    for(Annotation anno : annotations)
      if(anno instanceof Author){
        Author author = (Author)anno;
        System.out.println(author.value());
      }
        
  }
}

@Author("Brainy Ghosts")
class ClassA{
}

class ClassB extends ClassA{
}

@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Author{
  String value();
}
According to @Inherited Documentation: "this meta-annotation type has no effect if the annotated type is used to annotate anything other than a class. Note also that this meta-annotation only causes annotations to be inherited from superclasses; annotations on implemented interfaces have no effect".

No comments:

Post a Comment