JavaRush /Java 博客 /Random-ZH /Java @Annotations。它是什么以及如何使用它?
SemperAnte
第 4 级
Донецк

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

已在 Random-ZH 群组中发布
本文面向从未使用过注释但想了解它是什么以及它的用途的人们。如果您有这方面的经验,我认为本文不会以某种方式扩展您的知识(事实上,我并不追求这样的目标)。 另外,这篇文章不适合那些刚刚开始学习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