JavaRush /Java блог /Random UA /Java @Аннотації. Що це та як цим користуватися?
SemperAnte
4 рівень
Донецк

Java @Аннотації. Що це та як цим користуватися?

Стаття з групи Random UA
Ця стаття призначена для людей, які ніколи не працювали з Анотаціями, але хотіли б розібратися, що це та з чим його їдять. Якщо ж ви маєте досвід у цій сфері, не думаю, що ця стаття якось розширить ваші знання (та й, власне, такої мети я не переслідую). Також стаття не підходить для тих, хто тільки починає вивчати мову Java. Якщо Ви не розумієте що таке Map<> або HashMap<> або не знаєте що означає запис static{ } всередині визначення класу, або ніколи не працювали з рефлексією – Вам рано читати цю статтю і намагатися зрозуміти, що таке анотації. Сам по собі цей інструмент не створений для використання новачками, оскільки потребує вже не зовсім базових розуміння взаємодії класів та об'єктів.(моя думка) (дякую коментарям за те, що показали необхідність цієї приписки). Java @Аннотації.  Що це та як цим користуватися?  - 1Отже, почнемо. Анотації Java є свого роду мітками в коді, що описують метадані для функції/класу/пакета. Наприклад, всім відома інструкція @Override, що означає, що ми збираємося перевизначити метод батьківського класу. Так, з одного боку, можна і без неї, але якщо у батьків не виявиться цього методу, існує ймовірність, що ми дарма писали код, т.к. Саме цей спосіб може і викликатися ніколи, і з Інструкцією @Override компілятор нам скаже, що: " Не знайшов такого методу у батьках... щось тут нечисто " . Проте Анотації можуть нести у собі як сенс " надійності " : у яких можна зберігати якісь дані, які потім використовуватимуться.

Для початку розглянемо найпростіші інструкції, що надаються стандартною бібліотекою.

(Знову ж дякую коментарям, спочатку не подумав що цей блок потрібен) Спочатку обговоримо, які бувають анотації. Кожна з них має 2 головні обов'язкові параметри:
  • Тип зберігання (Retention);
  • Тип об'єкта з якого вона вказується (Target).

Тип зберігання

Під "типом зберігання" розуміється стадія до якої "доживає" наша інструкція всередині класу. Кожна інструкція має тільки один з можливих "типів зберігання", зазначений у класі RetentionPolicy :
  • SOURCE – анотація використовується тільки при написанні коду та ігнорується компілятором (тобто не зберігається після компіляції). Зазвичай використовується для будь-яких препроцесорів (умовно), або вказівок компілятора
  • CLASS – анотація зберігається після компіляції, проте ігнорується JVM (тобто не може бути використана під час виконання). Зазвичай використовується для будь-яких сторонніх сервісів, що підвантажують ваш код як plug-in додаток
  • RUNTIME - анотація яка зберігається після компіляції та підвантажується JVM (тобто може використовуватися під час виконання самої програми). Використовується як мітки у коді, які безпосередньо впливають на хід виконання програми (приклад буде розглянуто в даній статті)

Тип об'єкта над яким вказується

Даний опис варто розуміти буквально, т.к. Java анотації можуть вказуватися над чим завгодно (Поля, класи, функції, т.д.) і кожної анотації вказується, з чого саме може бути задана. Тут вже немає правила "щось одне", анотацію можна вказувати над усім нижче перерахованим, або вибрати тільки потрібні елементи класу ElementType :
  • ANNOTATION_TYPE - інша інструкція
  • CONSTRUCTOR – конструктор класу
  • FIELD – поле класу
  • LOCAL_VARIABLE - локальна змінна
  • METHOD – метод класу
  • PACKAGE - опис пакету package
  • PARAMETER - параметр методу public void hello (@ Annontation String param) {}
  • TYPE – вказується над класом
Загалом на момент версії Java SE 1.8 стандартна бібліотека мови надає нам 10 анотацій. У цій статті розглянемо найпоширеніші з них (кому цікаві вони всі Welcome to Javadoc ):

@Override

Retention: SOURCE; Target: METHOD. Ця інструкція показує, що метод над яким вона прописана успадкований у батьківського класу. Перша анотація з якою стикався кожен Java-програміст, що починає, при використанні IDE, яка наполегливо пхає ці @Override. Найчастіше вчителі з ютуба рекомендують або: "зітріть щоб не заважало", або: "залиште не замислюючись навіщо воно тут". Насправді анотація більш ніж корисна: вона не тільки дозволяє зрозуміти які методи були визначені в цьому класі вперше, а які вже є у батьків (що безперечно підвищує читання вашого коду), але також дана анотація служить "самоперевіркою", що ви не помаболися при визначенні функції, що перевантажується.

@Deprecated

Retention: Runtime; Target: CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE. Ця інструкція вказує на методи, класи або змінні, які є "застарілими" і можуть бути прибрані в наступних версіях продукту. З цією інструкцією зазвичай стикаються ті, хто читає документацію будь-яких API, або тієї ж стандартної бібліотеки Java. Іноді цю інструкцію ігнорують, т.к. вона не викликає жодних помилок і в принципі сама по собі сильно жити не заважає. Однак головний посил, який несе в собі дана інструкція - "ми вигадали більш зручний спосіб реалізації даного функціоналу, використовуй його, не використовуй старий" - ну, або ж - "ми перейменували функцію, а це так, для леґаси залишабо..." (що теж загалом непогано). Коротше кажучи, якщо бачите @Deprecated - краще намагатися не використовувати те, над чим вона висить, якщо в цьому немає прямої необхідності і, можливо, варто перечитати документацію, щоб зрозуміти яким чином тепер реалізується завдання, що виконується застарілим елементом. Наприклад, замість використанняnew Date().getYear() рекомендується використовувати Calendar.getInstance().get(Calendar.YEAR) .

@SuppressWarnings

Retention: SOURCE; Target: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE Ця інструкція вимикає виведення попереджень компілятора, які стосуються елемента, над яким вона вказана. Є SOURCE анотацією, що вказується над полями, методами, класами.

@Retention

Retention: RUNTIME; Target: ANNOTATION_TYPE; Дана інструкція задає "тип зберігання" інструкції над якою вона вказана. Та ця анотація використовується навіть для самої себе... магія та й годі.

@Target

Retention: RUNTIME; Target: ANNOTATION_TYPE; Ця інструкція задає тип об'єкта з якого може вказуватися створювана нами інструкція. Та й вона теж використовується для себе, звикайте... Думаю, на цьому можна завершити ознайомлення зі стандартними анотаціями бібліотеки Java, т.к. інші використовуються досить рідко і, хоч і несуть свою користь, стикатися з ними доводиться не всім і необов'язково. Якщо ж ви хочете щоб я розповів про якусь конкретну інструкцію зі стандартної бібліотеки (або, можливо, інструкції типу @NotNull і @Nullable які в STL не входять) напишіть у коментарях - або вам там дадуть відповідь добрі користувачі, або я коли побачу. Якщо вже багато людей будуть просити якусь інструкцію - також внесу її до статті.

Практичне застосування RUNTIME анотацій

Власне, думаю, вистачить теоретичної балаканини: давайте перейдемо до практики на прикладі робота. Допустимо ви хочете написати бота для якоїсь соцмережі. Всі великі мережі, такі як ВК, Facebook, Discord, мають свої API, які дозволяють написати бота. Для цих мереж є вже написані бібліотеки для роботи з API, на мові Java в тому числі. Тому не заглиблюватимемося в роботу будь-якого API або бібліотеки. Все, що нам потрібно знати в цьому прикладі - те, що наш робот вміє реагувати на повідомлення, надіслані в чат, в якому, власне, наш бот знаходиться. Тобто припустимо, у нас є клас MessageListener з функцією:
public class MessageListener
{
    public void onMessageReceived(MessageReceivedEvent event)
    {
    }
}
Вона відповідає за обробку прийнятого повідомлення. Все, що нам потрібно від класу MessageReceivedEvent - рядок отриманого повідомлення (наприклад, "Привіт" або "Біт, привіт"). Варто зважити: у різних бібліотеках ці класи називаються по-різному. Я використав бібліотеку для Discord. І ось ми хочемо зробити так, щоб бот реагував на якісь команди, що починаються з "Бот" (з комою чи без - вирішуйте самі: для уроку припустимо, що комою там бути не повинно). Тобто вже наша функція буде починатися з чогось на кшталт:
public void onMessageReceived(MessageReceivedEvent event)
{
    //Убираем чувствительность к регистру (БоТ, бОт и т.д.)
    String message = event.getMessage().toLowerCase();
    if (message.startsWith("бот"))
    {

    }
}
І ось тепер маємо безліч варіантів реалізації тієї чи іншої команди. Безперечно, спочатку потрібно відокремити команду від її аргументів, тобто розбити на масив.
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);
            //Получабо command = "(команда)"; nArgs = {"аргумент1", "аргумент2",..."аргументN"};
            //Данный массив может быть пустым
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            //Вывод списка команд або якого-либо повідомлення
            //В случае если просто написать "Бот"
        }
    }
}
Цього шматка коду нам ніяк не уникнути, тому що відокремлення команди від аргументів потрібно завжди. А ось далі вже маємо вибір:
  • Зробити if(command.equalsIngnoreCase("..."))
  • Зробити switch(command)
  • Зробити ще якийсь спосіб...
  • Або ж вдатися до допомоги Анотацій.
І ось ми нарешті дійшли практичної частини використання Анотацій. Давайте розглянемо код анотації нашого завдання (він може відрізнятися, звичайно ж).
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

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

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

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

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

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

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

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

}
Важливо! Кожен параметр описується як функція (з круглими дужками). Як параметри можуть бути використані тільки примітиви, String , Enum . Не можна написати List<String> args(); - Помилка. Тепер, коли ми описали Анотацію, заведемо клас, назвемо його 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();

    }
}
Варто відзначити одну невелику незручність: т.к. ми зараз боремося за універсальність, всі функції повинні мати однаковий список формальних параметрів, тому навіть якщо команда не має аргументів, функція повинна мати параметр String[] args . Ми зараз описали 3 команди: привіт, поки що, допомога. Тепер давайте модифікуємо наш MessageListener так, щоб він якось із цим працював. Для зручності та швидкості роботи, будемо відразу зберігати наші команди в HashMap :
public class MessageListner
{
    //Map который хранит як ключ команду
    //А як значення функцию которая будет обрабатывать команду
    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))
            {
                //Берем об'єкт нашей Аннотации
                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)
            {
                //Вывод списка команд або якого-либо повідомлення
                //В случае если просто написать "Бот"
            }
        }
    }
}
Ось, власне, і все, що потрібно, щоб наші команди працювали. Тепер додавання нової команди - це не новий if, не новий case, в яких потрібно було б заново перевчити кількість аргументів, також довелося б переписувати help, додаючи до нього нові рядки. Тепер, щоб додати команду, нам потрібно просто в класі CommandListener додати нову функцію з анотацією @Command і все - команда додана, випадки враховані, help доповнений автоматично. Абсолютно безперечно, що це завдання можна вирішити безліччю інших шляхів. Так, все що можна зробити за допомогою анотацій/рефлексій можна зробити і без них, питання лише у зручності, оптимальності та розмірах коду, звичайно ж, пхати Анотацію скрізь де є найменший натяк на те, що вийде її використовувати - теж не найраціональніший варіант , у всьому потрібно знати міру =). Але при написанні API,
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ