JavaRush /Blog Java /Random-FR /Java @Annotations. Qu'est-ce que c'est et comment l'utili...
SemperAnte
Niveau 4
Донецк

Java @Annotations. Qu'est-ce que c'est et comment l'utiliser ?

Publié dans le groupe Random-FR
Cet article est destiné aux personnes qui n'ont jamais travaillé avec Annotations, mais qui souhaitent comprendre de quoi il s'agit et à quoi il sert. Si vous avez de l'expérience dans ce domaine, je ne pense pas que cet article élargira vos connaissances d'une manière ou d'une autre (et, en fait, je ne poursuis pas un tel objectif). De plus, l'article ne convient pas à ceux qui commencent tout juste à apprendre le langage Java. Si vous ne comprenez pas ce qu'est Map<> ou HashMap<> ou si vous ne savez pas ce que signifie l' entrée static{ } dans une définition de classe, ou si vous n'avez jamais travaillé avec la réflexion, il est trop tôt pour lire cet article et essayez de comprendre ce que sont les annotations. Cet outil lui-même n'est pas créé pour être utilisé par des débutants, car il nécessite une compréhension pas tout à fait basique de l'interaction des classes et des objets (mon avis) (grâce aux commentaires pour avoir montré la nécessité de ce post-scriptum). Java @Annotations.  Qu'est-ce que c'est et comment l'utiliser ?  - 1Alors, commençons. Les annotations en Java sont une sorte d'étiquettes dans le code qui décrivent les métadonnées d'une fonction/classe/package. Par exemple, la célèbre @Override Annotation, qui indique que l'on va surcharger une méthode de la classe parent. Oui, d'une part, c'est possible sans cela, mais si les parents n'ont pas cette méthode, il est possible que nous ayons écrit le code en vain, car Cette méthode particulière ne sera peut-être jamais appelée, mais avec l'annotation @Override, le compilateur nous dira que : "Je n'ai pas trouvé une telle méthode chez les parents... quelque chose est sale ici." Cependant, les annotations peuvent avoir plus que la simple signification de « pour la fiabilité » : elles peuvent stocker certaines données qui seront utilisées ultérieurement.

Examinons d’abord les annotations les plus simples fournies par la bibliothèque standard.

(merci encore aux commentaires, au début je ne pensais pas que ce bloc était nécessaire) Tout d'abord, discutons de ce que sont les annotations. Chacun d'eux a 2 paramètres principaux requis :
  • Type de stockage (rétention) ;
  • Le type de l'objet sur lequel il est indiqué (Target).

Type de stockage

Par « type de stockage », nous entendons l'étape à laquelle notre annotation « survit » à l'intérieur de la classe. Chaque annotation n'a qu'un seul des "types de rétention" possibles spécifiés dans la classe RetentionPolicy :
  • SOURCE - l'annotation est utilisée uniquement lors de l'écriture du code et est ignorée par le compilateur (c'est-à-dire qu'elle n'est pas enregistrée après la compilation). Généralement utilisé pour tous les préprocesseurs (sous condition) ou les instructions destinées au compilateur
  • CLASS - l'annotation est conservée après la compilation, mais est ignorée par la JVM (c'est-à-dire qu'elle ne peut pas être utilisée au moment de l'exécution). Généralement utilisé pour tout service tiers qui charge votre code en tant qu'application plug-in
  • RUNTIME est une annotation enregistrée après compilation et chargée par la JVM (c'est-à-dire qui peut être utilisée lors de l'exécution du programme lui-même). Utilisé comme marques dans le code qui affectent directement l'exécution du programme (un exemple sera discuté dans cet article)

Le type d'objet au-dessus duquel est indiqué

Cette description doit être prise presque au pied de la lettre, car... en Java, les annotations peuvent être spécifiées sur n'importe quoi (champs, classes, fonctions, etc.) et pour chaque annotation, il est indiqué sur quoi exactement elle peut être spécifiée. Il n'y a plus de règle « une chose » ici ; une annotation peut être spécifiée au-dessus de tout ce qui est listé ci-dessous, ou vous pouvez sélectionner uniquement les éléments nécessaires de la classe ElementType :
  • ANNOTATION_TYPE - une autre annotation
  • CONSTRUCTEUR - constructeur de classe
  • FIELD - champ de classe
  • LOCAL_VARIABLE - variable locale
  • METHODE - méthode de classe
  • FORFAIT - description du forfait forfait
  • PARAMETER - paramètre de méthode public void hello (@Annontation String param){}
  • TYPE - indiqué au dessus de la classe
Au total, depuis Java SE 1.8, la bibliothèque du langage standard nous fournit 10 annotations. Dans cet article, nous examinerons les plus courants d'entre eux (qui sont intéressés par tous ? Bienvenue sur Javadoc ) :

@Passer outre

Rétention : SOURCE ; Cible : MÉTHODE. Cette annotation montre que la méthode sur laquelle elle est écrite est héritée de la classe parent. La première annotation que tout programmeur Java novice rencontre lorsqu'il utilise un IDE qui pousse de manière persistante ces @Override. Souvent, les professeurs de YouTube recommandent soit : « de l’effacer pour qu’il ne gêne pas », soit : « de le laisser sans se demander pourquoi il est là ». En fait, l'annotation est plus qu'utile : elle permet non seulement de comprendre quelles méthodes ont été définies dans cette classe pour la première fois, et lesquelles les parents possèdent déjà (ce qui augmente sans doute la lisibilité de votre code), mais aussi cette annotation sert d'« auto-vérification » pour vérifier que vous ne vous êtes pas trompé en définissant une fonction surchargée.

@Obsolète

Rétention : durée d'exécution ; Cible : CONSTRUCTEUR, CHAMP, LOCAL_VARIABLE, MÉTHODE, PACKAGE, PARAMÈTRE, TYPE. Cette annotation identifie les méthodes, classes ou variables « obsolètes » et susceptibles d'être supprimées dans les futures versions du produit. Cette annotation est généralement rencontrée par ceux qui lisent la documentation de n'importe quelle API ou la même bibliothèque Java standard. Parfois, cette annotation est ignorée car... cela ne provoque aucune erreur et, en principe, n'interfère pas beaucoup avec la vie. Cependant, le message principal que véhicule cette annotation est "nous avons mis au point une méthode plus pratique pour implémenter cette fonctionnalité, utilisez-la, n'utilisez pas l'ancienne" - enfin, ou bien - "nous avons renommé la fonction, mais cette c'est ainsi que nous l'avons laissé en héritage... » (ce qui n'est pas mal non plus dans l'ensemble). En bref, si vous voyez @Deprecated, il vaut mieux essayer de ne pas utiliser ce sur quoi il pèse à moins que ce ne soit absolument nécessaire, et cela vaut peut-être la peine de relire la documentation pour comprendre comment la tâche effectuée par l'élément obsolète est désormais implémentée. Par exemple, au lieu d'utiliser new Date().getYear() il est recommandé d'utiliser Calendar.getInstance().get(Calendar.YEAR) .

@Supprimer les avertissements

Rétention : SOURCE ; Cible : TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE Cette annotation désactive la sortie des avertissements du compilateur qui concernent l'élément sur lequel elle est spécifiée. Est l'annotation SOURCE indiquée au-dessus des champs, méthodes, classes.

@Rétention

Rétention : DURÉE D'EXÉCUTION ; Cible : ANNOTATION_TYPE ; Cette annotation précise le « type de stockage » de l'annotation au-dessus de laquelle elle est spécifiée. Oui, cette annotation est utilisée même pour elle-même... la magie et c'est tout.

@Cible

Rétention : DURÉE D'EXÉCUTION ; Cible : ANNOTATION_TYPE ; Cette annotation spécifie le type d'objet sur lequel l'annotation que nous créons peut être indiquée. Oui, et cela sert aussi à vous-même, habituez-vous... Je pense que c'est ici que nous pouvons terminer notre introduction aux annotations standards de la bibliothèque Java, car les autres sont utilisés assez rarement et, bien qu'ils aient leurs propres avantages, tout le monde n'a pas à s'en occuper et est totalement inutile. Si vous voulez que je parle d'une annotation spécifique de la bibliothèque standard (ou, peut-être, d'annotations comme @NotNull et @Nullable, qui ne sont pas incluses dans la STL), écrivez dans les commentaires - soit des utilisateurs aimables vous y répondront, soit Je le ferai quand je le verrai. Si beaucoup de gens demandent une sorte d'annotation, je l'ajouterai également à l'article.

Application pratique des annotations RUNTIME

En fait, je pense que c’est assez de bavardages théoriques : passons à la pratique en utilisant l’exemple d’un bot. Disons que vous souhaitez écrire un bot pour un réseau social. Tous les grands réseaux, tels que VK, Facebook, Discord, disposent de leurs propres API qui vous permettent d'écrire un bot. Pour ces mêmes réseaux, il existe déjà des bibliothèques écrites pour travailler avec des API, y compris en Java. Par conséquent, nous n’entrerons pas dans le travail d’une API ou d’une bibliothèque. Tout ce que nous devons savoir dans cet exemple, c'est que notre bot peut répondre aux messages envoyés au chat dans lequel se trouve réellement notre bot. Autrement dit, disons que nous avons une classe MessageListener avec une fonction :
public class MessageListener
{
    public void onMessageReceived(MessageReceivedEvent event)
    {
    }
}
Il est responsable du traitement du message reçu. Tout ce dont nous avons besoin de la classe MessageReceivedEvent est la chaîne du message reçu (par exemple, « Bonjour » ou « Bot, bonjour »). Cela vaut la peine d'être pris en compte : dans différentes bibliothèques, ces classes sont appelées différemment. J'ai utilisé la bibliothèque pour Discord. Nous voulons donc faire réagir le bot à certaines commandes commençant par « Bot » (avec ou sans virgule - décidez vous-même : pour le bien de cette leçon, nous supposerons qu'il ne devrait pas y avoir de virgule ici). Autrement dit, notre fonction commencera par quelque chose comme :
public void onMessageReceived(MessageReceivedEvent event)
{
    //Убираем чувствительность к регистру (БоТ, бОт и т.д.)
    String message = event.getMessage().toLowerCase();
    if (message.startsWith("бот"))
    {

    }
}
Et maintenant, nous disposons de nombreuses options pour implémenter telle ou telle commande. Sans aucun doute, vous devez d'abord séparer la commande de ses arguments, c'est-à-dire la diviser en un tableau.
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
            //В случае если просто написать "Бот"
        }
    }
}
Nous ne pouvons en aucun cas éviter ce morceau de code, car séparer la commande des arguments est toujours nécessaire. Mais alors nous avons le choix :
  • Faire if(command.equalsIngnoreCase("..."))
  • Changer (commande)
  • Faites une autre façon de traiter...
  • Ou recourir à l'aide des annotations.
Et maintenant, nous avons enfin atteint la partie pratique de l’utilisation des annotations. Regardons le code d'annotation de notre tâche (il peut différer, bien sûr).
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! Chaque paramètre est décrit comme une fonction (avec parenthèses). Seules les primitives String , Enum peuvent être utilisées comme paramètres . Vous ne pouvez pas écrire List<String> args(); - erreur. Maintenant que nous avons décrit l'annotation, créons une classe, appelons-la 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();

    }
}
Il convient de noter un petit inconvénient : t.c. Nous luttons désormais pour l'universalité, toutes les fonctions doivent avoir la même liste de paramètres formels, donc même si la commande n'a pas d'arguments, la fonction doit avoir un paramètre String[] args . Nous avons maintenant décrit 3 commandes : bonjour, au revoir, aide. Modifions maintenant notre MessageListener pour faire quelque chose avec cela. Pour plus de commodité et de rapidité de travail, nous stockerons immédiatement nos commandes dans 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
                //В случае если просто написать "Бот"
            }
        }
    }
}
C'est tout ce dont nos équipes ont besoin pour travailler. Désormais, l'ajout d'une nouvelle commande n'est pas un nouveau if, ni un nouveau cas, dans lequel le nombre d'arguments devrait être recalculé, et l'aide devrait également être réécrite, en y ajoutant de nouvelles lignes. Maintenant, pour ajouter une commande, il suffit d'ajouter une nouvelle fonction avec l'annotation @Command dans la classe CommandListener et c'est tout - la commande est ajoutée, les cas sont pris en compte, l'aide est ajoutée automatiquement. Il est absolument incontestable que ce problème peut être résolu de bien d’autres manières. Oui, tout ce qui peut être fait à l'aide d'annotations/réflexions peut être fait sans elles, la seule question est la commodité, l'optimalité et la taille du code, bien sûr, en collant une annotation partout où il y a le moindre indice qu'il sera possible d'utiliser ce n'est pas non plus l'option la plus rationnelle, dans tout il faut savoir s'arrêter =). Mais lors de l’écriture d’API, de bibliothèques ou de programmes dans lesquels il est possible de répéter le même type de code (mais pas exactement le même), les annotations sont sans aucun doute la solution optimale.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION