JavaRush /Blog Java /Random-PL /@Adnotacje w Javie. Co to jest i jak z niego korzystać?
SemperAnte
Poziom 4
Донецк

@Adnotacje w Javie. Co to jest i jak z niego korzystać?

Opublikowano w grupie Random-PL
Ten artykuł jest przeznaczony dla osób, które nigdy nie pracowały z Adnotacjami, ale chciałyby zrozumieć, czym jest i do czego służy. Jeśli masz doświadczenie w tym obszarze, nie sądzę, że ten artykuł w jakiś sposób poszerzy Twoją wiedzę (a właściwie nie dążę do takiego celu). Artykuł nie jest także odpowiedni dla osób, które dopiero zaczynają uczyć się języka Java. Jeśli nie rozumiesz, czym jest Map<> lub HashMap<> lub nie wiesz, co oznacza wpis static{ } w definicji klasy, albo nigdy nie pracowałeś z refleksją, jest za wcześnie, abyś przeczytał ten artykuł i spróbuj zrozumieć, czym są adnotacje. Samo to narzędzie nie jest stworzone do użytku przez początkujących, ponieważ wymaga nie do końca podstawowego zrozumienia interakcji klas i obiektów (moim zdaniem) (dzięki komentarzom za wykazanie potrzeby tego postscriptum). @Adnotacje w Javie.  Co to jest i jak z niego korzystać?  - 1Więc zacznijmy. Adnotacje w Javie to rodzaj etykiet w kodzie opisujących metadane funkcji/klasy/pakietu. Na przykład dobrze znana adnotacja @Override, która wskazuje, że zamierzamy zastąpić metodę klasy nadrzędnej. Tak, z jednej strony da się bez tego, ale jeśli rodzice nie mają tej metody, istnieje możliwość, że na próżno pisaliśmy kod, bo Ta konkretna metoda może nigdy nie zostać wywołana, ale dzięki adnotacji @Override kompilator powie nam, że: „Nie znalazłem takiej metody w rodzicach… coś tu jest nie tak”. Adnotacje mogą jednak nieść więcej niż tylko znaczenie „ze względu na niezawodność”: mogą przechowywać pewne dane, które zostaną później wykorzystane.

Najpierw przyjrzyjmy się najprostszym adnotacjom udostępnianym przez standardową bibliotekę.

(ponownie dzięki komentarzom, początkowo nie sądziłem, że ten blok jest potrzebny) Najpierw omówmy, czym są adnotacje. Każdy z nich ma 2 główne wymagane parametry:
  • Typ przechowywania (Retencja);
  • Typ obiektu, nad którym jest wskazany (Cel).

Typ składowania

Przez „typ przechowywania” rozumiemy etap, do którego „przetrwa” nasza adnotacja w klasie. Każda adnotacja ma tylko jeden z możliwych „typów przechowywania” określonych w klasie RetentionPolicy :
  • ŹRÓDŁO - adnotacja używana jest tylko podczas pisania kodu i jest ignorowana przez kompilator (tzn. nie jest zapisywana po kompilacji). Zwykle używane w przypadku dowolnych preprocesorów (warunkowo) lub instrukcji dla kompilatora
  • KLASA – adnotacja zostaje zachowana po kompilacji, ale jest ignorowana przez maszynę JVM (tj. nie można jej użyć w czasie wykonywania). Zwykle używany w przypadku usług innych firm, które ładują Twój kod jako aplikację wtyczki
  • RUNTIME to adnotacja, która jest zapisywana po kompilacji i ładowana przez maszynę JVM (tzn. może być używana podczas wykonywania samego programu). Używane jako znaczniki w kodzie, które bezpośrednio wpływają na wykonanie programu (przykład zostanie omówiony w tym artykule)

Rodzaj obiektu, który jest wskazany powyżej

Opis ten należy traktować niemal dosłownie, bo... w Javie adnotacje można określić nad wszystkim (pola, klasy, funkcje itp.), a dla każdej adnotacji jest wskazane, na czym dokładnie można ją określić. Nie ma tu już reguły „jednej rzeczy”; adnotację można umieścić nad wszystkim, co jest wymienione poniżej, lub można wybrać tylko niezbędne elementy klasy ElementType :
  • ANNOTATION_TYPE – kolejna adnotacja
  • KONSTRUKTOR - konstruktor klasy
  • POLE - pole klasy
  • LOCAL_VARIABLE - zmienna lokalna
  • METODA - metoda klasowa
  • PAKIET - opis pakietu pakietu
  • PARAMETR - parametr metody public void hello(@Annontation String param){}
  • TYP – wskazany nad klasą
W sumie od wersji Java SE 1.8 standardowa biblioteka językowa udostępnia nam 10 adnotacji. W tym artykule przyjrzymy się najczęstszym z nich (kogo interesują wszystkie z nich? Witamy w Javadoc ):

@Nadpisanie

Zatrzymanie: ŹRÓDŁO; Cel: METODA. Ta adnotacja pokazuje, że metoda, za pomocą której jest napisana, jest dziedziczona z klasy nadrzędnej. Pierwsza adnotacja, z którą spotyka się każdy początkujący programista Java, korzystając z IDE, które uporczywie wypycha te @Override. Często nauczyciele YouTube zalecają: „wymaż to, żeby nie przeszkadzało” albo: „zostaw to, nie zastanawiając się, dlaczego tam jest”. Tak naprawdę adnotacja jest więcej niż przydatna: pozwala nie tylko zrozumieć, które metody zostały zdefiniowane w tej klasie po raz pierwszy, a które rodzice już mają (co niewątpliwie zwiększa czytelność Twojego kodu), ale także ta adnotacja służy jako „samosprawdzenie”, czy nie pomyliłeś się przy definiowaniu przeciążonej funkcji.

@Przestarzałe

Przechowywanie: Czas działania; Cel: KONSTRUKTOR, POLE, ZMIENNA_LOKALNA, METODA, PAKIET, PARAMETR, TYP. Ta adnotacja identyfikuje metody, klasy lub zmienne, które są „przestarzałe” i mogą zostać usunięte w przyszłych wersjach produktu. Z tą adnotacją zwykle spotykają się osoby czytające dokumentację dowolnych interfejsów API lub tę samą standardową bibliotekę Java. Czasami ta adnotacja jest ignorowana, ponieważ... nie powoduje żadnych błędów i w zasadzie sam w sobie nie ingeruje zbytnio w życie. Jednak główny przekaz, jaki niesie ta adnotacja, brzmi: „wymyśliliśmy wygodniejszą metodę implementacji tej funkcjonalności, używajcie jej, nie używajcie starej” – no cóż, albo inaczej – „zmieniliśmy nazwę funkcji, ale to tak jest, zostawiliśmy to na dziedzictwo…” (co też ogólnie nie jest złe). Krótko mówiąc, jeśli zobaczysz @Deprecated, lepiej spróbować nie używać tego, na czym się zawiesza, chyba że jest to absolutnie konieczne, i może warto ponownie przeczytać dokumentację, aby zrozumieć, w jaki sposób zadanie wykonywane przez przestarzały element jest teraz zaimplementowane. Na przykład zamiast używać new Date().getYear() zaleca się użycie Calendar.getInstance().get(Calendar.YEAR) .

@Tłumić ostrzeżenia

Zatrzymanie: ŹRÓDŁO; Cel: TYPE, FIELD, METOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE Ta adnotacja wyłącza wyświetlanie ostrzeżeń kompilatora dotyczących elementu, dla którego jest określona. Czy adnotacja ŹRÓDŁO jest wskazana powyżej pól, metod, klas.

@Zatrzymanie

Przechowywanie: CZAS PRACY; Cel: ANNOTATION_TYPE; Ta adnotacja określa „typ przechowywania” adnotacji, powyżej której jest określony. Tak, ta adnotacja jest używana nawet sama w sobie… magia i tyle.

@Cel

Przechowywanie: CZAS PRACY; Cel: ANNOTATION_TYPE; Adnotacja ta określa rodzaj obiektu, nad którym można wskazać utworzoną przez nas adnotację. Tak, i u siebie też się z tego korzysta, przyzwyczaj się... Myślę, że na tym możemy zakończyć nasze wprowadzenie do standardowych adnotacji biblioteki Java, bo reszta jest używana dość rzadko i choć mają swoje zalety, nie każdy musi sobie z nimi radzić i jest całkowicie niepotrzebna. Jeśli chcesz, żebym opowiedział o konkretnej adnotacji ze standardowej biblioteki (lub być może o adnotacjach typu @NotNull i @Nullable, które nie są zawarte w STL), napisz w komentarzach - albo użytkownicy Ci tam odpowiedzą, albo Zrobię to, kiedy to zobaczę. Jeśli dużo osób poprosi o jakąś adnotację, również ją dodam do artykułu.

Praktyczne zastosowanie adnotacji RUNTIME

Właściwie myślę, że wystarczy gadania teoretycznego: przejdźmy do ćwiczeń na przykładzie bota. Załóżmy, że chcesz napisać bota dla jakiejś sieci społecznościowej. Wszystkie główne sieci, takie jak VK, Facebook, Discord, mają własne interfejsy API, które umożliwiają napisanie bota. Dla tych samych sieci istnieją już napisane biblioteki do pracy z API, w tym w Javie. Dlatego nie będziemy zagłębiać się w działanie żadnego API czy biblioteki. Jedyne, co musimy wiedzieć w tym przykładzie, to to, że nasz bot może odpowiadać na wiadomości wysyłane na czat, na którym faktycznie znajduje się nasz bot. To znaczy, powiedzmy, że mamy klasę MessageListener z funkcją:
public class MessageListener
{
    public void onMessageReceived(MessageReceivedEvent event)
    {
    }
}
Odpowiada za przetworzenie otrzymanej wiadomości. Z klasy MessageReceivedEvent potrzebujemy jedynie ciągu znaków odebranej wiadomości (np. „Hello” lub „Bot, hello”). Warto wziąć pod uwagę: w różnych bibliotekach te klasy nazywają się inaczej. Użyłem biblioteki dla Discorda. I tak chcemy, aby bot reagował na niektóre polecenia zaczynając od „Bot” (z przecinkiem lub bez - zdecyduj sam: na potrzeby tej lekcji założymy, że nie powinno tam być przecinka). Oznacza to, że nasza funkcja zacznie się od czegoś takiego:
public void onMessageReceived(MessageReceivedEvent event)
{
    //Убираем чувствительность к регистру (БоТ, бОт и т.д.)
    String message = event.getMessage().toLowerCase();
    if (message.startsWith("бот"))
    {

    }
}
A teraz mamy wiele opcji wdrożenia tego lub innego polecenia. Niewątpliwie najpierw trzeba oddzielić polecenie od jego argumentów, czyli podzielić je na tablicę.
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);
            //ПолучLub command = "(команда)"; nArgs = {"аргумент1", "аргумент2",..."аргументN"};
            //Данный массив может быть пустым
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            //Вывод списка команд Lub Jakого-либо wiadomości
            //В случае если просто написать "Бот"
        }
    }
}
Nie ma możliwości uniknięcia tego fragmentu kodu, ponieważ zawsze konieczne jest oddzielenie polecenia od argumentów. Ale wtedy mamy wybór:
  • Wykonaj if(command.equalsIngnoreCase("..."))
  • Wykonaj przełącznik (polecenie)
  • Wykonaj inny sposób przetwarzania...
  • Lub skorzystaj z pomocy Adnotacji.
I teraz w końcu dotarliśmy do praktycznej części korzystania z adnotacji. Przyjrzyjmy się kodowi adnotacji dla naszego zadania (oczywiście może się różnić).
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

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

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

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

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

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

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

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

}
Ważny! Każdy parametr jest opisany jako funkcja (w nawiasach). Jako parametry można używać tylko elementów pierwotnych, String i Enum . Nie możesz pisać List<String> args(); - błąd. Teraz, gdy opisaliśmy Adnotację, utwórzmy klasę i nazwijmy ją 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();

    }
}
Warto zwrócić uwagę na jedną małą niedogodność: t.c. Walczymy teraz o uniwersalność, wszystkie funkcje muszą mieć tę samą listę parametrów formalnych, więc nawet jeśli polecenie nie ma argumentów, funkcja musi mieć parametr String[] args . Opisaliśmy teraz 3 polecenia: hello, bye, help. Teraz zmodyfikujmy nasz MessageListener , aby coś z tym zrobić. Dla wygody i szybkości pracy nasze polecenia będziemy od razu przechowywać w HashMapie :
public class MessageListner
{
    //Map который хранит Jak ключ команду
    //А Jak oznaczający функцию которая будет обрабатывать команду
    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))
            {
                //Берем obiekt нашей Аннотации
                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)
            {
                //Вывод списка команд Lub Jakого-либо wiadomości
                //В случае если просто написать "Бот"
            }
        }
    }
}
To wszystko, czego potrzeba, aby nasze zespoły mogły pracować. Teraz dodanie nowego polecenia nie jest nowym ifem, nie jest nowym przypadkiem, w którym trzeba by przeliczyć liczbę argumentów, a także trzeba by przepisać pomoc, dodając do niej nowe linie. Teraz, żeby dodać polecenie wystarczy, że w klasie CommandListener dodamy nową funkcję z adnotacją @Command i gotowe – polecenie zostanie dodane, przypadki zostaną uwzględnione, pomoc dodana zostanie automatycznie. Nie ulega wątpliwości, że problem ten można rozwiązać na wiele innych sposobów. Tak, wszystko co da się zrobić za pomocą adnotacji/refleksji da się zrobić bez nich, kwestia tylko wygody, optymalności i wielkości kodu oczywiście wklejanie Adnotacji wszędzie tam, gdzie jest choćby najmniejsza wskazówka, że ​​będzie można ją zastosować nie jest to też najbardziej racjonalna opcja, we wszystkim trzeba wiedzieć, kiedy się zatrzymać =). Jednak przy pisaniu API, bibliotek czy programów, w których możliwe jest powtórzenie kodu tego samego typu (ale nie dokładnie tego samego) adnotacje są niewątpliwie optymalnym rozwiązaniem.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION