JavaRush /Java Blog /Random-TW /Java @Annotations。它是什麼以及如何使用它?
SemperAnte
等級 4
Донецк

Java @Annotations。它是什麼以及如何使用它?

在 Random-TW 群組發布
本文面向從未使用過註釋但想了解它是什麼以及它的用途的人。如果您有這方面的經驗,我認為本文不會以某種方式擴展您的知識(事實上,我並沒有追求這樣的目標)。 另外,這篇文章不適合剛開始學習Java語言的人。如果您不明白Map<>HashMap<>是什麼,或者不知道類別定義中的static{ }條目的含義,或者從未使用過反射,那麼現在閱讀本文還為時過早嘗試理解什麼是註釋。這個工具本身並不是為初學者使用而創建的,因為它需要對類別和物件的交互有不完全基本的了解(我的意見)(感謝表明本後記必要性的評論)。 Java @Annotations。 它是什麼以及如何使用它? - 1那麼就讓我們開始吧。Java 中的註解是程式碼中的一種標籤,用於描述函數/類別/套件的元資料。例如大家熟知的@Override註解,它表示我們要重寫父類別的某個方法。是的,一方面,沒有它也是可以的,但是如果家長沒有這個方法,就有可能我們白寫了程式碼,因為 這個特定的方法可能永遠不會被調用,但是使用 @Override 註解,編譯器會告訴我們:“我在父級中沒有找到這樣的方法......這裡有些髒東西。” 然而,註釋可以承載的不僅僅是「為了可靠性」的含義:它們可以儲存一些稍後將使用的資料。

首先,我們來看看標準函式庫提供的最簡單的註解。

(再次感謝評論,一開始我覺得不需要這個區塊)首先我們來討論一下什麼是註解。它們每個都有 2 個主要必需參數:
  • 儲存類型(Retention);
  • 所指示的物件的類型(目標)。

儲存類型

「儲存類型」是指我們的註解在類別中「存活」的階段。每個註解只有RetentionPolicy類別中指定的一種可能的「保留型別」:
  • SOURCE - 此註解僅在編寫程式碼時使用,並被編譯器忽略(即編譯後不儲存)。通常用於任何預處理器(有條件地)或編譯器指令
  • CLASS - 註釋在編譯後保留,但被 JVM 忽略(即在執行時不能使用)。通常用於將程式碼作為插件應用程式載入的任何第三方服務
  • RUNTIME是編譯後儲存並由 JVM 載入的註解(即可以在程式本身執行期間使用)。用作程式碼中直接影響程式執行的標記(本文將討論一個例子)

上面指示的物件類型

這個描述幾乎應該從字面上理解,因為...... 在Java中,註釋可以在任何東西(欄位、類別、函數等)上指定,並且對於每個註釋,它都指示它可以在什麼上指定。這裡不再有「一件事」規則;可以在下面列出的所有內容之上指定註釋,或者您可以僅選擇 ElementType 類別的必要元素
  • ANNOTATION_TYPE - 另一個註釋
  • CONSTRUCTOR - 類別建構函數
  • FIELD - 類別字段
  • LOCAL_VARIABLE - 局部變數
  • METHOD - 類別方法
  • PACKAGE-包包的描述
  • PARAMETER - 方法參數public void hello(@Annontation String param){}
  • 類型- 在類別上方標明
總的來說,從 Java SE 1.8 開始,標準語言庫為我們提供了 10 個註解。在這篇文章中,我們將看看其中最常見的(誰對所有這些都感興趣?歡迎來到 Javadoc):

@覆蓋

保留:來源;目標:方法。該註釋表明它所編寫的方法是從父類別繼承的。每個Java新手在使用持續推送這些@Override的IDE時遇到的第一個註解。通常,YouTube 的老師會建議:“將其刪除,這樣就不會造成乾擾”,或者:“將其保留,不要懷疑它為何會在那裡。” 事實上,註解的用處還不止於此:它不僅可以讓你了解哪些方法是第一次在這個類別中定義的,哪些是父類別已經擁有的(這無疑增加了你程式碼的可讀性),而且這個註解還可以充當“自我檢查”,確保您在定義重載函數時沒有弄錯。

@已棄用

保留:運行時;目標:建構子、欄位、本地變數、方法、套件、參數、型別。該註釋標識了「過時」的方法、類別或變量,並且可能會在產品的未來版本中刪除。那些閱讀任何 API 文件或相同標準 Java 庫的人通常會遇到此註釋。有時此註釋會被忽略,因為... 它不會造成任何錯誤,原則上,它本身不會對生活造成太大干擾。然而,這個註釋攜帶的主要信息是“我們已經想出了一種更方便的方法來實現這個功能,使用它,不要使用舊的” - 好吧,否則 - “我們重命名了該函數,但是這個是這樣,我們把它留給遺產......」(這通常也不錯)。簡而言之,如果您看到 @Deprecated,最好不要使用它所懸掛的內容,除非絕對必要,並且可能值得重新閱讀文件以了解已棄用元素執行的任務現在是如何實現的。例如,建議使用Calendar.getInstance().get(Calendar.YEAR ),而不是使用 new Date() .getYear() 。

@SuppressWarnings

保留:來源;目標:TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE 此註解停用與指定元素相關的編譯器警告的輸出。就是上面SOURCE註解所指示的字段、方法、類別。

@保留

保留:運行時;目標:ANNOTATION_TYPE;該註釋指定了上面指定的註釋的「儲存類型」。是的,這個註釋甚至可以用於它自己......魔法,僅此而已。

@目標

保留:運行時;目標:ANNOTATION_TYPE;該註釋指定了我們建立的註釋可以指示的物件類型。是的,而且也是給自己用的,習慣了…我想這裡我們就可以完成我們對Java庫的標準註解的介紹了,因為 其餘的很少使用,儘管它們有自己的好處,但並不是每個人都必須處理它們並且完全沒有必要。如果你想讓我談論標準庫中的特定註釋(或者,也許像@NotNull 和@Nullable 這樣的註釋,它們不包含在STL 中),請在註釋中寫下- 要么善良的用戶會在那裡回答你,要么當我看到它時我會的。如果很多人要求某種註釋,我也會將其添加到文章中。

RUNTIME註解的實際應用

實際上,我認為理論討論已經足夠了:讓我們繼續使用機器人的範例進行練習。假設您想為某個社交網路編寫一個機器人。所有主要網絡,例如 VK、Facebook、Discord,都有自己的 API,可讓您編寫機器人。對於這些相同的網絡,已經有用於使用 API 的編寫庫,包括 Java 中的庫。因此,我們不會深入研究任何API或庫的工作。在這個例子中我們需要知道的是我們的機器人可以回應發送到我們的機器人實際所在的聊天的訊息。也就是說,假設我們有一個帶有函數的 MessageListener類別:
public class MessageListener
{
    public void onMessageReceived(MessageReceivedEvent event)
    {
    }
}
它負責處理接收到的訊息。我們從MessageReceivedEvent類別中需要的只是接收到的訊息的字串(例如,「Hello」或「Bot,hello」)。值得考慮的是:在不同的函式庫中,這些類別的呼叫方式不同。我使用了 Discord 函式庫。因此,我們想讓機器人對一些以「Bot」開頭的命令做出反應(有或沒有逗號 - 自己決定:為了本課的目的,我們假設那裡不應該有逗號)。也就是說,我們的函數將以以下形式開始:
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);
            //Получor command = "(команда)"; nArgs = {"аргумент1", "аргумент2",..."аргументN"};
            //Данный массив может быть пустым
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            //Вывод списка команд or Howого-либо messages
            //В случае если просто написать "Бот"
        }
    }
}
我們無法避免這段程式碼,因為將命令與參數分開總是必要的。但接下來我們有一個選擇:
  • 執行 if(command.equalsIngnoreCase("..."))
  • 執行切換(命令)
  • 做一些其他的處理方式...
  • 或求助於註釋的幫助。
現在我們終於到達了使用註釋的實際部分。讓我們來看看我們任務的註釋程式碼(當然,它可能會有所不同)。
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();

}
重要的!每個參數都被描述為一個函數(帶括號)。只有原語、 StringEnum可以當參數。你不能寫List<String> args(); - 錯誤。現在我們已經描述了 Annotation,讓我們建立一個類,將其命名為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();

    }
}
值得注意的是一個小不便:t.c. 我們現在爭取通用性,所有函數都必須具有相同的形式參數列表,因此即使命令沒有參數,函數也必須有一個String[] args參數 現在我們已經描述了 3 個指令:hello、bye、help。現在讓我們修改MessageListener來執行一些操作。為了工作的方便和速度,我們將立即將命令儲存在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
                //В случае если просто написать "Бот"
            }
        }
    }
}
這就是我們團隊工作所需的一切。現在新增一個指令並不是一個新的 if,也不是一個新的情況,在這種情況下,需要重新計算參數的數量,並且還必須重寫幫助,向其添加新行。現在,要新增命令,我們只需要在 CommandListener 類別中使用 @Command 註解新增一個函數即可 - 新增命令,考慮情況,自動新增協助。毫無疑問,這個問題可以透過許多其他方式來解決。是的,在註釋/反射的幫助下可以完成的所有事情都可以在沒有它們的情況下完成,唯一的問題是便利性、最優性和代碼大小,當然,在任何有可能使用的輕微提示的地方都貼上註解它也不是最理性的選擇,在所有你需要知道何時停止的事情中 =)。但是,當編寫可能重複相同類型(但不完全相同)程式碼的 API、程式庫或程式時,註解無疑是最佳解決方案。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION