JavaRush /Blog Java /Random-ES /Java @Anotaciones. ¿Qué es y cómo utilizarlo?
SemperAnte
Nivel 4
Донецк

Java @Anotaciones. ¿Qué es y cómo utilizarlo?

Publicado en el grupo Random-ES
Este artículo está dirigido a personas que nunca han trabajado con Anotaciones, pero les gustaría entender qué son y para qué se utilizan. Si tiene experiencia en esta área, no creo que este artículo amplíe de alguna manera sus conocimientos (y, de hecho, no persigo ese objetivo). Además, el artículo no es adecuado para quienes recién están comenzando a aprender el lenguaje Java. Si no comprende qué es un Map<> o HashMap<> o no sabe qué significa la entrada estática{ } dentro de una definición de clase, o nunca ha trabajado con la reflexión, es demasiado pronto para leer este artículo y Intenta entender qué son las anotaciones. Esta herramienta en sí no está creada para que la utilicen principiantes, ya que requiere una comprensión no del todo básica de la interacción de clases y objetos (mi opinión) (gracias a los comentarios por mostrar la necesidad de esta posdata). Java @Anotaciones.  ¿Qué es y cómo utilizarlo?  - 1Entonces empecemos. Las anotaciones en Java son una especie de etiquetas en el código que describen los metadatos de una función/clase/paquete. Por ejemplo, la conocida Anotación @Override, que indica que vamos a anular un método de la clase padre. Sí, por un lado, es posible sin él, pero si los padres no tienen este método, existe la posibilidad de que escribimos el código en vano, porque Es posible que nunca se llame a este método en particular, pero con la anotación @Override el compilador nos dirá que: "No encontré tal método en los padres... algo está sucio aquí". Sin embargo, las anotaciones pueden tener más que solo el significado de "para confiabilidad": pueden almacenar algunos datos que se utilizarán más adelante.

Primero, veamos las anotaciones más simples proporcionadas por la biblioteca estándar.

(gracias de nuevo a los comentarios, al principio no pensé que este bloque fuera necesario) Primero, analicemos qué son las anotaciones. Cada uno de ellos tiene 2 parámetros principales requeridos :
  • Tipo de almacenamiento (Retención);
  • El tipo de objeto sobre el que se indica (Target).

Tipo de almacenamiento

Por "tipo de almacenamiento" nos referimos a la etapa en la que nuestra anotación "sobrevive" dentro de la clase. Cada anotación tiene sólo uno de los posibles "tipos de retención" especificados en la clase RetentionPolicy :
  • FUENTE : la anotación se usa solo al escribir código y el compilador la ignora (es decir, no se guarda después de la compilación). Normalmente se utiliza para cualquier preprocesador (condicionalmente) o instrucciones para el compilador.
  • CLASE : la anotación se conserva después de la compilación, pero la JVM la ignora (es decir, no se puede utilizar en tiempo de ejecución). Normalmente se utiliza para cualquier servicio de terceros que cargue su código como una aplicación complementaria.
  • RUNTIME es una anotación que se guarda después de la compilación y la JVM la carga (es decir, se puede utilizar durante la ejecución del programa). Se utilizan como marcas en el código que afectan directamente la ejecución del programa (se analizará un ejemplo en este artículo)

El tipo de objeto arriba indicado.

Esta descripción debe tomarse casi literalmente, porque... en Java, las anotaciones se pueden especificar sobre cualquier cosa (campos, clases, funciones, etc.) y para cada anotación se indica exactamente sobre qué se puede especificar. Aquí ya no existe una regla de “una sola cosa”; se puede especificar una anotación encima de todo lo que se enumera a continuación, o puede seleccionar solo los elementos necesarios de la clase ElementType :
  • ANNOTATION_TYPE : otra anotación
  • CONSTRUCTOR - constructor de clase
  • CAMPO - campo de clase
  • LOCAL_VARIABLE - variable local
  • MÉTODO - método de clase
  • PAQUETE - descripción del paquete paquete
  • PARÁMETRO - parámetro del método public void hola(@Annontation String param){}
  • TIPO - indicado encima de la clase
En total, a partir de Java SE 1.8, la biblioteca de lenguaje estándar nos proporciona 10 anotaciones. En este artículo veremos los más comunes (¿quién está interesado en todos ellos? Bienvenido a Javadoc ):

@Anular

Retención: FUENTE; Objetivo: MÉTODO. Esta anotación muestra que el método sobre el que se escribe se hereda de la clase principal. La primera anotación con la que se encuentra todo programador novato de Java cuando utiliza un IDE que empuja persistentemente estos @Override. A menudo, los profesores de YouTube recomiendan: “bórralo para que no interfiera” o “déjalo sin preguntarte por qué está ahí”. De hecho, la anotación es más que útil: no sólo le permite comprender qué métodos se definieron en esta clase por primera vez y cuáles ya tienen los padres (lo que sin duda aumenta la legibilidad de su código), sino también esta anotación. sirve como una “autocomprobación” de que no se equivocó al definir una función sobrecargada.

@Obsoleto

Retención: tiempo de ejecución; Objetivo: CONSTRUCTOR, CAMPO, LOCAL_VARIABLE, MÉTODO, PAQUETE, PARÁMETRO, TIPO. Esta anotación identifica métodos, clases o variables que están "obsoletos" y pueden eliminarse en futuras versiones del producto. Esta anotación la suelen encontrar quienes leen la documentación de cualquier API o la misma biblioteca estándar de Java. A veces esta anotación se ignora porque... no causa ningún error y, en principio, en sí mismo no interfiere mucho con la vida. Sin embargo, el mensaje principal que transmite esta anotación es "hemos ideado un método más conveniente para implementar esta funcionalidad, úselo, no use el anterior" - bueno, o bien - "cambiamos el nombre de la función, pero esto es así, lo dejamos como legado…” (lo cual tampoco está mal en general). En resumen, si ve @Deprecated, es mejor intentar no usar lo que aparece a menos que sea absolutamente necesario, y podría valer la pena volver a leer la documentación para comprender cómo se implementa ahora la tarea realizada por el elemento obsoleto. Por ejemplo, en lugar de utilizar new Date().getYear() , se recomienda utilizar Calendar.getInstance().get(Calendar.YEAR) .

@SuppressWarnings

Retención: FUENTE; Destino: TIPO, CAMPO, MÉTODO, PARAMETRO, CONSTRUCTOR, LOCAL_VARIABLE Esta anotación deshabilita la salida de advertencias del compilador que conciernen al elemento sobre el cual se especifica. ¿Es la anotación FUENTE indicada arriba de campos, métodos y clases?

@Retención

Retención: TIEMPO DE EJECUCIÓN; Objetivo: ANNOTATION_TYPE; Esta anotación especifica el "tipo de almacenamiento" de la anotación encima de la cual se especifica. Sí, esta anotación se usa incluso para sí misma... magia y eso es todo.

@Objetivo

Retención: TIEMPO DE EJECUCIÓN; Objetivo: ANNOTATION_TYPE; Esta anotación especifica el tipo de objeto sobre el que se puede indicar la anotación que creamos. Sí, y también lo usas tú mismo, acostúmbrate... Creo que aquí es donde podemos completar nuestra introducción a las anotaciones estándar de la biblioteca Java, porque el resto se utilizan muy raramente y, aunque tienen sus propias ventajas, no todo el mundo tiene que lidiar con ellos y es completamente innecesario. Si desea que le hable sobre una anotación específica de la biblioteca estándar (o, tal vez, anotaciones como @NotNull y @Nullable, que no están incluidas en STL), escriba en los comentarios: los amables usuarios le responderán allí o Lo haré cuando lo vea. Si mucha gente me pide algún tipo de anotación, también la añadiré al artículo.

Aplicación práctica de las anotaciones RUNTIME.

En realidad, creo que ya es suficiente charla teórica: pasemos a la práctica usando el ejemplo de un bot. Digamos que quieres escribir un bot para alguna red social. Todas las redes principales, como VK, Facebook, Discord, tienen sus propias API que le permiten escribir un bot. Para estas mismas redes, ya existen bibliotecas escritas para trabajar con API, incluso en Java. Por tanto, no profundizaremos en el trabajo de ninguna API o biblioteca. Todo lo que necesitamos saber en este ejemplo es que nuestro bot puede responder a los mensajes enviados al chat en el que realmente se encuentra nuestro bot. Es decir, digamos que tenemos una clase MessageListener con una función:
public class MessageListener
{
    public void onMessageReceived(MessageReceivedEvent event)
    {
    }
}
Es responsable de procesar el mensaje recibido. Todo lo que necesitamos de la clase MessageReceivedEvent es la cadena del mensaje recibido (por ejemplo, "Hola" o "Bot, hola"). Vale la pena considerarlo: en diferentes bibliotecas estas clases se llaman de manera diferente. Usé la biblioteca para Discord. Por eso queremos hacer que el bot reaccione a algunos comandos que comienzan con "Bot" (con o sin coma; decida usted mismo: por el bien de la lección, asumiremos que no debería haber una coma allí). Es decir, nuestra función comenzará con algo como:
public void onMessageReceived(MessageReceivedEvent event)
{
    //Убираем чувствительность к регистру (БоТ, бОт и т.д.)
    String message = event.getMessage().toLowerCase();
    if (message.startsWith("бот"))
    {

    }
}
Y ahora tenemos muchas opciones para implementar tal o cual comando. Sin duda, primero es necesario separar el comando de sus argumentos, es decir, dividirlo en una matriz.
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);
            //Получo command = "(команда)"; nArgs = {"аргумент1", "аргумент2",..."аргументN"};
            //Данный массив может быть пустым
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            //Вывод списка команд o Cómoого-либо mensajes
            //В случае если просто написать "Бот"
        }
    }
}
No hay forma de evitar este fragmento de código, porque siempre es necesario separar el comando de los argumentos. Pero entonces tenemos una opción:
  • Hazlo si(command.equalsIngnoreCase("..."))
  • Cambiar (comando)
  • Hacer alguna otra forma de procesar...
  • O recurrir a la ayuda de Anotaciones.
Y ahora finalmente hemos llegado a la parte práctica del uso de Anotaciones. Veamos el código de anotación de nuestra tarea (puede diferir, por supuesto).
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

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

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

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

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

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

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

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

}
¡Importante! Cada parámetro se describe como una función (entre paréntesis). Solo se pueden utilizar como parámetros primitivos, String y Enum . No puedes escribir List<String> args(); - error. Ahora que hemos descrito la anotación, creemos una clase, llamémosla 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 la pena señalar un pequeño inconveniente: t.c. Ahora estamos luchando por la universalidad, todas las funciones deben tener la misma lista de parámetros formales, por lo que incluso si el comando no tiene argumentos, la función debe tener un parámetro String[] args . Ahora hemos descrito 3 comandos: hola, adiós, ayuda. Ahora modifiquemos nuestro MessageListener para hacer algo con esto. Para mayor comodidad y rapidez de trabajo, almacenaremos inmediatamente nuestros comandos en HashMap :
public class MessageListner
{
    //Map который хранит Cómo ключ команду
    //А Cómo significado функцию которая будет обрабатывать команду
    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))
            {
                //Берем un objeto нашей Аннотации
                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)
            {
                //Вывод списка команд o Cómoого-либо mensajes
                //В случае если просто написать "Бот"
            }
        }
    }
}
Eso es todo lo que se necesita para que nuestros equipos funcionen. Ahora bien, agregar un nuevo comando no es un caso nuevo, no es un caso nuevo, en el que sería necesario volver a calcular el número de argumentos y también sería necesario reescribir la ayuda, agregándole nuevas líneas. Ahora, para agregar un comando, solo necesitamos agregar una nueva función con la anotación @Command en la clase CommandListener y listo: se agrega el comando, se tienen en cuenta los casos y la ayuda se agrega automáticamente. Es absolutamente indiscutible que este problema puede resolverse de muchas otras maneras. Sí, todo lo que se puede hacer con la ayuda de anotaciones/reflexiones se puede hacer sin ellas, la única cuestión es la conveniencia, la optimización y el tamaño del código, por supuesto, pegando una Anotación dondequiera que haya el más mínimo indicio de que será posible utilizarla. Tampoco es la opción más racional, en todo hay que saber cuándo parar =). Pero al escribir API, bibliotecas o programas en los que es posible repetir el mismo tipo de código (pero no exactamente el mismo), las anotaciones son sin duda la solución óptima.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION