Как написать аннотацию на Java за 5 шагов
Разбираемся с аннотациями в Java: пошаговое руководство.


AD Design Award 2021
Аннотации нужны для описания метаданных — дополнительной информации о программных элементах. Они могут использоваться, например, для конфигурирования программного окружения или для выдачи указаний компилятору: чтобы он учитывал эти аннотации при проверке ошибок или подавлении предупреждений.
Аннотации — удобный способ собрать данные о сущности и правила их обработки в одном месте.
В Java есть встроенные аннотации. Например:
- @Override у метода — говорит о том, что родительский метод переопределён в наследнике. Компилятор при наличии такой аннотации проверяет, не нарушены ли правила переопределения.
- @Deprecated помечаются элементы языка, которые больше не рекомендуется использовать, так как они заменены другими.
Но нам мало встроенных аннотаций. Поэтому научимся создавать свои: начнём с простой маркерной и за пять шагов дойдём до вершины мастерства — повторяющейся аннотации.
Текущий релиз Java SE с долгосрочной поддержкой (LTS) — Java 11. Поэтому описание синтаксиса и примеры ниже даны для этой версии языка.
Шаг нулевой, подготовительный
Формулируем задачу и описываем контекст
Потренируемся в написании аннотаций на примере Telegram-каналов: недавно мне попался очередной опрос от создателей мессенджера, в котором был и вопрос о числе подписок на каналы. Внезапно оказалось, что у меня их больше 30, самых разных, — вот пусть и поработают подопытными кроликами.
Создадим аннотации, показывающие, насколько интересен контент канала и какого рода информация там публикуется, ну а дальше как пойдёт 😀
Шаг первый
Создаём каркас аннотации
Аннотации похожи на интерфейсы. Если вы пока не очень ориентируетесь в интерфейсах, освежите свои знания о них в этой статье, перед тем как мы продолжим.
Они не просто похожи на интерфейсы: под капотом JVM (виртуальная машина Java) преобразует элементы аннотации в методы интерфейса, а саму аннотацию — в имплементацию (реализацию) этого интерфейса.
Вот как выглядит объявление простейшей аннотации для отметки особо интересных каналов:
public @interface Cool {
}
Здесь, кроме заголовка, нет ничего полезного. Такие аннотации ещё называют маркерными — они действительно просто маркируют, обозначают какой-то признак.
Как использовать:
@Cool()
class VeryInterestingChannel{}
Или так:
@Cool
class VeryInterestingChannel{}
При использовании маркерных аннотаций можно опускать круглые скобки после названия.
В аннотациях могут быть:
- обязательные элементы,
- необязательные элементы,
- константы.
Шаг второй
Добавляем обязательные элементы
Напишем аннотацию, которой будем обозначать каналы о хобби. У неё будет один обязательный элемент — собственно описание хобби: фотография, музыка, путешествия и тому подобное.
public @interface Hobby {
String kind();
}
После названия элемента обязательны круглые скобки — как для методов в интерфейсе. Набор типов для элементов ограничен, можно использовать только:
- примитивные типы (byte, short, int, long, float, double, boolean, char);
- String;
- Class;
- перечисление (enum);
- другую аннотацию;
- массив любого из вышеперечисленных типов.
Вот так можно:
public @interface Hobby {
int level(); //уровень сложности материала
String[] tags(); //теги
MyDescriptionEnum description(); //какой-то дополнительный
//описательный признак
}
enum MyDescriptionEnum {
DESC_TYPE1, DESC_TYPE2
}
А так нельзя:
public @interface Hobby {
Integer level(); //не скомпилируется
List<String> tags(); //не скомпилируется
MyDescriptionClass description(); //не скомпилируется
}
class MyDescriptionClass {
public String someProperty;
}
Для элементов аннотаций можно использовать модификаторы, но не любые, потому что, как и методы интерфейсов, все элементы аннотаций неявно public и abstract.
Это значит, что можно определить элемент таким образом:
public @interface Hobby {
public abstract String kind();
}
Но нельзя, например, так:
public @interface Hobby {
private final String kind(); //не скомпилируется
}
final здесь недопустим, потому что конфликтует с неявным abstract. Это выглядит логичным, если вспомнить, что final запрещает переопределять метод, а abstract, напротив, требует реализации в наследниках. Исключение — только если наследник тоже абстрактный.
Как использовать:
@Hobby(kind="photo", level = 3, tags={"travel", "nature"}, description = MyDescriptionEnum.DESC_TYPE1)
class NationalGeographicChannel{}
@Hobby(kind = "music", level=1, tags="rock", description = MyDescriptionEnum.DESC_TYPE2) class RockChannel{}
Каждому обязательному элементу необходимо задать значение — на то они и обязательные. Для определения элемента-массива указываются фигурные {} скобки. Если в массиве только один элемент (как во втором примере с музыкальным каналом), скобки можно опустить.
Где-то в коде вам может попасться аннотация, у которой в скобках просто написано значение элемента без названия, вот так:
@Fun(100500)
class PikabuChannel {}
Можете быть уверены, что у этой аннотации есть элемент с особым названием:
- он называется value;
- он обязательный;
- других обязательных элементов нет.
public @interface Fun {
int value();
String other() default "";
}
Необязательные элементы допустимы, но если захотите изменить их значения по умолчанию, придётся явно указать и название элемента value:
@Fun(value=100500, other="other")
class PikabuChannel {}
Шаг третий
Добавляем необязательные элементы
Сделаем необязательными все элементы, кроме kind. Для этого всем остальным элементам нужно задать значения по умолчанию.
Вот как это делается:
public @interface Hobby {
String kind();
int level() default 1;
String[] tags() default {};
MyDescriptionEnum description() default MyDescriptionEnum.DESC_TYPE1;
}
В качестве дефолтного значения может использоваться только константное НЕ null-значение. То есть вот такие объявления компилятор не пропустит:
public @interface BadAnnotation {
String stringProp() default new String(""); //не скомпилируется
String anotherStringProp() = null; //не скомпилируется
}
Зато пустую строку в качестве значения по умолчанию использовать можно, вот так:
public @interface GoodAnnotation {
String okStringProp() default "";
}
Как использовать:
@Hobby(kind="photo", level = 3, tags={"travel", "nature"})
class NationalGeographicChannel{}
@Hobby(kind = "music", description = MyDescriptionEnum.DESC_TYPE2)
class RockChannel{}
Текста по сравнению с предыдущим пунктом стало меньше, потому что значения необязательных элементов можно не указывать. В этом случае будут использоваться значения по умолчанию.
Однако никто не запрещает указать своё значение вместо дефолтного. В примере выше каналу NationalGeographicChannel мы установили не равный единице элемент level, а ещё задали для обоих классов непустые множества тегов — хоть это и необязательный элемент.
Кроме элементов со значениями по умолчанию, в аннотациях можно использовать константы. Они, как и в интерфейсах, неявно public, static и final. Вот, к примеру, аннотация для описания времени на чтение каналов, в которой заданы константы для минимального и максимального тайминга:
public @interface ReadingTime {
int MIN_POST_READING = 1;
int MAX_POST_READING = 20;
int avgTime() default MIN_POST_READING;
int readLimit() default MAX_POST_READING;
}
Шаг четвёртый
Определяем область действия
К чему применима аннотация
До этого шага мы использовали аннотации только для классов, но они применимы к интерфейсам, методам, параметрам класса, локальным переменным и не только. За область применимости аннотации отвечает другая аннотация — @Target. Полный список доступных значений для её единственного элемента value есть в официальной документации.
Если не задавать @Target, аннотацию можно использовать для любых программных элементов.
Мы же для примера напишем аннотацию, которая будет применима к методам и конструкторам классов. Предположим, она будет показывать, что после выполнения таких конструкторов и таких методов нужно будет отправить кому-то тревожное сообщение. Реализацию обработчика оставим за кадром, а аннотация будет выглядеть так:
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface Alarm {
}
Как использовать:
class SecureChannel{
@Alarm
public SecureChannel() {
//do something
}
@Alarm
void secureMethod(){
//do something
}
}
Если теперь попробуем написать @Alarm перед названием самого класса SecureChannel, получим ошибку компиляции, потому что в @Target не включено значение для типа элемента «класс».
Когда аннотация доступна
Если мы и правда хотим прямо во время выполнения программы искать какие-то помеченные аннотацией @Alarm методы, одним только указанием @Target не обойтись.
Есть ещё одна аннотация для описания аннотаций — это @Retention. Она определяет доступность в течение жизненного цикла программы. У её единственного элемента value всего три доступных значения:
Значение | Описание |
---|---|
RetentionPolicy.SOURCE | Аннотация останется только в файлах исходников, в .class-файлы сведения о ней не попадут |
RetentionPolicy.CLASS — значение по умолчанию | Аннотация будет сохранена в .class-файлах, но не будет доступна во время выполнения программы |
RetentionPolicy.RUNTIME | Аннотация будет сохранена в .class-файлах, доступна во время выполнения программы |
Воспользуемся новыми знаниями и допишем нашу тревожную аннотацию:
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface Alarm {
}
Теперь она будет доступна в рантайме, что и требовалось.
Шаг пятый
Делаем аннотацию повторяющейся
Вернёмся к аннотации для описания хобби и зададимся вопросом: что, если в канале попадается информация о нескольких увлечениях? Например, в нём публикуются обзоры новых фильмов и афиши концертов — вроде и про кино, и про музыку.
Попробуем пометить класс такого универсального канала сразу двумя @Hobby с разными значениями элементов:
@Hobby(kind="cinema")
@Hobby(kind="music") //не скомпилируется
class UniversalChannel{}
Иии… получим ошибку компиляции! Вот такую:

Что ж, сделаем её repeatable, то есть повторяющейся. Для этого опять понадобится аннотация — @Repeatable. Пометим ею @Hobby:
@Repeatable //не скомпилируется
public @interface Hobby {
String kind();
int level() default 1;
String[] tags() default {};
MyDescriptionEnum description() default MyDescriptionEnum.DESC_TYPE1;
}
Но снова натыкаемся на возражения компилятора: оказывается, у @Repeatable должен быть указан обязательный элемент, а тип этого элемента — ещё одна аннотация 😵
Глубоко выдохнем и создадим её. В этой очередной аннотации нужно указать, какую другую аннотацию — в нашем случае @Hobby — мы собираемся повторять:
public @interface Hobbies{
Hobby[] value();
}
И последний шажок — укажем класс этой новой аннотации в качестве значения для @Repeatable:
@Repeatable(Hobbies.class)
public @interface Hobby {
String kind();
int level() default 1;
String[] tags() default {};
MyDescriptionEnum description() default MyDescriptionEnum.DESC_TYP
Теперь, наконец, всё скомпилируется, и @Hobby можно будет повторять для одного класса сколько угодно раз.
Подытожим
Чтобы вам было ещё проще создавать свои аннотации на Java, мы подготовили две схемы, в которых собрали правила синтаксиса для их объявления и использования.


Узнайте больше об аннотациях и других элементах языка Java на нашем курсе «Профессия Java-разработчик». Вы научитесь программировать на самом востребованном языке и сможете устроиться на высокооплачиваемую работу.