Руководство по аннотациям в Java и механизму их работы

Введение
Аннотации появились в Java 5 в 2004 году.
Это специальная форма синтаксических метаданных, которая может быть добавлена в исходный код. Аннотации используются для анализа кода, при компиляции или во время выполнения программы. Их можно применять к пакетам, классам, методам, переменным и параметрам.
Самая простая аннотация, применимая к классу, выглядит так:
@MyAnnotation 
public class Foo {}
1. Маркерный интерфейс
С момента появления языка Java возникла необходимость помечать, для выполнения тех или иных действий, определенным образом класс или иерархию классов. До Java 5 это делалось через интерфейсы без методов.
Этот вид интерфейса не похож ни на один другой. Он не определяет никаких контрактов между собой и реализующими его классами, т. к. всегда пуст. Поэтому он называется маркерным интерфейсом. Такие интерфейсы нужны для маркировки чего-либо для JVM, компилятора или какой-либо библиотеки.
Serializable и Cloneable — два примера маркерных интерфейсов, которые достались нам в наследство. Например, Serializable позволяет пометить класс, сообщая о том, что его экземпляры можно сериализовать. При этом перед сериализацией делается проверка на наличие имплементации этого интерфейса.
Пример маркерных интерфейсов из JDK
С появлением аннотаций необходимость в использовании маркерных интерфейсов хоть и отпала, но они до сих пор используются.
Пример интерфейса и аналогичной ему аннотации:
public class Foo implements MarkerInterface {} (1)

@MyAnnotation
public class Foo {} (2)
(1) Маркерный интерфейс
(2) Аннотация — эквивалент маркерного интерфейса
1.1 Интерфейсы определяют тип
По факту, маркерный интерфейс отмечает объект, реализующий какой-либо тип, что исключает ошибки на этапе компиляции.
Например, создадим интерфейс без методов MyMark и ряд классов: MarkedClass (реализует MyMark); NonMarkedClass; Main, в котором разместим метод test, принимающий на вход объект типа MyMark.
public interface MyMark {}


class MarkedClass implements MyMark {}


class NonMarkedClass {}


class Main {

    public static void main(String[] args) {
    	  MarkedClass marked = new MarkedClass();
    	  NonMarkedClass nonMarked = new NonMarkedClass();
   	 
    	  test(marked);
        //test(nonMarked); 
    }

    static void test(MyMark markedObject) {
        System.out.println("Метод 'Test' успешно завершен!");
    }
}
Код test (marked) успешно выполнится, поскольку класс объекта marked реализует интерфейс MyMark, что требуется для работы метода test (MyMark markedObject).
Если раскомментировать строку test (nonMarked), мы получим ошибку компиляции:
Ошибка вызвана тем, что требуемым типом для метода test() является MyMark, а мы передаем тип NonMarkedClass.
1.2 Интерфейс определяет тип для наследников класса
Если класс реализует интерфейс, то и все его наследники будут реализовывать этот интерфейс. Нельзя «отвязать» интерфейс от наследников.
В этом месте аннотации имеют преимущество, поскольку позволяют реализовать такой механизм «отвязывания». Но в этом есть и минус — проверка наличия маркера (аннотации) теперь проводится во время исполнения, а не во время компиляции, что чревато ошибками.
Рассмотрим следующий код (тут следует пояснить, что детальное объяснение, используемых в коде аннотаций, дается в последующих главах):
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.TYPE)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface MyAnnotation {}
public interface MyMark {}

@MyAnnotation
class Parent implements MyMark {}

class Child extends Parent {}

class Main {

    public static void main(String[] args) {
        Parent parent = new Parent();
        Child child = new Child();

        testInterface(parent);
        testInterface(child);

        testAnnotation(parent);
        testAnnotation(child);
    }

    public static void testInterface(MyMark markedObject) {
        System.out.println("Метод 'TestInterface' успешно завершен!");
    }

    public static void testAnnotation(Object object) {
        if (!object.getClass().isAnnotationPresent(MyAnnotation.class)) {
            throw new RuntimeException("Объект не аннотирован аннотацией 'MyAnnotation'");
        }
        System.out.println("Метод 'testAnnotation' успешно завершен!");
    }
}
Результат выполнения кода:
Вызов метода testAnnotation (child) на этапе исполнения генерирует исключение, сообщая, что объект не аннотирован аннотацией MyAnnotation, которой был аннотирован его родительский класс Parent. Для успешной компиляции классу Child также необходимо использовать MyAnnotation.
1.3 Ключевые моменты:
  • Если требуется знать, могут ли методы принимать объекты каких-то классов, то такие классы удобнее пометить (реализовать) интерфейсами, так как ошибка выявится на этапе компиляции
  • Если необходимо провести анализ метаданных класса, то использование аннотаций даёт больше возможностей, в том числе принимая во внимание возможность аннотаций иметь параметры. Однако в этом случае анализ аннотаций происходит во время исполнения кода
2. Анатомия аннотации
В прошлой главе вы смогли познакомиться с механизмом аннотаций в действии, а также с созданием собственной аннотации. Мы сознательно не описывали назначение тех или иных аннотаций, поскольку пример имел иную цель. В этой и последующих частях давайте рассмотрим детальнее определение и состав аннотации.
Итак, реализация базового определения аннотации имеет следующий вид:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public @interface MyAnnotation {
    String name() default "";
    int value();
}
Начальный символ @ сообщает о наличии аннотации.
Кратко расшифруем каждую строку с аннотациями и что они определяют:
  • @Retention: в каком жизненном цикле кода аннотация (тут и до конца абзаца речь про @MyAnnotation) будет доступна (в исходнике, в class-файле или во время выполнения)
  • @Target: для какого элемента ее можно использовать (поле, класс, пакет и тд)
  • @Inherited: позволяет реализовать наследование аннотаций родительского класса классом-наследником
  • @Documented: аннотация будет помещена в сгенерированную документацию javadoc
  • @interface: сообщает о том, что это аннотация
  • Как значения параметров аннотации, так и значения по умолчанию, являются опциональными (в данном примере присутствует два параметра: name типа String со значением по умолчанию и value типа int).
Возможно, сейчас вам ничего не понятно, это нормально. Более детально эти аннотации будут рассмотрены далее.
Стоит заметить, что в аннотации можно перечислить несколько значений, если значения по умолчанию отсутствуют. При этом переменная, именуемая value, относится к специальным переменным. Значение value может использоваться без имени переменной, если другие значения отсутствуют. Например:
// Оба значения приведены, их именование обязательно
@MyAnnotation(name = "какое-то имя", value = 42)
public class MyType { ... }
// Присутствует только "value()", в качестве "name()" будет его значение по умолчанию
@MyAnnotation(value = 42)
public class MyType2 { ... }
// Если требуется только "value()", мы можем опустить имя
@MyAnnotation(42)
public class MyType3 { ... }
Закрепим полученные знания на примере. Создадим аннотацию JavaFileInfo, которая будет аннотировать классы и методы информацией об их авторах и версиях класса/метода:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JavaFileInfo {
    String name() default "Igor M.";
    String value() default "0.0";
}
Добавим аннотируемый класс DemoClass:
@JavaFileInfo("2.0")
public class DemoClass {
    @JavaFileInfo(name = "Chi Max", value = "1.0")
    public void printString() {}
}
Создадим класс Main, который при помощи рефлексии извлечет параметры нашей аннотации из DemoClass:
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, SecurityException {
        Class<DemoClass> demoClassObj = DemoClass.class;
        readAnnotationOn(demoClassObj);
        Method method = demoClassObj.getMethod("printString");
        readAnnotationOn(method);
    }

    static void readAnnotationOn(AnnotatedElement element) {
        try {
            System.out.println("\nПоиск аннотаций в " + element.getClass().getName());
            Annotation[] annotations = element.getAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation instanceof JavaFileInfo fileInfo) {
                    System.out.println("Автор: " + fileInfo.name());
                    System.out.println("Версия: " + fileInfo.value());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Результат выполнения программы:
3. Классификация аннотаций
Аннотации можно классифицировать по следующим признакам:
  • аннотации для аннотаций:
  1. @Target
  • аннотации типов:
  1. @Retention
  2. @Documented
  3. @Inherited
  4. @Repeatable
  • аннотации для кода:
  1. @Override
  2. @Deprecated
  3. @SuppressWarnings
  4. @SafeVarargs
  5. @FunctionalInterface
  • нативные аннотации
  • аннотации, написанные программистом
Рассмотрим перечисленные классы аннотаций более детально.
4. Аннотации для аннотаций
Аннотации для аннотаций еще называют мета-аннотациями.
Их пять:
  • @Target: указывает контекст, для которого применима аннотация
  • @Retention: указывает, до какого шага во время компиляции аннотация будет доступна
  • @Documented: указывает, что аннотация должна быть задокументирована с помощью javadoc
  • @Inherited: позволяет реализовать наследование аннотаций родительского класса классом-наследником
  • @Repeatable: указывает, что аннотация может быть использована повторно в том же месте
Рассмотрим перечисленные мета-аннотации более детально.
4.1 Аннотация @Target
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target
Обратите внимание, для определения мета-аннотации @Target используется мета-аннотация @Target. :-)
@Target определяет контекст, для которого она применима (актуально для Java 15):
  1. ElementType.ANNOTATION_TYPE: применяется для определения другой аннотации
  2. ElementType.CONSTRUCTOR: применяется для определения конструктора
  3. ElementType.FIELD: применяется для определения поля, включая константы Enum
  4. ElementType.LOCAL_VARIABLE: применяется для определения локальной переменной
  5. ElementType.METHOD: применяется для определения метода
  6. ElementType.MODULE: применяется для определения модуля (с Java 9)
  7. ElementType.PACKAGE: применяется для определения пакета
  8. ElementType.PARAMETER: применяется для определения параметра
  9. ElementType.TYPE: применяется для определения класса, интерфейса (включая аннотируемый тип), Enum или record.
  10. ElementType.TYPE_PARAMETER: применяется для определения типа параметра (с Java 8)
  11. ElementType.TYPE_USE: применяется для определения используемого типа (с Java 8)
  12. ElementType.RECORD_COMPONENT: ассоциируется с records как компонент записи (с Java 14)
ElementType представляет собой Enum, обеспечивая простую классификацию возможных мест для размещения аннотаций в коде. В свою очередь, они делятся на контексты объявления, где аннотации применяются к объявлениям, и на контексты типов, где аннотации применяются к типам, используемые в объявлениях и выражениях.
Константы ANNOTATION_TYPE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE и TYPE_PARAMETER соответствуют контекстам объявления. TYPE_USE соответствует контекстам типа, а также двум контекстам объявления: объявлениям типов (включая объявления типов аннотаций) и объявлениям параметров типа.
Например, аннотация, тип которой аннотирован с помощью @Target (ElementType.FIELD), может быть записан только как модификатор для объявления поля. В тоже время аннотация, тип которой аннотирован с помощью @Target (ElementType.TYPE_USE), может быть записана в типе поля, а также может выступать в качестве модификатора, например, для объявления класса.
Рассмотрим несколько примеров.
Пример 1: В этом примере @Target информирует о том, что определяемый аннотацией MetaAnnotationType тип сам по себе является мета-аннотацией и может быть использован только для аннотаций:
@Target(ElementType.ANNOTATION_TYPE)
public @interface MetaAnnotationType {
    ...
}
Ярким примером такого использования аннотации является определение самой аннотации @Target, показанное ранее.
Пример 2: @Target информирует о том, что объявленный ею тип предназначен исключительно для использования в качестве типа элемента в объявлениях сложных типов аннотаций:
@Target({})
public @interface MemberType {
    ...
}
Пример 3: Когда константа ElementType появляется более одного раза в аннотации @Target, возникает ошибка времени компиляции (compile-time error):
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.FIELD})
public @interface Bogus {
    ...
}
В последующем изложении материала вы будете встречать эту аннотацию постоянно, поэтому постарайтесь понять ее смысл как можно раньше.
4.1.1 Аннотации типов
До Java 8 аннотации можно было размещать только перед объявлением метода, класса, конструктора и т. д. В Java 8 добавилось еще одно место для размещения аннотаций — рядом с типом. Такой вид аннотации часто называют аннотацией типа. Теперь мы можем аннотировать возвращаемый методом тип, дженерики. Аннотации типов важны, поскольку улучшают систему типов Java и позволяют программным инструментам (например, IDE) автоматически выполнять дополнительные проверки типов во время компиляции.
Аннотация типа должна включать ElementType.TYPE_USE или/и ElementType.TYPE_PARAM в качестве «target». Пример:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface TypeAnnotation { ... }
ElementType.TYPE_PARAMETER указывает, что аннотация TypeAnnotation может быть записана в объявлении переменной типа.
В тоже время, ElementType.TYPE_USE указывает, что аннотация может быть использована для любого типа (например, типов, появляющихся в объявлениях, дженериках и при преобразованиях типов).
Аннотацию @TypeAnnotation необходимо разместить перед аннотируемым типом:
void method() throws @TypeAnnotation NullPointerException {...}
Другие возможные варианты применения аннотации типов:
@NotNull String str = getValue(args);
@Encrypted String str;
@Format(theFormatterConstant) String str;
@Localized String str;
List<@ReadOnly T> list;
Store<@NotNull Product> product;
Store<@Prod(Type.Grocery) Product> product;
void showResources(@Authenticated User user);
@SwingElementOrientation int orientation;
@Positive int i;
@CreditCard string cardNumber;
Date date = (@Readonly Date) object;
Date date = (@NotNull Date) object;
В языке Java отсутствуют встроенные аннотации типов, но мы можем создать их самостоятельно, а также написать свой обработчик аннотаций и подключить его к компилятору для проверки аннотированного кода. При этом генерируя на основе созданных нами правил ошибки или предупреждения, если код им не соответствует.
4.2 Аннотация @Retention
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention
Аннотация @Retention (с англ. означает удержание, задержка) определяет, до какого шага во время компиляции аннотация будет доступна. Все шаги (они еще называются политиками) находятся в Enum:
  1. RetentionPolicy.SOURCE: аннотация сохраняется только в исходном файле и удаляется во время компиляции
  2. RetentionPolicy.CLASS: аннотация сохраняется в файле .class во время компиляции, но недоступна во время выполнения через JVM
  3. RetentionPolicy.RUNTIME: аннотация сохраняется в файле .class во время компиляции и доступна через JVM во время выполнения
В случае отсутствия аннотации @Retention по умолчанию будет использована политика RetentionPolicy.CLASS.
Рассмотрим пример.
Опишем аннотацию в RetentionAnnotation.java:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RetentionAnnotation {}
Создадим файл AnnotatedClass. java, аннотированный двумя аннотациями:
@RetentionAnnotation
@Deprecated
public class AnnotatedClass {}
Создадим и запустим файл Main.java:
import java.lang.annotation.Annotation;

public class Main {
    public static void main(String[] args) {
        AnnotatedClass anAnnotatedClass = new AnnotatedClass();
    	  Annotation[] annotations = anAnnotatedClass.getClass().getAnnotations();

        System.out.println("Общее кол-во аннотаций времени исполнения (RunTime): " + annotations.length);
    	  System.out.println("1: " + annotations[0]);
    	  System.out.println("2: " + annotations[1]);
    }
}
Результат выполнения программы:
В этом примере мы создали свою собственную аннотацию RetentionAnnotation, а также использовали аннотацию @Deprecated, которая также имеет политику RetentionPolicy.RUNTIME.
Если мы исправим политику аннотации RetentionAnnotation с RetentionPolicy.RUNTIME на RetentionPolicy.SOURCE (и закомментируем строку в классе Main, выводящую второй элемент массива), то программа отобразит только одну аннотацию deprecated, поскольку аннотация с RetentionPolicy.SOURCE во время компиляции будет удалена.
4.3 Аннотация @Documented
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented
По умолчанию аннотации не включаются в javadoc. Аннотация, помеченная @Documented информирует, что такая аннотация должна быть задокументирована с помощью инструмента javadoc.
Рассмотрим пример использования @Documented.
Создадим аннотацию @TestDocumented, используя @Documented:
import java.lang.annotation.Documented;

@Documented
public @interface TestDocumented {
    String doTestDocument();
}
Создадим аннотацию @TestNotDocumented, и не пометим её какой-либо аннотацией:
public @interface TestNotDocumented {
    String doTestNoDocument();
}
Теперь создадим класс Tester, пометив в нем два метода, созданными ранее аннотациями:
public class Tester {
    @TestDocumented(doTestDocument = "Hello DOC with annotation")
    public void doSomeTestDocumented() {
        System.out.println("Test for annotation with 'Documented'");
    }

    @TestNotDocumented(doTestNoDocument = "Hello DOC without annotation")
    public void doSomeTestNotDocumented() {
        System.out.println("Test for annotation without 'Documented'");
    }
}
Теперь, если вы запустите команду javadoc (или используете IntellijIdea: Tools -> Generate JavaDoc…) и просмотрите сгенерированный файл Tester.html, то увидите следующее (представлена часть видимого экрана):
Как видно на скриншоте, для метода doSomeTestNotDocumented() отсутствует информация о типе аннотации, но эта информация предоставляется для метода doSomeTestDocumented(). Причина этому @Documented, прикрепленная к нашей аннотации @TestDocumented. @TestNotDocumented не использует эту аннотацию.
4.4 Аннотация @Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited
В данном примере @Inherited может использоваться только для аннотирования класса.
По умолчанию аннотации, примененные к родительскому классу, не будут доступны в наследуемом классе. Если мы хотим, чтобы аннотации также наследовались, родительский класс необходимо пометить аннотацией @Inherited: в этом случае все аннотации родительского класса будут применимы к наследникам.
Рассмотрим пример использования @Inherited.
Создадим наследуемую аннотацию @InheritantAnnotation:
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface InheritantAnnotation {}
Создадим не «наследуемую» аннотацию @NonInheritantAnnotation:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface NonInheritantAnnotation {}
Создадим родительский класс Parent, применив к нему две аннотации:
@InheritantAnnotation
@NonInheritantAnnotation
public class Parent {}
Создадим наследника Parent, класс Child:
public class Child extends Parent {}
Что мы получили сейчас: в классе Parent применены две аннотации (одна из них наследуемая), а в классе Child аннотации явно отсутствуют, но неявно присутствует унаследованная от родительского класса аннотация @InheritantAnnotation.
Используем перечисленные выше классы в Main и запустим его:
import java.lang.annotation.Annotation;

public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent();
    	  Child child = new Child();

    	  if(parent.getClass().getAnnotations().length > 0) {
            System.out.println("Для класса 'Parent' применены следующие аннотации: ");
            for(Annotation annotationName: parent.getClass().getAnnotations()) {
                System.out.println(annotationName);
        }
    } else {
        System.out.println("К классу 'Parent' не применены какие-либо аннотации.");
    	  }

    	  if(child.getClass().getAnnotations().length > 0) {
            System.out.println("\nДля класса 'Child' применены следующие аннотации: ");
        	for(Annotation annotationName: child.getClass().getAnnotations()) {
                System.out.println(annotationName);
        }
    } else {
        System.out.println("\nК классу 'Child' не применены какие-либо аннотации.");
        }
    }
}
Результат выполнения программы:
Как говорится, что и требовалось доказать. :-)
В коде класса Main используется рефлексия (Reflection), что может усложнить его понимание. Поскольку целью этой статьи является ознакомление вас с аннотациями, то механизм рефлексии в ней не рассматривается.
4.5 Аннотация @Repeatable
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable
Иногда возникают ситуации, когда необходимо повторно применить одну и ту же аннотацию к какому-то элементу (объявлению класса, интерфейсу, полю или к используемому типу).
До Java 8 применялось группирование аннотаций в единый контейнер аннотаций. Выглядело это следующим образом.
Определим повторяемую аннотацию Game:
@interface Game {
    String name() default "Что-то под вопросом";
    String day();
}
Определим контейнер Games:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Games {
    Game[] value();
}
Использовалось это так:
@Games({
    @Game(name = "Крикет",  day = "воскресенье"),
    @Game(day = "четверг"),
    @Game(name = "Хоккей",   day = "понедельник")
})
public class Main {
    public static void main(String[] args) {
        Games games = Main.class.getAnnotation(Games.class);

        for (Game game : games.value()) {
            System.out.println(game.name() + " в " + game.day());
        }
    }
}
Обратите внимание, повторяющиеся аннотации разделяются запятой.
Результат выполнения программы:
С появлением Java 8 и @Repeatable все стало немного проще.
В поле value такой аннотации необходимо указать контейнер для повторяющейся аннотации. Контейнер также представляет собой аннотацию, в которой поле value является массивом типа повторяющейся аннотации.
Таким образом, мы должны создать контейнерную аннотацию, а затем указать её тип в качестве аргумента.
В нашем случае, перед определением аннотации @Game необходимо добавить новую аннотацию @Repeatable:
import java.lang.annotation.Repeatable;

@Repeatable(Games.class)
@interface Game {
    String name() default "Что-то под вопросом";
    String day();
}
Теперь перед определением класса Main можно применить несколько раз аннотацию @Game:
@Game(name = "Крикет",  day = "воскресенье")
@Game(day = "четверг")
@Game(name = "Хоккей",   day = "понедельник")
public class Main {
    public static void main(String[] args) {
        Games games = Main.class.getAnnotation(Games.class);

    	  for (Game game : games.value()) {
            System.out.println(game.name() + " в " + game.day());
    	  }
    }
}
Результат выполнения программы тот же:
Мы также можем вместо getAnnotation (Games.class) использовать getAnnotationsByType (Game.class) или getDeclaredAnnotationsByТуре (Game.class):
@Game(name = "Крикет",  day = "воскресенье")
@Game(day = "вторник")
@Game(name = "Хоккей",   day = "пятница")
public class Main {
    public static void main(String[] args) {
        Game[] arrayGames = Main.class.getAnnotationsByType(Game.class);
    	  for(Game game : arrayGames) {
            System.out.println(game.name() + " в " + game.day());
    	  }
    }
}
Результат будет тем же.
5. Аннотации для кода
Их пять:
  • @Override: указывает, что метод переопределяет, объявленный в суперклассе или интерфейсе метод
  • @Deprecated: помечает код, как устаревший
  • @SuppressWarnings: отключает для аннотированного элемента предупреждения компилятора. Обратите внимание, что если необходимо отключить несколько категорий предупреждений, их следует добавить в фигурные скобки, например @SuppressWarnings ({"unchecked", "cast"}).
  • @SafeVarargs: отключает предупреждения для всех методов или конструкторов, принимающих в качестве параметра varargs
  • @FunctionalInterface: помечает интерфейсы, имеющие только один абстрактный метод (при этом они могут содержать любое количество методов по умолчанию или статических)
Рассмотрим перечисленные аннотации более детально.
5.1 Аннотация @Override
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override
Аннотация @Override относится к маркерным аннотациям и указывает, что метод переопределяет/реализует унаследованный метод. Эта информация не является строго необходимой, но помогает уменьшить количество ошибок, поскольку при такой аннотации компилятор должен генерировать сообщение об ошибке, если не выполняется одно из двух следующих условий:
— Метод переопределяет или реализует метод, объявленный в супертипе
— У метода есть сигнатура (название метода + список параметров), эквивалентная переопределяемой сигнатуре метода, объявленного в родительском классе/интерфейсе.
Продемонстрируем применение аннотации. Создадим класс Parent с методом display(), класс Child, который является его наследником, и класс Main, который создает экземпляр Child и запускает метод display():
public class Parent {
    public void display() {
        System.out.println("Выполнился метод из родительского класса");
    }
}
public class Child extends Parent {
    public void display() {
        System.out.println("Выполнился метод из класса-наследника");
    }
}
public class Main {
    public static void main(String args[]) {
        Child instance = new Child();
    	  instance.display();
    }
}
Результат выполнения программы:
Давайте умышленно добавим ошибку в названии метода в классе Child:
public class Child extends Parent {
    public void dispay() {
        System.out.println("Выполнился метод из класса-наследника");
    }
}
Результат выполнения программы:
В итоге в классе Child мы имеем два метода: унаследованный метод суперкласса display() и новый метод dispay(). В классе Main у нас вызывается именно родительский метод, поскольку другого метода display() в классе Child нет.
Перед определением метода в класс Child добавим аннотацию @Override:
public class Child extends Parent {    
    @Override
    public void dispay() {
        System.out.println("Выполнился метод из класса-наследника");
    }
}
В такой ситуации IDE подчеркнет красным аннотацию, информируя, что «Method does not override method from its superclass» (метод не переопределяет метод его суперкласса).
При запуске получим ошибку компиляции:
Теперь уже компилятор сообщает нам, что «метод не переопределяет или не реализует метод его суперкласса»
Исправим «опечатку» в названии метода в классе Child и запустим программу:
public class Child extends Parent {
    @Override
    public void display() {
        System.out.println("Выполнился метод из класса-наследника");
    }
}
Результат выполнения программы:
Таким образом, применяя аннотацию @Override, мы даем задание компилятору выполнять проверку соответствия сигнатуры метода класса наследника классу родителя, что устраняет ошибки «по невнимательности» в виде опечаток.
5.2 Аннотация @Deprecated
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated
Определяющие аннотацию аннотации мы рассмотрели ранее, тут для вас не должно быть трудностей. Приведем примеры использования.
В код предыдущего примера добавим в класс Child аннотацию @Deprecated:
public class Child extends Parent {
    @Override
    @Deprecated(since = "1.2", forRemoval = true)
    public void display() {
        System.out.println("Выполнился метод из класса-наследника");
    }
}
Результат выполнения программы:
Результат остался тем же, ошибок нет. Но, обратите внимание на класс Main, используемый метод display() в IntellijIdea перечеркнут (!). Подобные визуальные оповещения есть и в других IDE.
public class Main {
    public static void main(String args[]) {
        Child instance = new Child();
    	  instance.display();  
    }
}
5.3 Аннотация @SuppressWarnings
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings
Предупреждающие сообщения компилятора обычно полезны, но иногда, они могут «зашумлять» полезную информацию. Особенно, когда мы не можем или не хотим их устранять. В таких случаях можно воспользоваться аннотацией @SuppressWarnings, отключив такие предупреждения, чтобы они не отображались.
Рассматривая код для аннотации @Override, мы вызывали в классе Main метод display() из класса Child. В тоже время метод display() из класса Parent не использовался. Среда IDE предполагала, что здесь где-то может быть ошибка (создали лишний метод или ошибочно используем не тот метод и т. д.) и соответственно, подсвечивая, выделяла цветом название неиспользуемого метода display() (и при наведении курсора выдавала сообщение: «Method 'Display()' is never used»).
Чтобы этого небыло, такое предупреждение можно отключить аннотацией @SuppressWarnings («unused»), установив её перед методом display():
public class Parent {
    @SuppressWarnings("unused")
    public void display() {
        System.out.println("Выполнился метод из родительского класса");
    }
}
Еще одним предупреждением компилятора является предупреждение о применении устаревшего метода, помеченного в коде аннотацией @Deprecated. Чтобы его устранить, необходимо пометить вызов метода main() в классе Main аннотацией @SuppressWarnings («deprecation»):
public class Main {
    @SuppressWarnings("deprecation")
    public static void main(String[] args) {
        Child instance = new Child();
    	  instance.display();
    }
}
Сам код теперь стал проще для чтения, а название метода display() не перечеркивается.
Чтобы отключить список из нескольких предупреждений, необходимо через запятую перечислить список предупреждений.
Например, в следующем виде:
@SuppressWarnings({"unused", "deprecation"})
5.4 Аннотация @SafeVarargs
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SafeVarargs
Эта аннотация, представленная в java 7, гарантирует, что тело аннотированного метода или конструктора не выполняет потенциально небезопасные операции с параметром varargs. Аннотация @SafeVarargs похожа на @SupressWarnings тем, что позволяет нам объявить, что конкретное предупреждение компилятора является ложным срабатыванием. Добавлять эту аннотацию мы можем только убедившись в том, что наши действия безопасны.
5.5 Аннотация @FunctionalInterface
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface
Как и аннотация @Override, аннотация @FunctionalInterface защищает код от возможных ошибок программиста. Несмотря на то, что любой интерфейс может содержать бесконечное множество абстрактных методов, функциональный интерфейс может содержать исключительно один абстрактный метод, иначе он не сможет использоваться в лямбда-выражении.
В тоже время, абстрактные методы, переопределяющие один из публичных методов класса Object, не учитываются.
Рассмотрим простейший пример: напишем функциональный интерфейс, реализующий что-то абстрактное и важное.
@FunctionalInterface
public interface MyFunctionalInterface {
    abstract public void abstractMethod();
    //abstract public void anotherAbstractMethod();
}
public class Main implements MyFunctionalInterface {

    public static void main(String[] args) {
        Main main = new Main();
    	  main.abstractMethod();
    }

    @Override
    public void abstractMethod() {
        System.out.println("Это сообщение из abstractMethod()");
    }
}
Если мы добавим еще один абстрактный метод (anotherAbstractMethod(), в коде он закомментирован), компилятор сообщит про ошибку:
Ошибка сообщает, что наш интерфейс functionalInterface не является функциональным интерфейсом и включает в себя несколько не переопределенных абстрактных методов.
Создадим еще один функциональный интерфейс и расширим им интерфейс, созданный нами ранее:
package functional;

@FunctionalInterface
public interface AnotherFunctionalInterface extends MyFunctionalInterface {
    abstract public void anotherAbstractMethod();
}
Вроде все хорошо, у нас один абстрактный метод, но IDE снова подсказывает о наличии той же самой ошибки:
Ошибка вызвана тем, что мы, расширяя интерфейс MyFunctionalInterface, наследуем абстрактный метод расширяемого интерфейса, и как результат, имеем два абстрактных метода, что не совместимо с аннотацией @FunctionalInterface.
Таким образом, мы не сможем использовать интерфейс, помеченный аннотацией @FunctionalInterface и включающей в себя явно или неявно два и более абстрактных метода.
6. Аннотация @Native
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Native
Начиная с Java 8, в пакете java.lang.annotation появилась новая аннотация под названием @Native, применимая только к полям. Она указывает, что аннотированное поле является константой, на которую можно ссылаться с нативного кода. Например, вот как она используется в классе Integer:
public final class Integer {
    @Native public static final int MIN_VALUE = 0x80000000;
    // последующий код опущен
}
Эта аннотация также может служить подсказкой для программных инструментов, генерирующих некоторые вспомогательные файлы.
7. Обработка аннотаций
В этой главе мы поговорим об обработке аннотаций. И рефлексия, и обработчики аннотаций, затрагиваемые здесь, заслуживают отдельной, полноценной статьи не меньшего размера, чем текущая. Частично применение рефлексии рассматривалось в примерах использования аннотаций по тексту выше, но это был исключительно необходимый минимум для демонстрации использования аннотаций.
7.1 Обработка аннотаций во время выполнения: рефлексия
Рефлексия, как способность получать информацию о коде во время его выполнения, появилась в Java с момента появления языка.
Например:
var session = request.getHttpSession();
var object = session.getAttribute("object"); (1)
var clazz = object.getClass();               (2)
var methods = clazz.getMethods();            (3)


for (var method : methods) {
    if (method.getParameterCount() == 0) {   (4)
        method.invoke(foo);                  (5)
    }
}
(1) Получаем объект, хранящийся в сеансе
(2) Получаем класс среды выполнения (runtime class) объекта
(3) Получаем все общедоступные методы, имеющиеся у объекта
(4) Если у метода нет параметра, то
(5) Вызываем этот метод
С появлением аннотаций рефлексия получила соответствующие улучшения:
Фреймворки начали использовать аннотации для различных сценариев использования. Среди них сценарий конфигурирования был одним из наиболее часто используемых: например, вместо (или, точнее, в дополнение к) XML, Spring добавил возможность конфигурирования на основе аннотаций.
7.2 Обработка аннотаций во время компиляции: обработчики аннотаций
Долгое время и получатели данных, и поставщики данных были довольны доступом через рефлексию к аннотациям во время выполнения. Поскольку основное внимание уделяется сценариям конфигурирования, рефлексия происходит во время запуска приложения. В ограниченном по производительности окружении это слишком большая нагрузка для приложений: наиболее известным примером такого окружения является платформа Android. Здесь хотелось бы иметь самое быстрое время запуска, но рефлексия замедляет его.
Альтернативным решением этой проблемы является обработка аннотаций во время их компиляции. Для этого компилятор должен быть настроен на использование специальных обработчиков аннотаций. У них могут быть разные выходные данные: простые файлы, сгенерированный код и т. д. Компромисс этого подхода заключается в том, что компиляция приложения каждый раз снижает производительность, но при этом не влияет на время запуска.
Одним из первых фреймворков, в которых использовался этот подход для генерации кода, был Dagger: это DI-фреймворк (Dependency Injection) для Android. Работа фреймворка базируется не на времени выполнения (runtime-based), а на времени компиляции (compile-time). Долгое время генерация кода во время компиляции использовалась только в экосистеме Android.
Однако, в последнее время такой подход приняли и такие серверные фреймворки, как Quarkus и Micronaut. Цель состоит в том, чтобы сократить время запуска приложения за счет генерации кода во время компиляции вместо самоанализа во время выполнения. Кроме того, предварительная компиляция полученного байт-кода в собственный код дополнительно сокращает время запуска, а также потребление памяти.
Мир обработчиков аннотаций огромен: этот раздел представляет собой очень небольшое введение, поэтому при желании можно продолжить их изучение.
Обработчик аннотаций — это просто определенный класс, который необходимо зарегистрировать во время компиляции. Зарегистрировать их можно несколькими способами. С Maven это просто вопрос настройки плагина компилятора:
pom.xml
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <annotationProcessors>
                    <annotationProcessor>ch.frankel.blog.SampleProcessor</annotationProcessor>
                </annotationProcessors>
            </configuration>
        </plugin>
    </plugins>
</build>
Сам обработчик должен реализовывать Processor, но абстрактный класс AbstractProcessor самостоятельно реализует большую часть своих методов, кроме метода process(): на практике достаточно наследоваться от AbstractProcessor. Очень упрощенная схема API выглядит так:
Давайте создадим очень простой обработчик. Он должен перечислять только классы, помеченные конкретными аннотациями. Настоящие процессоры аннотаций, скорее всего, сделают что-нибудь полезное, например сгенерируют код, но эта дополнительная логика выходит далеко за рамки этого поста.
@SupportedAnnotationTypes("ch.frankel.blog.*")                                          (1)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SampleProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations,                      (2)
    RoundEnvironment env) {
        annotations.forEach(annotation -> {                                             (3)
            Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation); (4)
            elements.stream()
                    .filter(TypeElement.class::isInstance)                              (5)
                    .map(TypeElement.class::cast)                                       (6)
                    .map(TypeElement::getQualifiedName)                                 (7)
                    .map(name -> "Class " + name + " is annotated with " +         annotation.getQualifiedName())
                    .forEach(System.out::println);
        });
    return true;
    }
}
(1) Обработчик будет вызываться для каждой аннотации, принадлежащей пакету ch.frankel.blog
(2) process(): основной метод, подлежащий переопределению
(3) Цикл вызывается для каждой аннотации
(4) Аннотация не так интересна, как аннотированный ею элемент. Это способ получить аннотированный элемент
(5) В зависимости от того, какой элемент аннотирован, его необходимо привести к правильному дочернему интерфейсу Element. Здесь могут быть аннотированы только классы, поэтому, переменная должна быть протестирована, чтобы проверить, имеет ли назначаемый TypeElement доступ к своим дополнительным атрибутам далее по цепочке операций
(6) Нам нужно полное имя класса, для которого установлена аннотация, поэтому необходимо привести его к типу, который делает этот конкретный атрибут доступным
(7) получаем полное имя от TypeElement
Заключение
Аннотации очень эффективны и не важно, используются ли они во время выполнения или во время компиляции. С другой стороны, самая большая проблема заключается в том, что они работают как будто по волшебству: нет простого способа узнать, какой класс, использующий рефлексию, или обработчик аннотаций их использует. Каждый в своем контексте должен решать, перевешивают ли плюсы аннотаций их минусы. Использование аннотаций без каких-либо предположений о будущем программы оказывает большую медвежью услугу такому коду… так же, как и отказ от них из-за неуместной идеологии.
Автор: Малянов Игорь
Технический редактор: Чимаев Максим
Оцените статью, если она вам понравилась!