JavaRush /Java Blog /Random EN /Java @Annotations. What is it and how to use it?
SemperAnte
Level 4
Донецк

Java @Annotations. What is it and how to use it?

Published in the Random EN group
This article is intended for people who have never worked with Annotations, but would like to understand what it is and what it is used with. If you have experience in this area, I don’t think that this article will somehow expand your knowledge (and, in fact, I don’t pursue such a goal). Also, the article is not suitable for those who are just starting to learn the Java language. If you don’t understand what a Map<> or HashMap<> is or don’t know what the static{ } entry inside a class definition means, or have never worked with reflection, it’s too early for you to read this article and try to understand what annotations are. This tool itself is not created for use by beginners, since it requires a not entirely basic understanding of the interaction of classes and objects (my opinion) (thanks to the comments for showing the need for this postscript). Java @Annotations.  What is it and how to use it?  - 1So let's get started. Annotations in Java are a kind of labels in the code that describe the metadata for a function/class/package. For example, the well-known @Override Annotation, which indicates that we are going to override a method of the parent class. Yes, on the one hand, it is possible without it, but if the parents do not have this method, there is a possibility that we wrote the code in vain, because This particular method may never be called, but with the @Override Annotation the compiler will tell us that: “I didn’t find such a method in the parents... something is dirty here.” However, Annotations can carry more than just the meaning of “for reliability”: they can store some data that will be used later.

First, let's look at the simplest annotations provided by the standard library.

(thanks again to the comments, at first I didn’t think that this block was needed) First, let’s discuss what annotations are. Each of them has 2 main required parameters:
  • Storage type (Retention);
  • The type of the object over which it is indicated (Target).

Storage type

By “storage type” we mean the stage to which our annotation “survives” inside the class. Each annotation has only one of the possible "retention types" specified in the RetentionPolicy class :
  • SOURCE - the annotation is used only when writing code and is ignored by the compiler (i.e., it is not saved after compilation). Typically used for any preprocessors (conditionally), or instructions to the compiler
  • CLASS - the annotation is preserved after compilation, but is ignored by the JVM (i.e. cannot be used at runtime). Typically used for any third-party services that load your code as a plug-in application
  • RUNTIME is an annotation that is saved after compilation and loaded by the JVM (i.e. can be used during execution of the program itself). Used as marks in the code that directly affect the execution of the program (an example will be discussed in this article)

The type of object above which is indicated

This description should be taken almost literally, because... in Java, annotations can be specified over anything (Fields, classes, functions, etc.) and for each annotation it is indicated what exactly it can be specified over. There is no longer a “one thing” rule here; an annotation can be specified above everything listed below, or you can select only the necessary elements of the ElementType class :
  • ANNOTATION_TYPE - another annotation
  • CONSTRUCTOR - class constructor
  • FIELD - class field
  • LOCAL_VARIABLE - local variable
  • METHOD - class method
  • PACKAGE - description of the package package
  • PARAMETER - method parameter public void hello(@Annontation String param){}
  • TYPE - indicated above the class
In total, as of Java SE 1.8, the standard language library provides us with 10 annotations. In this article we will look at the most common of them (who are interested in all of them? Welcome to Javadoc ):

@Override

Retention: SOURCE; Target: METHOD. This annotation shows that the method over which it is written is inherited from the parent class. The first annotation that every novice Java programmer comes across when using an IDE that persistently pushes these @Override. Often, teachers from YouTube recommend either: “erase it so it doesn’t interfere,” or: “leave it without wondering why it’s there.” In fact, the annotation is more than useful: it not only allows you to understand which methods were defined in this class for the first time, and which the parents already have (which undoubtedly increases the readability of your code), but also this annotation serves as a “self-check” that you were not mistaken when defining an overloaded function.

@Deprecated

Retention: Runtime; Target: CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE. This annotation identifies methods, classes or variables that are "obsolete" and may be removed in future versions of the product. This annotation is usually encountered by those who read the documentation of any APIs, or the same standard Java library. Sometimes this annotation is ignored because... it does not cause any errors and, in principle, in itself does not interfere much with life. However, the main message that this annotation carries is “we have come up with a more convenient method for implementing this functionality, use it, don’t use the old one” - well, or else - “we renamed the function, but this is so, we left it for legacy...” (which is also generally not bad). In short, if you see @Deprecated, it’s better to try not to use what it hangs over unless it’s absolutely necessary, and it might be worth re-reading the documentation to understand how the task performed by the deprecated element is now implemented. For example, instead of using new Date().getYear() it is recommended to use Calendar.getInstance().get(Calendar.YEAR) .

@SuppressWarnings

Retention: SOURCE; Target: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE This annotation disables the output of compiler warnings that concern the element over which it is specified. Is the SOURCE annotation indicated above fields, methods, classes.

@Retention

Retention: RUNTIME; Target: ANNOTATION_TYPE; This annotation specifies the "storage type" of the annotation above which it is specified. Yes, this annotation is used even for itself... magic and that’s all.

@Target

Retention: RUNTIME; Target: ANNOTATION_TYPE; This annotation specifies the type of object over which the annotation we create can be indicated. Yes, and it is also used for yourself, get used to it... I think this is where we can complete our introduction to the standard annotations of the Java library, because the rest are used quite rarely and, although they have their own benefits, not everyone has to deal with them and is completely unnecessary. If you want me to talk about a specific annotation from the standard library (or, perhaps, annotations like @NotNull and @Nullable, which are not included in the STL), write in the comments - either kind users will answer you there, or I will when I see it. If a lot of people ask for some kind of annotation, I will also add it to the article.

Practical application of RUNTIME annotations

Actually, I think that’s enough theoretical chatter: let’s move on to practice using the example of a bot. Let's say you want to write a bot for some social network. All major networks, such as VK, Facebook, Discord, have their own APIs that allow you to write a bot. For these same networks, there are already written libraries for working with APIs, including in Java. Therefore, we will not delve into the work of any API or library. All we need to know in this example is that our bot can respond to messages sent to the chat in which our bot is actually located. That is, let's say we have a MessageListener class with a function:
public class MessageListener
{
    public void onMessageReceived(MessageReceivedEvent event)
    {
    }
}
It is responsible for processing the received message. All we need from the MessageReceivedEvent class is the string of the received message (for example, “Hello” or “Bot, hello”). It is worth considering: in different libraries these classes are called differently. I used the library for Discord. And so we want to make the bot react to some commands starting with “Bot” (with or without a comma - decide for yourself: for the sake of the lesson, we’ll assume that there shouldn’t be a comma there). That is, our function will begin with something like:
public void onMessageReceived(MessageReceivedEvent event)
{
    //Убираем чувствительность к регистру (БоТ, бОт и т.д.)
    String message = event.getMessage().toLowerCase();
    if (message.startsWith("бот"))
    {

    }
}
And now we have many options for implementing this or that command. Undoubtedly, first you need to separate the command from its arguments, that is, split it into an array.
public void onMessageReceived(MessageReceivedEvent event)
{
    //Убираем чувствительность к регистру (БоТ, бОт и т.д.)
    String message = event.getMessage().toLowerCase();
    if (message.startsWith("бот"))
    {
        try
        {
            //получим массив {"Бот", "(команду)", "аргумент1", "аргумент2",... "аргументN"};
            String[] args = message.split(" ");
            //Для удобства уберем "бот" и отделим команду от аргументов
            String command = args[1];
            String[] nArgs = Arrays.copyOfRange(args, 2, args.length);
            //Получor command = "(команда)"; nArgs = {"аргумент1", "аргумент2",..."аргументN"};
            //Данный массив может быть пустым
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            //Вывод списка команд or Howого-либо messages
            //В случае если просто написать "Бот"
        }
    }
}
There is no way we can avoid this piece of code, because separating the command from the arguments is always necessary. But then we have a choice:
  • Do if(command.equalsIngnoreCase("..."))
  • Do switch(command)
  • Do some other way of processing...
  • Or resort to the help of Annotations.
And now we have finally reached the practical part of using Annotations. Let's look at the annotation code for our task (it may differ, of course).
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//Указывает, что наша Аннотация может быть использована
//Во время выполнения через Reflection (нам How раз это нужно).
@Retention(RetentionPolicy.RUNTIME)

//Указывает, что целью нашей Аннотации является метод
//Не класс, не переменная, не поле, а именно метод.
@Target(ElementType.METHOD)
public @interface Command //Описание. Заметим, что перед interface стоит @;
{
    //Команда за которую будет отвечать функция (например "привет");
    String name();

     //Аргументы команды, использоваться будут для вывода списка команд
    String args();

     //Минимальное количество аргументов, сразу присвоor 0 (логично)
    int minArgs() default 0;

    //Описание, тоже для списка
    String desc();

     //Максимальное число аргументов. В целом не обязательно, но тоже можно использовать
    int maxArgs() default Integer.MAX_VALUE;

     //Показывать ли команду в списке (вовсе необязательная строка, но мало ли, пригодится!)
    boolean showInHelp() default true;

    //Какие команды будут считаться эквивалентными нашей
    //(Например для "привет", это может быть "Здаров", "Прив" и т.д.)
    //Под каждый случай заводить функцию - не рационально
    String[] aliases();

}
Important! Each parameter is described as a function (with parentheses). Only primitives, String , Enum can be used as parameters . You can't write List<String> args(); - error. Now that we have described the Annotation, let's create a class, call it CommandListener .
public class CommandListener
{
    @Command(name = "привет",
            args = "",
            desc = "Будь культурным, поздоровайся",
            showInHelp = false,
            aliases = {"здаров"})
    public void hello(String[] args)
    {
        //Какой-то функционал, на Ваше усмотрение.
    }

    @Command(name = "пока",
            args = "",
            desc = "",
            aliases = {"удачи"})
    public void bye(String[] args)
    {
         // Функционал
    }

    @Command(name = "помощь",
            args = "",
            desc = "Выводит список команд",
            aliases = {"help", "команды"})
    public void help(String[] args)
    {
        StringBuilder sb = new StringBuilder("Список команд: \n");
        for (Method m : this.getClass().getDeclaredMethods())
        {
            if (m.isAnnotationPresent(Command.class))
            {
                Command com = m.getAnnotation(Command.class);
                if (com.showInHelp()) //Если нужно показывать команду в списке.
                {
                    sb.append("Бот, ")
                       .append(com.name()).append(" ")
                       .append(com.args()).append(" - ")
                       .append(com.desc()).append("\n");
                }
            }
        }
        //Отправка sb.toString();

    }
}
It is worth noting one small inconvenience: t.c. We are now fighting for universality, all functions must have the same list of formal parameters, so even if the command has no arguments, the function must have a String[] args parameter . We have now described 3 commands: hello, bye, help. Now let's modify our MessageListener to do something with this. For convenience and speed of work, we will immediately store our commands in HashMap :
public class MessageListner
{
    //Map который хранит How ключ команду
    //А How meaning функцию которая будет обрабатывать команду
    private static final Map<String, Method> COMMANDS = new HashMap<>();

    //Объект класса с командами (по сути нужен нам для рефлексии)
    private static final CommandListener LISTENER = new CommandListener();

    static
    {
       //Берем список всех методов в классе CommandListener
        for (Method m : LISTENER.getClass().getDeclaredMethods())
        {
            //Смотрим, есть ли у метода нужная нам Аннотация @Command
            if (m.isAnnotationPresent(Command.class))
            {
                //Берем an object нашей Аннотации
                Command cmd = m.getAnnotation(Command.class);
                //Кладем в качестве ключа нашей карты параметр name()
                //Определенный у нашей аннотации,
                //m — переменная, хранящая наш метод
                COMMANDS.put(cmd.name(), m);

                //Также заносим каждый элемент aliases
               //Как ключ указывающий на тот же самый метод.
                for (String s : cmd.aliases())
                {
                    COMMANDS.put(s, m);
                }
            }
        }
    }

    public void onMessageReceived(MessageReceivedEvent event)
    {

        String message = event.getMessage().toLowerCase();
        if (message.startsWith("бот"))
        {
            try
            {
                String[] args = message.split(" ");
                String command = args[1];
                String[] nArgs = Arrays.copyOfRange(args, 2, args.length);
                Method m = COMMANDS.get(command);
                if (m == null)
                {
                    //(вывод помощи)
                    return;
                }
                Command com = m.getAnnotation(Command.class);
                if (nArgs.length < com.minArgs())
                {
                    //что-то если аргументов меньше чем нужно
                }
                else if (nArgs.length > com.maxArgs())
                {
                    //что-то если аргументов больше чем нужно
                }
                //Через рефлексию вызываем нашу функцию-обработчик
                //Именно потому что мы всегда передаем nArgs у функции должен быть параметр
                //String[] args — иначе она просто не будет найдена;
                m.invoke(LISTENER, nArgs);
            }
            catch (ArrayIndexOutOfBoundsException e)
            {
                //Вывод списка команд or Howого-либо messages
                //В случае если просто написать "Бот"
            }
        }
    }
}
That's all that's needed for our teams to work. Now adding a new command is not a new if, not a new case, in which the number of arguments would need to be re-calculated, and help would also have to be rewritten, adding new lines to it. Now, to add a command, we just need to add a new function with the @Command annotation in the CommandListener class and that’s it - the command is added, cases are taken into account, help is added automatically. It is absolutely indisputable that this problem can be solved in many other ways. Yes, everything that can be done with the help of annotations/reflections can be done without them, the only question is convenience, optimality and code size, of course, sticking an Annotation wherever there is the slightest hint that it will be possible to use it is also not the most rational option , in everything you need to know when to stop =). But when writing APIs, Libraries or programs in which it is possible to repeat the same type (but not exactly the same) code, annotations are undoubtedly the optimal solution.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION