Тема 27. Аннотации - BelyiZ/JavaCourses GitHub Wiki

Содержание:

  1. Обзор функциональности аннотаций
  2. Создание аннотаций
  3. Примеры использования
  4. Список литературы/курсов

#Обзор функциональности аннотаций Аннотации – это пометки, с помощью которых программист указывает компилятору Java и средствам разработки, что делать с участками кода помимо исполнения программы. Аннотировать можно переменные, параметры, классы, пакеты. Можно писать свои аннотации или использовать стандартные – встроенные в Java.

Аннотации представляют из себя дескрипторы, включаемые в текст программы, и используются для хранения метаданных программного кода, необходимых на разных этапах жизненного цикла программы. Информация, хранимая в аннотациях, может использоваться соответствующими обработчиками для создания необходимых вспомогательных файлов или для маркировки классов, полей и т.д.

Аннотации в Java позволяют:

  • автоматически создавать конфигурационные XML-файлы и дополнительный Java-код на основе исходного аннотированного кода;
  • документировать приложения и базы данных параллельно с их разработкой;
  • проектировать классы без применения маркерных интерфейсов;
  • быстрее подключать зависимости к программным компонентам;
  • выявлять ошибки, незаметные компилятору;
  • решать другие задачи по усмотрению программиста.

С момента появления языка Java возникла необходимость помечать, для выполнения тех или иные действий, определенным образом класс или иерархию классов. До Java 5 это делалось через интерфейсы без методов. Этот вид интерфейса не похож ни на один другой. Он не определяет никаких контрактов между собой и реализующим его классами, т.к. всегда пуст. Поэтому он называется маркерным интерфейсом. Интерфейсы без каких-либо методов действуют как маркеры. Такие интерфейсы нужны для маркировки чего-либо для JVM, компилятора или какой-либо библиотеки. Они лишь говорят компилятору, что объекты классов, которые имплементируют такой интерфейс без методов, должны иметь отличительные черты, восприниматься иначе.

Serializable и Cloneable — два примера маркерных интерфейсов, которые достались нам в наследство. Например, Serializable (java.io.Serializable) позволяет пометить класс, сообщая о том, что его экземпляры можно сериализовать. При этом перед сериализацией делается проверка на наличие имплементации этого интерфейса.

С появлением аннотаций необходимость в использовании маркерных интерфейсов хоть и отпала, но до сих пор повсеместно используется. Пример интерфейса и аналогичной ему аннотации:

public class Sport implements MarkerInterface {
    
} 
@MyAnnotation
public class Sport {
    
} 

Преимущество использования:

  • Аннотации позволяют «отвязать» интерфейс от наследников в случае если класс реализует интерфейс, и все его наследники реализовывают этот интерфейс. Обычно нельзя «отвязать» интерфейс от наследников. Но при использовании аннотаций - можно. Но в этом есть и минус — проверка наличия маркера (аннотации) теперь проводится во время исполнения, а не во время компиляции, что чревато ошибками. То есть если требуется знать, могут ли методы принимать объекты каких-то классов, то такие классы удобнее пометить (реализовать) интерфейсами, так как ошибка выявится на этапе компиляции.

  • Если необходимо провести анализ метаданных класса, то использование аннотаций даёт больше возможностей, в том числе принимая во внимание возможность аннотаций иметь параметры. Однако в этом случае анализ аннотаций происходит во время исполнения кода.

Обработка аннотаций

На основе аннотаций компилятор может с помощью специальных обработчиков генерировать новый код и файлы конфигурации.

Обработчиками обычно выступают библиотеки и утилиты, которые можно брать у сторонних авторов (или создавать самостоятельно) и прикреплять к проекту в среде разработки. Способ подключения зависит от IDE или системы сборки.

Парсинг аннотаций происходит циклически. Компилятор ищет их в пользовательском коде и выбирает подходящие обработчики. Если вызванный обработчик на основе аннотации создаёт новые файлы с кодом, начинается следующий этап, где исходным материалом становится сгенерированный код. Так продолжается до тех пор, пока не будут созданы все необходимые файлы.

#Создание аннотаций

Каждая из аннотаций имеет 2 главных обязательных параметра:

  • Тип хранения (Retention).
  • Тип объекта над которым она указывается (Target).

Тип хранения Под "типом хранения" понимается стадия до которой "доживает" наша аннотация внутри класса. Каждая аннотация имеет только один из возможных "типов хранения" указанный в классе RetentionPolicy:

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

Тип объекта над которым указывается Данное описание стоит понимать практически буквально, т.к. в Java аннотации могут указываться над чем угодно (Поля, класса, функции, т.д.) и для каждой аннотации указывается, над чем конкретно она может быть задана. Здесь уже нет правила "что-то одно", аннотацию можно указывать над всем ниже перечисленным, либо же выбрать только нужные элементы класса ElementType: ANNOTATION_TYPE - другая аннотация. CONSTRUCTOR - конструктор класса. FIELD - поле класса. LOCAL_VARIABLE - локальная переменная. METHOD - метод класса. PACKAGE - описание пакета package. PARAMETER - параметр метода public void hello(@Annontation String param){}. TYPE - указывается над классом.

Существуют аннотации, которые предоставляются стандартной библиотекой. На момент версии Java SE 1.8 стандартная библиотека языка предоставляет нам 10 аннотаций.

Аннотации можно классифицировать по следующим признакам:

  • аннотации для аннотаций: @Target
  • аннотации типов: @Retention @Documented @Inherited @Repeatable
  • аннотации для кода: @Override @Deprecated @SuppressWarnings @SafeVarargs @FunctionalInterface
  • нативные аннотации
  • аннотации, написанные программистом

Рассмотрим самые часто встречающиеся из них, остальные возможно изучить на ресурсе Welcome to Javadoc.

@Override

Retention: SOURCE;

Target: METHOD.

Данная аннотация показывает, что метод над котором она прописана, наследован у родительского класса. @Override более чем полезна, она не только позволяет понять, какие методы были определены в этом классе впервые, а какие уже есть у родителей, что повышает читаемость вашего кода, но также данная аннотация служит "самопроверкой", что вы не ошиблись при определении перегружаемой функции.

  • @Deprecated

Retention: RUNTIME;

Target: CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE.

Данная аннотация указывает на методы, классы или переменные, которые является "устаревшими" и могут быть убраны в последующих версиях продукта.

С данной аннотацией обычно сталкиваются те, кто читает документацию каких-либо API, либо той же стандартной библиотеки Java. Иногда эту аннотацию игнорируют, т.к. она не вызывает никаких ошибок и в принципе сама по себе сильно жить не мешает. Однако главный посыл, который несет в себе данная аннотация – "мы придумали более удобный метод реализации данного функционала, используй его, не используй старый". Таким образом, если видите @Deprecated- лучше стараться не использовать то, над чем она висит, если в этом нет прям крайней необходимости и, возможно, стоит перечитать документацию, чтобы понять каким образом теперь реализуется задача, выполняемая устаревшим элементом.

Например вместо использований

 new Date().getYear() 

рекомендуется использовать

Calendar.getInstance().get(Calendar.YEAR)
  • @SuppressWarnings

Retention: SOURCE;

Target: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE

Данная аннотация отключает вывод предупреждений компилятора, которые касаются элемента над которым она указана. Является SOURCE аннотацией указываемой над полями, методами, классами.

  • @Retention

Retention: RUNTIME;

Target: ANNOTATION_TYPE;

Данная аннотация задает "тип хранения" аннотации над которой она указана. Эта аннотация используется даже для самой себя.

  • @Target

Retention: RUNTIME;

Target: ANNOTATION_TYPE;

Данная аннотация задает тип объекта над которым может указываться создаваемая нами аннотация.

Реализация базового определения аннотации имеет следующий вид:


@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).

#Примеры использования

  • @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» (метод не переопределяет метод его суперкласса).

При запуске получим ошибку компиляции: java: method does not override or implement a method from a supertype

Теперь уже компилятор сообщает, что «метод не переопределяет или не реализует метод его суперкласса».

Исправим "опечатку" в названии метода в классе Child и запустим программу:

public class Child extends Parent {
    @Override
    public void display() {
        System.out.println("Выполнился метод из класса-наследника");
    }
}

Результат выполнения программы: Выполнился метод из класса-наследника

Таким образом, применяя аннотацию @Override, мы даем задание компилятору выполнять проверку соответствия сигнатуры метода класса наследника классу родителя, что устраняет ошибки "по невнимательности" в виде опечаток.

  • @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 перечеркнут (!).

public class Main {
    public static void main(String args[]) {
        Child instance = new Child();
        instance.display();  
    }
}
  • @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"})
  • @Retention
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention

Аннотация @Retention (с англ. означает удержание, задержка) определяет, до какого шага во время компиляции аннотация будет доступна. Все шаги (они еще называются политиками) находятся в Enum:

  • RetentionPolicy.SOURCE: аннотация сохраняется только в исходном файле и удаляется во время компиляции.
  • RetentionPolicy.CLASS: аннотация сохраняется в файле .class во время компиляции, но недоступна во время выполнения через JVM.
  • 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]);
    }
}

Результат выполнения программы:

Общее кол-во аннотаций времени исполнения (RunTime): 2 1: RetentionAnnotation() 2: java.lang.Depricated(forRemoval=false,since="")

В этом примере мы создали свою собственную аннотацию RetentionAnnotation, а также использовали аннотацию @Deprecated, которая также имеет политику RetentionPolicy.RUNTIME. Если мы исправим политику аннотации RetentionAnnotation с RetentionPolicy.RUNTIME на RetentionPolicy.SOURCE (и закомментируем строку в классе Main, выводящую второй элемент массива), то программа отобразит только одну аннотацию deprecated, поскольку аннотация с RetentionPolicy.SOURCE во время компиляции будет удалена.

Список литературы/курсов

  1. https://docs.oracle.com/javase/tutorial/java/annotations/predefined.html
  2. https://java-blog.ru/osnovy/annotatsii-java
  3. https://javascopes.com/java-default-annotations-1e4f0b32/

Тема 24. Исключения | Оглавление | Тема 26. Работа с консолью и логами