JavaRush /Blogue Java /Random-PT /Java @Anotações. O que é e como usá-lo?
SemperAnte
Nível 4
Донецк

Java @Anotações. O que é e como usá-lo?

Publicado no grupo Random-PT
Este artigo é destinado a pessoas que nunca trabalharam com Anotações, mas gostariam de entender o que é e para que serve. Se você tem experiência nesta área, não creio que este artigo vá de alguma forma ampliar o seu conhecimento (e, de fato, não persigo esse objetivo). Além disso, o artigo não é adequado para quem está começando a aprender a linguagem Java. Se você não entende o que é Map<> ou HashMap<> ou não sabe o que significa a entrada static{ } dentro de uma definição de classe, ou nunca trabalhou com reflexão, é muito cedo para você ler este artigo e tente entender o que são anotações. Esta ferramenta em si não foi criada para ser usada por iniciantes, pois requer um conhecimento não totalmente básico da interação de classes e objetos (minha opinião) (graças aos comentários por mostrarem a necessidade deste postscript). Java @Anotações.  O que é e como usá-lo?  - 1Então vamos começar. Anotações em Java são um tipo de rótulo no código que descreve os metadados de uma função/classe/pacote. Por exemplo, a conhecida anotação @Override, que indica que vamos sobrescrever um método da classe pai. Sim, por um lado é possível sem ele, mas se os pais não tiverem esse método, existe a possibilidade de termos escrito o código em vão, porque Este método específico pode nunca ser chamado, mas com a anotação @Override o compilador nos dirá que: “Não encontrei tal método nos pais... algo está sujo aqui”. No entanto, as Anotações podem carregar mais do que apenas o significado de “para confiabilidade”: elas podem armazenar alguns dados que serão usados ​​posteriormente.

Primeiro, vejamos as anotações mais simples fornecidas pela biblioteca padrão.

(obrigado novamente aos comentários, a princípio não achei que esse bloco fosse necessário) Primeiro, vamos discutir o que são anotações. Cada um deles possui 2 parâmetros principais obrigatórios :
  • Tipo de armazenamento (Retenção);
  • O tipo do objeto sobre o qual está indicado (Target).

Tipo de armazenamento

Por “tipo de armazenamento” queremos dizer o estágio ao qual nossa anotação “sobrevive” dentro da classe. Cada anotação possui apenas um dos possíveis "tipos de retenção" especificados na classe RetentionPolicy :
  • SOURCE - a anotação é usada apenas ao escrever código e é ignorada pelo compilador (ou seja, não é salva após a compilação). Normalmente usado para qualquer pré-processador (condicionalmente) ou instruções para o compilador
  • CLASSE - a anotação é preservada após a compilação, mas é ignorada pela JVM (ou seja, não pode ser usada em tempo de execução). Normalmente usado para serviços de terceiros que carregam seu código como um aplicativo de plug-in
  • RUNTIME é uma anotação que é salva após a compilação e carregada pela JVM (ou seja, pode ser usada durante a execução do próprio programa). Usado como marcas no código que afetam diretamente a execução do programa (um exemplo será discutido neste artigo)

O tipo de objeto acima do qual é indicado

Esta descrição deve ser interpretada quase literalmente, porque... em Java, as anotações podem ser especificadas sobre qualquer coisa (campos, classes, funções, etc.) e para cada anotação é indicado exatamente sobre o que ela pode ser especificada. Não existe mais uma regra de “uma coisa” aqui; uma anotação pode ser especificada acima de tudo listado abaixo, ou você pode selecionar apenas os elementos necessários da classe ElementType :
  • ANNOTATION_TYPE - outra anotação
  • CONSTRUTOR - construtor de classe
  • CAMPO - campo de classe
  • LOCAL_VARIABLE - variável local
  • MÉTODO - método de classe
  • PACOTE - descrição do pacote do pacote
  • PARÂMETRO - parâmetro do método public void hello(@Annontation String param){}
  • TIPO - indicado acima da classe
No total, a partir do Java SE 1.8, a biblioteca da linguagem padrão nos fornece 10 anotações. Neste artigo veremos os mais comuns deles (quem está interessado em todos eles? Bem-vindo ao Javadoc ):

@Sobrepor

Retenção: FONTE; Alvo: MÉTODO. Esta anotação mostra que o método sobre o qual foi escrito é herdado da classe pai. A primeira anotação que todo programador Java novato encontra ao usar um IDE que envia persistentemente esses @Override. Muitas vezes, os professores do YouTube recomendam: “apague para não atrapalhar” ou: “deixe sem se perguntar por que está ali”. Na verdade, a anotação é mais do que útil: ela não só permite entender quais métodos foram definidos nesta classe pela primeira vez e quais os pais já possuem (o que sem dúvida aumenta a legibilidade do seu código), mas também esta anotação serve como uma “autoverificação” de que você não se enganou ao definir uma função sobrecarregada.

@Descontinuada

Retenção: Tempo de execução; Alvo: CONSTRUTOR, CAMPO, LOCAL_VARIABLE, MÉTODO, PACOTE, PARÂMETRO, TIPO. Esta anotação identifica métodos, classes ou variáveis ​​que estão "obsoletos" e podem ser removidos em versões futuras do produto. Essa anotação geralmente é encontrada por quem lê a documentação de qualquer API ou da mesma biblioteca Java padrão. Às vezes, essa anotação é ignorada porque... não causa erros e, em princípio, por si só não interfere muito na vida. Porém, a principal mensagem que esta anotação carrega é “nós criamos um método mais conveniente para implementar esta funcionalidade, use-o, não use o antigo” - bem, ou então - “nós renomeamos a função, mas isso é assim, deixamos para legado...” (o que geralmente também não é ruim). Resumindo, se você vir @Deprecated, é melhor tentar não usar o que está pendurado, a menos que seja absolutamente necessário, e pode valer a pena reler a documentação para entender como a tarefa executada pelo elemento obsoleto agora é implementada. Por exemplo, em vez de usar new Date().getYear() é recomendado usar Calendar.getInstance().get(Calendar.YEAR) .

@Suprimir avisos

Retenção: FONTE; Destino: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE Esta anotação desabilita a saída de avisos do compilador que dizem respeito ao elemento sobre o qual ela é especificada. A anotação SOURCE está indicada acima dos campos, métodos, classes.

@Retenção

Retenção: TEMPO DE EXECUÇÃO; Alvo: ANNOTATION_TYPE; Esta anotação especifica o "tipo de armazenamento" da anotação acima da qual é especificada. Sim, esta anotação é usada até para si mesma... magia e isso é tudo.

@Alvo

Retenção: TEMPO DE EXECUÇÃO; Alvo: ANNOTATION_TYPE; Esta anotação especifica o tipo de objeto sobre o qual a anotação que criamos pode ser indicada. Sim, e também é usado para você, acostume-se... Acho que é aqui que podemos completar nossa introdução às anotações padrão da biblioteca Java, porque o resto raramente é utilizado e, embora tenha benefícios próprios, nem todos têm que lidar com eles e são totalmente desnecessários. Se você quiser que eu fale sobre uma anotação específica da biblioteca padrão (ou, talvez, anotações como @NotNull e @Nullable, que não estão incluídas no STL), escreva nos comentários - qualquer usuário gentil responderá lá, ou Eu irei quando eu ver. Se muita gente pedir algum tipo de anotação, também irei adicioná-la ao artigo.

Aplicação prática de anotações RUNTIME

Na verdade, acho que já chega de conversa teórica: vamos passar à prática usando o exemplo de um bot. Digamos que você queira escrever um bot para alguma rede social. Todas as principais redes, como VK, Facebook, Discord, têm suas próprias APIs que permitem escrever um bot. Para essas mesmas redes, já existem bibliotecas escritas para trabalhar com APIs, inclusive em Java. Portanto, não nos aprofundaremos no trabalho de nenhuma API ou biblioteca. Tudo o que precisamos saber neste exemplo é que nosso bot pode responder às mensagens enviadas para o chat onde nosso bot está realmente localizado. Ou seja, digamos que temos uma classe MessageListener com uma função:
public class MessageListener
{
    public void onMessageReceived(MessageReceivedEvent event)
    {
    }
}
É responsável por processar a mensagem recebida. Tudo o que precisamos da classe MessageReceivedEvent é a string da mensagem recebida (por exemplo, “Hello” ou “Bot, hello”). Vale a pena considerar: em diferentes bibliotecas essas classes têm nomes diferentes. Usei a biblioteca do Discord. E então queremos fazer o bot reagir a alguns comandos começando com “Bot” (com ou sem vírgula - decida por si mesmo: para o bem desta lição, assumiremos que não deveria haver vírgula ali). Ou seja, nossa função começará com algo como:
public void onMessageReceived(MessageReceivedEvent event)
{
    //Убираем чувствительность к регистру (БоТ, бОт и т.д.)
    String message = event.getMessage().toLowerCase();
    if (message.startsWith("бот"))
    {

    }
}
E agora temos muitas opções para implementar este ou aquele comando. Sem dúvida, primeiro você precisa separar o comando de seus argumentos, ou seja, dividi-lo em um 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
            //В случае если просто написать "Бот"
        }
    }
}
Não há como evitar esse trecho de código, pois é sempre necessário separar o comando dos argumentos. Mas então temos uma escolha:
  • Faça if(command.equalsIngnoreCase("..."))
  • Faça a troca (comando)
  • Faça alguma outra forma de processamento ...
  • Ou recorra à ajuda de Anotações.
E agora finalmente chegamos à parte prática do uso de Anotações. Vejamos o código de anotação da nossa tarefa (pode ser diferente, é claro).
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();

}
Importante! Cada parâmetro é descrito como uma função (entre parênteses). Apenas primitivos, String , Enum podem ser usados ​​como parâmetros . Você não pode escrever List<String> args(); - erro. Agora que descrevemos a Anotação, vamos criar uma classe, chamada 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();

    }
}
Vale a pena notar um pequeno inconveniente: t.c. Estamos agora lutando pela universalidade, todas as funções devem ter a mesma lista de parâmetros formais, então mesmo que o comando não tenha argumentos, a função deve ter um parâmetro String[] args . Descrevemos agora 3 comandos: olá, tchau, ajuda. Agora vamos modificar nosso MessageListener para fazer algo com isso. Para maior comodidade e rapidez de trabalho, armazenaremos imediatamente nossos comandos no 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
                //В случае если просто написать "Бот"
            }
        }
    }
}
Isso é tudo o que é necessário para que nossas equipes trabalhem. Agora adicionar um novo comando não é um novo se, não é um novo caso, no qual o número de argumentos precisaria ser recalculado, e a ajuda também teria que ser reescrita, adicionando novas linhas a ele. Agora, para adicionar um comando, basta adicionar uma nova função com a anotação @Command na classe CommandListener e pronto - o comando é adicionado, os casos são levados em consideração, a ajuda é adicionada automaticamente. É absolutamente indiscutível que este problema pode ser resolvido de muitas outras maneiras. Sim, tudo o que pode ser feito com a ajuda de anotações/reflexões pode ser feito sem elas, a única questão é conveniência, otimalidade e tamanho do código, claro, colando uma Anotação onde houver o menor indício de que será possível usar também não é a opção mais racional, em tudo é preciso saber quando parar =). Mas ao escrever APIs, bibliotecas ou programas nos quais é possível repetir o mesmo tipo (mas não exatamente o mesmo) código, as anotações são sem dúvida a solução ideal.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION