Аннотация в Java это метка в коде, которая ничего не делает сама, а хранит метаданные о классе, методе, поле или параметре. Пишется она через собаку (@Override, @Deprecated, @Autowired), объявляется словом @interface, а всю работу выполняет тот, кто эту метку прочитает: компилятор, IDE, фреймворк или ваш собственный код через рефлексию. Два параметра решают судьбу любой аннотации: Retention (до какого этапа она доживет) и Target (над чем ее разрешено ставить).

Кратко

  • Аннотация это метаданные, а не исполняемый код. Работу делает тот, кто ее читает: компилятор, IDE, фреймворк или ваша программа.
  • Retention задает срок жизни: SOURCE (исчезает при компиляции), CLASS (лежит в class-файле, но JVM ее не грузит), RUNTIME (доступна во время работы программы). Без @Retention по умолчанию CLASS.
  • Target задает, над чем аннотацию можно ставить: метод, поле, класс, параметр и другие варианты из ElementType. Без @Target ее можно ставить над любым объявлением.
  • Через рефлексию видны только RUNTIME-аннотации, читают их методами isAnnotationPresent() и getAnnotation().
  • Своя аннотация объявляется как public @interface Имя, параметры описываются как методы со скобками, а типы разрешены не любые: примитив, String, Class, enum, другая аннотация и массив из перечисленного.
  • Практический смысл там, где иначе разрастается цепочка if или switch: новый случай добавляется одной пометкой над методом, а не правкой в трех местах.
Данная статья предназначена для людей, которые никогда не работали с Аннотациями, но хотели бы разобраться, что это и с чем его едят. Если же вы имеете опыт в данной сфере, не думаю, что эта статья как-то расширит ваши знания (да и, собственно, такую цель я не преследую). Также статья не подходит для тех, кто только начинает изучать язык Java. Если Вы не понимаете что такое Map<> или HashMap<> или не знаете что означает запись static{ } внутри определения класса, либо же никогда не работали с рефлексией, вам рано читать эту статью и пытаться понять, что такое аннотации. Сам по себе этот инструмент не создан для использования новичками, так как требует уже не совсем базовых пониманий взаимодействия классов и объектов (мое мнение) (спасибо комментариям за то, что показали необходимость этой приписки). Слово @ANNOTATION зеленым пиксельным шрифтом на фоне из нулей и единицИтак, приступим. Аннотации в Java являются своего рода метками в коде, описывающими метаданные для функции/класса/пакета. Например, всем известная Аннотация @Override, обозначающая, что мы собираемся переопределить метод родительского класса. Да, с одной стороны, можно и без нее, но если у родителей не окажется этого метода, существует вероятность, что мы зря писали код, т.к. конкретно этот метод может и не вызваться никогда, а с Аннотацией @Override компилятор нам скажет, что: "Я не нашел такого метода в родителях... что-то здесь нечисто". Однако Аннотации могут нести в себе не только смысл "для надежности": в них можно хранить какие-то данные, которые после будут использоваться.

Для начала рассмотрим простейшие аннотации, предоставляемые стандартной библиотекой.

(снова же спасибо комментариям, вначале не подумал что этот блок нужен) Сначала обсудим, какие бывают аннотации. У каждой есть 2 главных параметра:
  • Тип хранения (Retention);
  • Тип объекта над которым она указывается (Target).
Оба задаются мета-аннотациями @Retention и @Target, и оба необязательны, просто умолчания редко совпадают с тем, что нужно. Если @Retention не написать, аннотация получит политику CLASS и до выполнения программы не доживет, а без @Target ее можно будет ставить над любым объявлением. Так это описано в документации Oracle.

Тип хранения

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

Тип объекта над которым указывается annotation

Данное описание стоит понимать практически буквально, т.к. в Java аннотации могут указываться над чем угодно (Поля, классы, функции, т.д.) и для каждой аннотации указывается, над чем конкретно она может быть задана. Здесь уже нет правила "что-то одно", аннотацию можно указывать над всем ниже перечисленным, либо же выбрать только нужные элементы перечисления ElementType, в котором сейчас двенадцать констант:
  • ANNOTATION_TYPE - другая аннотация
  • CONSTRUCTOR - конструктор класса
  • FIELD - поле класса
  • LOCAL_VARIABLE - локальная переменная
  • METHOD - метод класса
  • PACKAGE - описание пакета package
  • PARAMETER - параметр метода public void hello(@Annotation String param){}
  • TYPE - указывается над классом
  • TYPE_PARAMETER - параметр типа, например T в объявлении дженерика (с Java 8)
  • TYPE_USE - любое место, где тип употребляется, например @NotNull String name (с Java 8)
  • MODULE - объявление модуля (с Java 9)
  • RECORD_COMPONENT - компонент рекорда (с Java 16)
На момент актуальной версии Java 25 (LTS) стандартная библиотека предоставляет множество встроенных аннотаций. В данной статье мы разберем наиболее часто используемые из них. Полный список можно найти в официальной документации Oracle

@Override

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

@Deprecated

Retention: Runtime; Target: CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE. Данная аннотация указывает на методы, классы или переменные, которые являются "устаревшими" и могут быть убраны в последующих версиях продукта. С данной аннотацией обычно сталкиваются те, кто читает документацию каких-либо API, либо той же стандартной библиотеки Java. Иногда эту аннотацию игнорируют, т.к. она не вызывает никаких ошибок и в принципе сама по себе сильно жить не мешает. Однако главный посыл, который несет в себе данная аннотация: "мы придумали более удобный метод реализации данного функционала, используй его, не используй старый" - ну, либо же - "мы переименовали функцию, а это так, для легаси оставили..." (что тоже в общем-то неплохо). Короче говоря, если видите @Deprecated - лучше стараться не использовать то, над чем она висит, если в этом нет прям крайней необходимости и, возможно, стоит перечитать документацию, чтобы понять каким образом теперь реализуется задача, выполняемая устаревшим элементом. Например, метод new Date().getYear() помечен устаревшим еще в Java 1.1 и возвращает не год, а год минус 1900. Класс Calendar устаревшим не помечен, но и он проигрывает по читаемости современному API java.time, где тот же год берется так: LocalDate.now().getYear() Еще одна деталь: с Java 9 у самой аннотации появились параметры since и forRemoval. Первый показывает, в какой версии элемент объявили устаревшим, второй предупреждает, что его собираются удалить, а не просто не советуют трогать.

@SuppressWarnings

Retention: SOURCE; Target: любое объявление. Данная аннотация отключает вывод предупреждений компилятора, которые касаются элемента, над которым она указана, и всего, что внутри этого элемента. В Java 8 у нее был явный список целей (TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE), а начиная с Java 9 @Target у нее убрали совсем, так что ставить ее можно над любым объявлением.

@Retention

Retention: RUNTIME; Target: ANNOTATION_TYPE; Данная аннотация задает "тип хранения" аннотации над которой она указана. Да эта аннотация используется даже для самой себя... магия да и только.

@Target

Retention: RUNTIME; Target: ANNOTATION_TYPE; Данная аннотация задает тип объекта над которым может указываться создаваемая нами аннотация. Да и она тоже используется для себя же, привыкайте...

Другие аннотации из JDK

На этом список стандартных аннотаций не заканчивается. В актуальных версиях Java активно применяются следующие: @FunctionalInterface: гарантирует, что в интерфейсе ровно один абстрактный метод, то есть его можно реализовать лямбдой. @SafeVarargs: убирает предупреждения компилятора при использовании varargs с дженериками. @Documented: делает так, что аннотация отображается в Javadoc. @Repeatable: позволяет одной аннотации повторяться несколько раз над одним элементом. @Native: помечает константу, на которую может ссылаться нативный код. По этой метке инструменты, которые генерируют заголовочные файлы для JNI, понимают, что константу надо вынести в заголовок. Версии у них разные: @Documented есть еще с Java 5, @SafeVarargs появилась в Java 7, а @FunctionalInterface, @Repeatable и @Native в Java 8. Все они используются по сей день, поэтому полезно о них знать.

Шпаргалка: аннотации из JDK

Все, что разобрано выше, плюс мета-аннотация @Inherited, о которой речь еще не заходила, в одной таблице. Значения Retention и Target взяты из документации Java 25.
Аннотация Retention Target Зачем нужна
@Override SOURCE METHOD Компилятор проверяет, что метод действительно переопределяет метод супертипа
@Deprecated RUNTIME почти любое объявление, включая модуль Помечает устаревший элемент, с Java 9 есть параметры since и forRemoval
@SuppressWarnings SOURCE любое объявление Гасит предупреждения компилятора у элемента и у всего, что внутри него
@FunctionalInterface RUNTIME TYPE Компилятор проверяет, что в интерфейсе ровно один абстрактный метод
@SafeVarargs RUNTIME CONSTRUCTOR, METHOD Автор ручается, что varargs с дженериками используется безопасно
@Retention RUNTIME ANNOTATION_TYPE Мета-аннотация: задает срок жизни другой аннотации
@Target RUNTIME ANNOTATION_TYPE Мета-аннотация: задает, над чем можно ставить другую аннотацию
@Documented RUNTIME ANNOTATION_TYPE Мета-аннотация: аннотация попадает в Javadoc помеченного элемента
@Inherited RUNTIME ANNOTATION_TYPE Мета-аннотация: аннотацию класса видно и у его наследников
@Repeatable RUNTIME ANNOTATION_TYPE Мета-аннотация: разрешает ставить аннотацию над одним элементом несколько раз
@Native SOURCE FIELD Помечает константу, на которую ссылается нативный код

Аннотации из фреймворков и библиотек

Кроме встроенных аннотаций JDK, в современной Java крайне важны аннотации из популярных библиотек и фреймворков:

Spring Framework

@Service, @Component, @Repository: помечают классы для автоматического сканирования и внедрения зависимостей. @Autowired: внедрение зависимостей. @Configuration, @Bean: конфигурация приложения. @GetMapping, @PostMapping: связывают методы с HTTP-маршрутами.

Lombok

@Getter, @Setter: автоматически генерируют геттеры и сеттеры. @Builder: создает паттерн «строитель». @Data: объединяет несколько аннотаций для генерации кода.

Bean Validation (Jakarta Validation, Hibernate Validator)

@NotNull, @Size, @Email: валидация данных. Эти аннотации широко применяются в реальных проектах и делают код чище, понятнее и короче.

Практическое применение RUNTIME аннотаций

Собственно, думаю, хватит теоретической болтовни: давайте перейдем к практике на примере бота. Собирать будем в три шага:
  1. описать свою аннотацию @Command с параметрами команды;
  2. повесить ее на методы-обработчики в отдельном классе;
  3. один раз собрать все помеченные методы в Map через рефлексию и вызывать нужный по имени команды.
Допустим вы хотите написать бота для какой-то соцсети. У всех крупных сетей, таких как ВК, 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)
            {
                //Вывод списка команд или какого-либо сообщения
                //В случае если просто написать "Бот"
            }
        }
    }
Данного куска кода нам никак не избежать, потому что отделение команды от аргументов нужно всегда. А вот дальше уже у нас есть выбор: Со временем switch стал заметно мощнее, только версии тут разные. Стрелочная форма и switch как выражение доехали до релиза в Java 14 (JEP 361), а сопоставление с образцом по типу стало стандартом только в Java 21 (JEP 441). Стрелочная запись выглядит так:
switch (command) {
    case "start" -> System.out.println("Стартуем!");
    case "stop" -> System.out.println("Останавливаемся!");
    default -> System.out.println("Неизвестная команда");
}
И вот мы наконец дошли до практической части использования Аннотаций.

Шаг 1. Своя аннотация @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, Class, перечисление, другая аннотация и массив из чего-то перечисленного. Именно поэтому в листинге выше спокойно живет String[] aliases();, а вот List<String> args(); написать нельзя, будет ошибка компиляции. Полный список зафиксирован в спецификации языка, раздел 9.6.1.

Шаг 2. Класс с методами-командами

Теперь, когда мы описали Аннотацию, давайте заведем класс, назовем его 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 команды: привет, пока, помощь.

Шаг 3. Карта команд и вызов через рефлексию

Теперь давайте модифицируем наш MessageListener так, чтобы он как-то с этим работал. Для удобства и скорости работы, будем сразу хранить наши команды в HashMap, а заполним ее один раз в статическом блоке, при загрузке класса:

    public class MessageListener
    {
        //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, иначе она просто не будет найдена;  
                    //Приведение к Object обязательно: без него массив
                    //разложится на отдельные аргументы
                    m.invoke(LISTENER, (Object) nArgs);
                }
                catch (ArrayIndexOutOfBoundsException e)
                {
                    //Вывод списка команд или какого-либо сообщения
                    //В случае если просто написать "Бот"
                }
            }
        }
    }
Отдельно стоит сказать про приведение (Object) в вызове m.invoke, потому что тут легко попасться. У метода invoke сигнатура invoke(Object obj, Object... args), то есть аргументы он принимает через varargs. А String[] это одновременно и Object[], поэтому без приведения компилятор передаст наш массив не как один аргумент, а как весь список аргументов: рефлексия решит, что у метода столько параметров, сколько слов было в команде. Метод hello(String[] args) объявляет ровно один параметр, так что вместо вызова прилетит IllegalArgumentException. В документации это прямо названо среди причин исключения: "if the number of actual and formal parameters differ". Приведение к Object заставляет компилятор завернуть массив в Object[] из одного элемента, и метод получает ровно то, что ждет. Вот собственно и все, что нужно, чтобы наши команды работали. Теперь добавление новой команды это не новый if и не новый case, в которых нужно было бы заново переучесть количество аргументов, также пришлось бы переписывать help, добавляя в него новые строки. Теперь же, чтобы добавить команду, нам нужно просто в классе CommandListener добавить новую функцию с аннотацией @Command, и все: команда добавлена, случаи учтены, help дополнен автоматически.

Когда аннотации оправданы, а когда нет

Абсолютно бесспорно, что данную задачу можно решить множеством других путей. Да, все что можно сделать при помощи аннотаций/рефлексий можно сделать и без них, вопрос лишь в удобстве, оптимальности и размерах кода, конечно же, совать Аннотацию везде где есть малейший намек на то, что получится ее использовать - тоже не самый рациональный вариант, во всем нужно знать меру =). Но при написании API, Библиотек или программ, в которых возможно повторение однотипного (но не совсем одинакового) кода, аннотации - бесспорно оптимальное решение.

Вопросы и ответы

Чем аннотация отличается от комментария?

Комментарий видит только человек, компилятор его выбрасывает. Аннотация это часть кода: она попадает в class-файл, и ее читают компилятор, IDE, фреймворки и сама программа через рефлексию. Поэтому по аннотации можно принимать решения во время работы приложения, а по комментарию нельзя.

Почему моя аннотация не видна через рефлексию?

Скорее всего у нее не тот срок жизни. Через рефлексию видны только аннотации с политикой RUNTIME. Если над объявлением аннотации не написать @Retention, она по умолчанию получит политику CLASS: попадет в class-файл, но JVM ее не загрузит, и isAnnotationPresent вернет false. Нужно явно указать @Retention(RetentionPolicy.RUNTIME).

Обязательно ли писать @Retention и @Target у своей аннотации?

Формально нет, обе мета-аннотации необязательны. Но умолчания редко совпадают с тем, что нужно: без @Retention аннотация не доживет до выполнения программы, а без @Target ее можно будет ставить над любым объявлением, в том числе там, где она бессмысленна. Поэтому в рабочем коде их почти всегда пишут явно.

Какие типы можно использовать у параметров аннотации?

Спецификация языка разрешает примитивы, String, Class, перечисления, другие аннотации и массивы из перечисленного. Коллекции и произвольные объекты запрещены: List на параметре аннотации дает ошибку компиляции. Именно поэтому список синонимов команды хранят в массиве строк, а не в списке.

Замедляют ли аннотации программу?

Сами по себе нет. Аннотации с политикой SOURCE исчезают после компиляции, с политикой CLASS просто лежат в файле. Расходы появляются только там, где RUNTIME-аннотации читают через рефлексию, и заплатить за это можно один раз, если сложить результат в Map при загрузке класса, как сделано в примере с ботом.

Зачем нужна аннотация, если можно обойтись switch или цепочкой if?

Обойтись можно всегда, вопрос в цене изменений. С switch каждая новая команда это новая ветка, новая правка в проверке аргументов и новая строка в справке. С аннотацией новая команда это один метод с пометкой, а карта команд, проверка аргументов и справка собираются сами. На двух командах разницы нет, на двадцати она заметна.

Читайте также