Код
Java
#База знаний

Как написать аннотацию на 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-разработчик». Вы научитесь программировать на самом востребованном языке и сможете устроиться на высокооплачиваемую работу.


Жизнь можно сделать лучше!
Освойте востребованную профессию, зарабатывайте больше и получайте от работы удовольствие.
Каталог возможностей
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована