Аннотации Java — основы
Если вы когда-либо видели кодовую базу Java, есть большая вероятность, что вы видели и что-то вроде @Override или подобного перед методом или классом. Такие тэги называются аннотациями. Аннотации — это тэги метаданных, которые помогают определить дополнительную информацию для классов, интерфейсов, методов или полей. Аннотации не добавляют дополнительной реализации к функциональному коду, но помогают с необязательной информацией, такой как:
- Информация для компилятора. Компилятор может использовать аннотации для обнаружения ошибок или подавления предупреждений.
- Информация для разработчика. Когда кто-то просматривает код, аннотации помогают сделать его читабельным и более легким для понимания.
- Обработка времени компиляции и развертывания. Программные средства могут обрабатывать аннотации для создания кода, XML-файлов и т.д.
- Обработка во время выполнения. Аннотации могут проверяться во время выполнения для прохождения тестов.
Java содержит целый набор предопределенных аннотаций и даже позволяет определять свои собственные. Ниже приведем список наиболее широко распространенных и важных аннотаций.
Предопределенные аннотации
@Override
Аннотация @Override сообщает компилятору, что элемент подкласса переопределяет элемент родительского класса или суперкласса. Хотя эта аннотация не обязательна при переопределении метода, она помогает предотвратить ошибки. Если метод, помеченный как “override”, не может правильно переопределить метод суперкласса, компилятор выдает ошибку.
class Avatar <
public void destroyEvil() < . >// переопределенный
>class Aang extends Avatar <
@Override
// overriding method
public void destroyEvil() < . >
>
@SuppressWarnings
Предупреждения компилятора полезны, если вы их читаете, но они часто создают “шум” в терминале. @SuppressWarnings будет подавлять эти предупреждения. Компилятор Java может выдавать множество предупреждений, но с помощью @SuppressWarnings вы можете скрыть все предупреждения или выбрать предупреждения, появления которых вам хочется избежать.
@SuppressWarnings() будет скрывать предупреждения о непроверенных (unchecked) и устаревших (deprecated) методах.
@SuppressWarnings("unchecked")
void uncheckedWarning() <
List words = new ArrayList();
words.add("hello"); //выбрасывает предупреждение о непроверенном
>
@deprecated
Обозначение @deprecated также очень распространено. Это означает, что аннотированный метод устарел и больше не поддерживается разработчиками. Компилятор не будет обрабатывать устаревший метод иначе, чем обычный метод. Таким образом, даже если метод допустимо вызвать, возвращаемый им ответ может быть не идеальным. Это документация для разработчиков.
@depricated
public String prepareForY2K()
@author
Тэг @author — простая аннотация, документирующая автора метода или файла. Обычно соединяется с некоторой дополнительной информацией, такой как версия, номер релиза и т.д.
Oracle рекомендует писать тэги в следующем порядке:
- @author — документирует автора кода;
- @version — обеспечивает только одно обновление за раз (позволяет избежать блокировки);
- @param — документирует имя и описание параметров;
- @return — документирует возвращаемое значение; опускать, если возвращает пустоту (void);
- @throws — документирует проверенные исключения (объявленные в throws );
- @see — ссылка или указание на ссылку;
- @since — документирует версию продукта, с которой была добавлена новая функциональность;
- @deprecated — документирует, что код больше не поддерживается.
Тест-аннотации
Написание тестов — важнейший аспект цикла разработки и так же (если не более) важно, как и написание самой кодовой базы. Существуют различные аннотации, созданные явно для тестов.
@Test
@Test сообщает JUnit, что аннотированный метод должен выполняться как тест. Чтобы запустить метод, JUnit создает новый экземпляр класса, а затем вызывает тестовый метод.
Для этой аннотации можно указать два необязательных параметра:
@Timeout приводит к сбою метода тестирования, если выполнение занимает больше времени, чем указанное время, измеренное на часах в миллисекундах.
Например, следующий тест упадет (через 0,1 секунды):
@Test(timeout=100)
public void toInfinityAndBeyond() <
while(true);
>
@Expected объявляет, что тестовый метод должен выдавать определенное исключение, в противном случае тест завершится неудачей.
Например, в примере ниже произойдет сбой:
@Test(expected=NullPointerException.class)
public void outOfBounds() <
new ArrayList().get(1);
>
@Ignore
Аннотация @Ignore указывает игнорировать тест или группу тестов, чтобы избежать потенциального сбоя при выполнении.
Игнорировать тесты можно в двух сценариях:
- игнорировать тестовый метод, помеченный @Test .
- игнорировать все тесты на уровне класса.
@Ignore
@Test(expected=NullPointerException.class) //obviously wrong test
public void outOfBounds() <
new ArrayList().get(1);
>
@Before
Методы, помеченные тэгом @Before , выполняются перед каждым тестом. Это полезно, когда вы хотите выполнить некоторый код перед запуском теста, например, настроить тестовую среду. В Junit5 @Before был переименован в @beforeEach , что также работает.
Родственная аннотация @beforeAll или @BeforeClass используется, когда перед серией тестов необходимо выполнить дорогостоящую операцию, такую как запуск сервера или внесение изменений в базу данных.
@After
@After — это противоположность предыдущему тэгу. Все методы, помеченные @After , будут запущены после теста.
Методы @AfterAll или @AfterClass выполняются после выполнения всех тестов класса.
Все методы, аннотированные @beforeAll и @afterAll должны быть статическими, так как выполняются перед запуском тестов класса.
Однако методы @Before и @After не должны быть статическими, иначе компилятор выдаст ошибку.
public class OutputFileTest < @BeforeAll
public static void startServer() @Before
public void createTestLogFile() < . >
@After
public void deleteTestLogFile() < . >
@Test
public void test1() @Test
public void test2() @AfterAll
public static void stopServer() < . >
>
Код, приведенный выше, выполнится в следующем порядке:
0️⃣ startServer() 1️⃣ createTestLogFile() 2️⃣ test1() 3️⃣ deleteTestLogFile() 4️⃣ createTestLogFile() 5️⃣ test2() 6️⃣ deleteTestLogFile() 7️⃣ stopServer()
Этот список далеко не исчерпывающий, но он охватывает самые основные аннотации. Воспользуйтесь приведенными выше примерами, чтобы получить представление о лучших практиках в программировании на Java.
- Когда стоит использовать перечисления в Java?
- Java-библиотеки, которые повысят вашу производительность
- Как правильно учиться Java-программированию: история одного тьютора
Аннотации в Java и их обработка

Аннотация — это специальная конструкция языка, связанная с классом, методом или переменной, предоставляющая программе дополнительную информацию, на основе которой программа может предпринять дальнейшие действия или реализовать дополнительную функциональность, такую как генерация кода, проверка ошибок и т. д.
Помимо использования стандартных аннотаций из пакета java.lang, о которых мы поговорим далее, можно также создавать свои аннотации и обрабатывать их.
В этой статье мы обсудим назначение стандартных аннотаций, а также рассмотрим на практическом примере создание и обработку своих аннотаций.
Код примеров вы можете найти на GitHub.
Основы аннотаций
Аннотации начинаются с символа @ . Например, в пакете java.lang определены аннотации @Override и @SuppressWarnings .
Сама по себе аннотация не выполняет никаких действий. Она просто предоставляет информацию, которую можно использовать во время компиляции или в рантайме.
В качестве примера рассмотрим аннотацию @Override :
public class ParentClass < public String getName() > public class ChildClass extends ParentClass < @Override public String getname() >
Аннотация @Override используется для обозначения переопределенного метода из базового класса. Приведенная выше программа при компиляции выдаст ошибку, потому что метод getname() в классе ChildClass аннотирован @Override , но в родительском классе ParentClass метода getname() нет.
Используя аннотацию @Override в ChildClass , компилятор проверяет, что имя переопределенного метода в дочернем классе совпадает с именем метода в родительском классе.
Стандартные аннотации
Рассмотрим некоторые из распространенных стандартных аннотаций из пакета java.lang . Чтобы увидеть их влияние на поведение компилятора, запускайте примеры из командной строки, поскольку большинство IDE могут подавлять предупреждения.
@SuppressWarnings
Аннотация @SuppressWarnings используется для подавления предупреждений компилятора. Например, @SuppressWarnings («unchecked») отключает предупреждения, связанные с «сырыми» типами (Raw Types).
Давайте рассмотрим пример использования @SuppressWarnings :
public class SuppressWarningsDemo < public static void main(String[] args) < SuppressWarningsDemo swDemo = new SuppressWarningsDemo(); swDemo.testSuppressWarning(); >public void testSuppressWarning() < Map testMap = new HashMap(); testMap.put(1, "Item_1"); testMap.put(2, "Item_2"); testMap.put(3, "Item_3"); >>
Если мы запустим компиляцию из командной строки с параметром -Xlint:unchecked , то получим следующее сообщение:
javac -Xlint:unchecked ./com/reflectoring/SuppressWarningsDemo.java Warning: unchecked call to put(K,V) as a member of the raw type Map
Это пример легаси кода (до Java 5) — в коллекции мы можем случайно сохранить объекты разных типов. Для проверки подобных ошибок на этапе компиляции, были придуманы обобщенные типы (generics, дженерики). Чтобы этот код компилировался без предупреждений измените строку:
Map testMap = new HashMap();
Map testMap = new HashMap<>();
Если подобного легаси кода много, то вы вряд ли захотите вносить изменения, поскольку это влечет за собой много регрессионного тестирования. В этом случае к классу можно добавить аннотацию @SuppressWarning , чтобы логи не загромождались избыточными предупреждениями.
@SuppressWarnings() public class SuppressWarningsDemo
Теперь при компиляции предупреждений не будет.
@Deprecated
Аннотация @Deprecated используется для пометки устаревших методов или типов.
IDE автоматически обрабатывают эту аннотацию и обычно отображают устаревший метод зачеркнутым шрифтом, сообщая разработчику, что больше не следует его использовать.
В примере ниже метод testLegacyFunction() помечен как устаревший:
public class DeprecatedDemo < @Deprecated(since = "4.5", forRemoval = true) public void testLegacyFunction() < System.out.println("This is a legacy function"); >>
В атрибуте since этой аннотации содержится версия, с которой элемент объявлен устаревшим, а forRemoval указывает, будет ли элемент удален в следующей версии.
Теперь вызов устаревшего метода, вызовет предупреждение во время компиляции, указывая, что лучше этот метод не использовать:
./com/reflectoring/DeprecatedDemoTest.java:8: warning: [removal] testLegacyFunction() in DeprecatedDemo has been deprecated and marked for removal demo.testLegacyFunction(); ^ 1 warning
@Override
Мы уже упоминали выше аннотацию @Override . Она используется для проверки переопределенных методов во время компиляции на такие ошибки, как опечатки в регистре символов:
public class Employee < public void getEmployeeStatus()< System.out.println("This is the Base Employee class"); >> public class Manager extends Employee < public void getemployeeStatus()< System.out.println("This is the Manager class"); >>
Здесь мы хотели переопределить метод getEmployeeStatus() , но неправильно написали имя метода. Это может привести к серьезным ошибкам. Приведенная выше программа скомпилируется и запуститься без проблем, не обнаружив эту ошибку при компиляции.
Если добавить аннотацию @Override к методу getemployeeStatus() , то при компиляции получим следующую ошибку:
./com/reflectoring/Manager.java:5: error: method does not override or implement a method from a supertype @Override ^ 1 error
@FunctionalInterface
Аннотация @FunctionalInterface используется для указания того, что в интерфейсе не может быть более одного абстрактного метода. Если абстрактных методов будет больше одного, то компилятор выдаст ошибку. Функциональные интерфейсы появились в Java 8 для реализации лямбда-выражений и гарантии того, что в них не более одного абстрактного метода.
Но и без аннотации @FunctionalInterface компилятор выдаст ошибку, если вы включите в интерфейс больше одного абстрактного метода. Так зачем же нужна необязательная аннотация @FunctionalInterface ?
Давайте рассмотрим следующий пример:
@FunctionalInterface interface Print
Если в интерфейс Print мы добавим еще один метод printString2() , то компилятор или IDE выдаст ошибку.
А что, если интерфейс Print находится в отдельном модуле и без аннотации @FunctionalInterface ? Разработчики этого модуля могут легко добавить в интерфейс еще один метод и сломать ваш код. Добавив аннотацию @FunctionalInterface , мы сразу получим предупреждение в IDE:
Multiple non-overriding abstract methods found in interface com.reflectoring.Print
Поэтому рекомендуется всегда использовать аннотацию @FunctionalInterface , если интерфейс должен использоваться в качестве лямбды.
@SafeVarargs
Функциональность varargs позволяет создавать методы с переменным количеством аргументов. До Java 5 единственной возможностью создания методов с необязательными параметрами было создание нескольких методов, каждый из которых с разным количеством параметров. Varargs позволяет создать один метод с переменным количеством параметров с помощью следующего синтаксиса:
// можно написать так: void printStrings(String. stringList) // вместо этого мы делаем: void printStrings(String string1, String string2)
Однако при использовании в аргументах метода обобщенных типов выдаются предупреждения. Аннотация @SafeVarargs позволяет подавить их:
package com.reflectoring; import java.util.Arrays; import java.util.List; public class SafeVarargsTest < private void printString(String test1, String test2) < System.out.println(test1); System.out.println(test2); >private void printStringVarargs(String. tests) < for (String test : tests) < System.out.println(test); >> private void printStringSafeVarargs(List. testStringLists) < for (ListtestStringList : testStringLists) < for (String testString : testStringList) < System.out.println(testString); >> > public static void main(String[] args) < SafeVarargsTest test = new SafeVarargsTest(); test.printString("String1", "String2"); test.printString("*******"); test.printStringVarargs("String1", "String2"); test.printString("*******"); ListtestStringList1 = Arrays.asList("One", "Two"); List testStringList2 = Arrays.asList("Three", "Four"); test.printStringSafeVarargs(testStringList1, testStringList2); > >
Методы printString() и printStringVarargs() приводят к одинаковому результату. Но при компиляции для метода printStringSafeVarargs() выдается предупреждение, поскольку в нем используются обобщенные типы:
javac -Xlint:unchecked ./com/reflectoring/SafeVarargsTest.java ./com/reflectoring/SafeVarargsTest.java:28: warning: [unchecked] Possible heap pollution from parameterized vararg type List private void printStringSafeVarargs(List. testStringLists) < ^ ./com/reflectoring/SafeVarargsTest.java:52: warning: [unchecked] unchecked generic array creation for varargs parameter of type List[] test.printStringSafeVarargs(testStringList1, testStringList2); ^ 2 warnings
Добавив аннотацию @SafeVarargs , мы можем избавиться от этого предупреждения:
@SafeVarargs private void printStringSafeVarargs(List. testStringLists)
Пользовательские аннотации
Мы можем создавать свои аннотации, например, для реализации следующей функциональности:
- Уменьшение дублирования кода.
- Автоматизация генерации бойлерплейт кода.
- Отлов ошибок во время компиляции, например, потенциальные Null Pointer Exception.
- Настройка поведения в рантайме на основе наличия аннотации.
Для примера рассмотрим аннотацию @Company :
@Company < name="ABC" city="XYZ" >public class CustomAnnotatedEmployee
При создании экземпляров класса CustomAnnotatedEmployee все экземпляры будут содержать одно и то же название компании (name) и города (city) — больше не нужно добавлять эту информацию в конструктор.
Создать пользовательскую аннотацию можно с помощью ключевого слова @interface :
public @interface Company
Чтобы указать информацию об области действия аннотации и о типах элементов, к которым она может быть применена, используются мета-аннотации.
Например, чтобы указать, что аннотация применяется только к классам, используется аннотация @Target(ElementType.TYPE) . А мета-аннотация @Retention(RetentionPolicy.RUNTIME) указывает, что аннотация должна быть доступна в рантайме.
С мета-аннотациями наша аннотация @Company выглядит следующим образом:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Company
Далее добавим атрибуты в нашу аннотацию: имя ( name ) и город ( city ). Добавляем их, как показано ниже:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Company
Создадим класс CustomAnnotatedEmployee и применим к нему аннотацию @Company :
@Company public class CustomAnnotatedEmployee < private int id; private String name; public CustomAnnotatedEmployee(int id, String name) < this.id = id; this.name = name; >public void getEmployeeDetails() < System.out.println("Employee Id: " + id); System.out.println("Employee Name: " + name); >>
Прочитать аннотацию @Company в рантайме можно следующим образом:
import java.lang.annotation.Annotation; public class TestCustomAnnotatedEmployee < public static void main(String[] args) < CustomAnnotatedEmployee employee = new CustomAnnotatedEmployee(1, "John Doe"); employee.getEmployeeDetails(); Annotation companyAnnotation = employee .getClass() .getAnnotation(Company.class); Company company = (Company)companyAnnotation; System.out.println("Company Name: " + company.name()); System.out.println("Company City: " + company.city()); >>
Результат будет следующий:
Employee Id: 1 Employee Name: John Doe Company Name: ABC Company City: XYZ
Анализируя аннотацию в рантайме, мы можем получить доступ к некоторой общей информации обо всех сотрудниках и избежать дублирования кода.
Мета-аннотации
Мета-аннотации — это аннотации, применяемые к другим аннотациям для предоставления информации об аннотации компилятору или среде выполнения.
Мета-аннотации могут ответить на следующие вопросы об аннотации:
- Может ли аннотация наследоваться дочерними классами?
- Должна ли аннотация отображаться в документации?
- Можно ли применить аннотацию несколько раз к одному и тому же элементу?
- К какому типу элементов можно применить аннотацию: к классу, методу, полю и т.д.?
- Обрабатывается ли аннотация во время компиляции или в рантайме?
@Inherited
По умолчанию аннотация не наследуется от родительского класса к дочернему. Мета-аннотация @Inherited позволяет ей наследоваться:
@Inherited @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Company < String name() default "ABC"; String city() default "XYZ"; >@Company public class CustomAnnotatedEmployee < private int id; private String name; public CustomAnnotatedEmployee(int id, String name) < this.id = id; this.name = name; >public void getEmployeeDetails() < System.out.println("Employee Id: " + id); System.out.println("Employee Name: " + name); >> public class CustomAnnotatedManager extends CustomAnnotatedEmployee < public CustomAnnotatedManager(int id, String name) < super(id, name); >>
Поскольку CustomAnnotatedEmployee аннотирован @Company , а CustomAnnotatedManager наследуется от него, то нет необходимости ставить аннотацию на класс CustomAnnotatedManager .
Давайте проверим это.
public class TestCustomAnnotatedManager < public static void main(String[] args) < CustomAnnotatedManager manager = new CustomAnnotatedManager(1, "John Doe"); manager.getEmployeeDetails(); Annotation companyAnnotation = manager .getClass() .getAnnotation(Company.class); Company company = (Company)companyAnnotation; System.out.println("Company Name: " + company.name()); System.out.println("Company City: " + company.city()); >>
Аннотация @Company доступна, хотя мы не указывали ее явно для класса Manager .
@Documented
@Documented указывает, что аннотация должна присутствовать в JavaDoc.
По умолчанию информация об аннотациях не отображается в JavaDoc-документации, но если использовать @Documented, она появится:
@Inherited @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Company
@Repeatable
@Repeatable позволяет использовать аннотацию несколько раз на одном методе, классе или поле. Для использования @Repeatable — аннотации необходимо создать аннотацию-контейнер, которая хранит значение в виде массива исходных аннотаций:>
@Target(ElementType.TYPE) @Repeatable(RepeatableCompanies.class) @Retention(RetentionPolicy.RUNTIME) public @interface RepeatableCompany
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface RepeatableCompanies < RepeatableCompany[] value() default<>; >
Использовать аннотацию можно следующим образом:
@RepeatableCompany @RepeatableCompany(name = "Name_2", city = "City_2") public class RepeatedAnnotatedEmployee
public class TestRepeatedAnnotation < public static void main(String[] args) < RepeatableCompany[] repeatableCompanies = RepeatedAnnotatedEmployee.class .getAnnotationsByType(RepeatableCompany.class); for (RepeatableCompany repeatableCompany : repeatableCompanies) < System.out.println("Name: " + repeatableCompany.name()); System.out.println("City: " + repeatableCompany.city()); >> >
Получим следующий результат, отображающий значение нескольких аннотаций @RepeatableCompany :
Name: Name_1 City: City_1 Name: Name_2 City: City_2
@Target
@Target определяет типы элементов, к которым может применяться аннотация. Например, в приведенном выше примере аннотация @Company была определена как TYPE, и поэтому может быть применена только к классам.
Давайте попробуем применить аннотацию @Company к методу:
@Company public class Employee < @Company public void getEmployeeStatus()< System.out.println("This is the Base Employee class"); >>
В этом случае мы получим ошибку компилятора: @Company not applicable to method .
Существуют следующие типы целей, названия которых говорят сами за себя:
- ElementType.ANNOTATION_TYPE
- ElementType.CONSTRUCTOR
- ElementType.FIELD
- ElementType.LOCAL_VARIABLE
- ElementType.METHOD
- ElementType.PACKAGE
- ElementType.PARAMETER
- ElementType.TYPE
@Retention
@Retention указывает, когда аннотация будет доступна:
- SOURCE — аннотация доступна в исходном коде и удаляется после компиляции.
- CLASS — аннотация сохраняется в class-файле во время компиляции, но недоступна при выполнении программы.
- RUNTIME — аннотация доступна в рантайме.
Если аннотация нужна только для проверки ошибок во время компиляции, как это делает @Override , мы используем SOURCE. Если аннотация нужна для обеспечения функциональности в рантайме, например, @Test в JUnit, то используем RUNTIME. Давайте поэкспериментируем с разными значениями RetentionPolicy :
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface ClassRetention < >@Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface SourceRetention < >@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface RuntimeRetention
Создадим класс, который использует все три аннотации:
@SourceRetention @RuntimeRetention @ClassRetention public class EmployeeRetentionAnnotation
Для проверки доступности аннотаций запустите следующий код:
public class RetentionTest < public static void main(String[] args) < SourceRetention[] sourceRetention = new EmployeeRetentionAnnotation() .getClass() .getAnnotationsByType(SourceRetention.class); System.out.println("Source Retentions at runtime: " + sourceRetention.length); RuntimeRetention[] runtimeRetention = new EmployeeRetentionAnnotation() .getClass() .getAnnotationsByType(RuntimeRetention.class); System.out.println("Runtime Retentions at runtime: " + runtimeRetention.length); ClassRetention[] classRetention = new EmployeeRetentionAnnotation() .getClass() .getAnnotationsByType(ClassRetention.class); System.out.println("Class Retentions at runtime: " + classRetention.length); >>
Результат будет следующим:
Source Retentions at runtime: 0 Runtime Retentions at runtime: 1 Class Retentions at runtime: 0
Итак, мы убедились, что в рантайме доступна только RUNTIME-аннотация.
Классификация аннотаций
Аннотации можно классифицировать по количеству передаваемых в них параметров: без параметров, с одним параметром и с несколькими параметрами.
Маркерные аннотации
Маркерные аннотации не содержат никаких членов или данных. Для определения наличия аннотации можно использовать метод isAnnotationPresent() .
Например, если бы у нашей компании было несколько клиентов с разными способами передачи данных, мы могли бы аннотировать класс аннотацией, указывающей способ передачи данных:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface CSV
Класс Client может использовать аннотацию следующим образом:
@CSV public class XYZClient
Обработать аннотацию можно следующим образом:
public class TestMarkerAnnotation < public static void main(String[] args) < XYZClient client = new XYZClient(); Class clientClass = client.getClass(); if (clientClass.isAnnotationPresent(CSV.class))< System.out.println("Write client data to CSV."); >else < System.out.println("Write client data to Excel file."); >> >
На основании присутствия аннотации @CSV , мы можем решить, куда записать информацию — в CSV или в файл Excel. Приведенная выше программа выдаст следующий результат:
Write client data to CSV.
Аннотации с одним значением
Аннотации с одним значением содержат только один атрибут, который принято называть value.
Давайте создадим аннотацию SingleValueAnnotationCompany с одним атрибутом value :
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface SingleValueAnnotationCompany
Создайте класс, использующий аннотацию:
@SingleValueAnnotationCompany("XYZ") public class SingleValueAnnotatedEmployee < private int id; private String name; public SingleValueAnnotatedEmployee(int id, String name) < this.id = id; this.name = name; >public void getEmployeeDetails() < System.out.println("Employee Id: " + id); System.out.println("Employee Name: " + name); >>
Запустите следующий пример:
public class TestSingleValueAnnotatedEmployee < public static void main(String[] args) < SingleValueAnnotatedEmployee employee = new SingleValueAnnotatedEmployee(1, "John Doe"); employee.getEmployeeDetails(); Annotation companyAnnotation = employee .getClass() .getAnnotation(SingleValueAnnotationCompany.class); SingleValueAnnotationCompany company = (SingleValueAnnotationCompany)companyAnnotation; System.out.println("Company Name: " + company.value()); >>
Переданное значение "XYZ" переопределяет значение атрибута аннотации по умолчанию. Результат выглядит следующим образом:
Employee Id: 1 Employee Name: John Doe Company Name: XYZ
Полные аннотации
Они состоят из нескольких пар "имя-значение". Например, Company(name = "ABC", city = "XYZ") . Рассмотрим наш исходный пример Company:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Company
Давайте создадим класс MultiValueAnnotatedEmployee со значением параметров, как показано ниже. Значения по умолчанию будут перезаписаны.
@Company(name = "AAA", city = "ZZZ") public class MultiValueAnnotatedEmployee
Запустите следующий пример:
public class TestMultiValueAnnotatedEmployee < public static void main(String[] args) < MultiValueAnnotatedEmployee employee = new MultiValueAnnotatedEmployee(); Annotation companyAnnotation = employee.getClass().getAnnotation(Company.class); Company company = (Company)companyAnnotation; System.out.println("Company Name: " + company.name()); System.out.println("Company City: " + company.city()); >>
Company Name: AAA Company City: ZZZ
Практический пример
В качестве практического примера обработки аннотаций напишем простой аналог аннотации @Test из JUnit. Пометив методы аннотацией @Test , мы сможем определить в рантайме, какие методы тестового класса нужно запускать как тесты.
Сначала создадим маркерную аннотацию для методов-тестов:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test
Далее создадим класс AnnotatedMethods , в котором применим аннотацию @Test к методу test1() . Это позволит выполнить метод в рантайме. У метода test2() аннотации нет и он не должен выполняться.
public class AnnotatedMethods < @Test public void test1() < System.out.println("This is the first test"); >public void test2() < System.out.println("This is the second test"); >>
Теперь напишем код для запуска тестов из класса AnnotatedMethods :
import java.lang.annotation.Annotation; import java.lang.reflect.Method; public class TestAnnotatedMethods < public static void main(String[] args) throws Exception < ClassannotatedMethodsClass = AnnotatedMethods.class; for (Method method : annotatedMethodsClass.getDeclaredMethods()) < Annotation annotation = method.getAnnotation(Test.class); Test test = (Test) annotation; // If the annotation is not null if (test != null) < try < method.invoke(annotatedMethodsClass .getDeclaredConstructor() .newInstance()); >catch (Throwable ex) < System.out.println(ex.getCause()); >> > > >
Через метод getDeclaredMethods() мы получаем методы класса AnnotatedMethods . Затем перебираем методы и проверяем, аннотирован ли метод аннотацией @Test . Наконец, выполняем вызов методов, которые были аннотированы с помощью @Test .
В результате метод test1() выполнится, поскольку он аннотирован @Test , а test2() нет, так как он без аннотации @Test .
This is the first test
Заключение
Мы сделали обзор основных стандартных аннотаций и рассмотрели, как создавать и обрабатывать свои аннотации.
Возможностей по использованию аннотаций гораздо больше, чем мы рассмотрели. Например, можно автоматически генерировать код для паттерна Builder. Шаблон проектирования Builder (строитель) используется как альтернатива конструкторам, когда в конструкторы передается много параметров или есть необходимость в нескольких конструкторах с необязательными параметрами. При большом количестве таких классов возможность генерации кода обработчиком аннотаций сэкономит много времени и поможет избежать дублирования кода.
Примеры кода вы можете найти на GitHub.
Всех желающих приглашаем на Demo-занятие «Объектно-ориентированное и функциональное программирование». На вебинаре поговорим о стилях программирования и необходимости каждого из них. Разберём основные принципы объектно-ориентированного стиля (Инкапсуляция, Наследование, Полиморфизм), а также возможности функционального стиля, которые предоставляет язык Java. Регистрация для всех желающих по ссылке.
- java
- Аннотации в Java
- ООП
- функциональное программирование
- Блог компании OTUS
- Java
Java @Аннотации. Что это и как этим пользоваться?


Данная статья предназначена для людей, которые никогда не работали с Аннотациями, но хотели бы разобраться, что это и с чем его едят. Если же вы имеете опыт в данной сфере, не думаю, что эта статья как-то расширит ваши знания (да и, собственно, такую цель я не преследую). Также статья не подходит для тех, кто только начинает изучать язык Java. Если Вы не понимаете что такое Map<> или HashMap<> или не знаете что означает запись static < >внутри определения класса, либо же никогда не работали с рефлексией – Вам рано читать эту статью и пытаться понять, что такое аннотации. Сам по себе этот инструмент не создан для использования новичками, так как требует уже не совсем базовых пониманий взаимодействия классов и объектов (моё мнение) (спасибо комментариям за то, что показали необходимость этой приписки). Итак, приступим. Аннотации в Java являются своего рода метками в коде, описывающими метаданные для функции/класса/пакета. Например, всем известная Аннотация @Override, обозначающая, что мы собираемся переопределить метод родительского класса. Да, с одной стороны, можно и без неё, но если у родителей не окажется этого метода, существует вероятность, что мы зря писали код, т.к. конкретно этот метод может и не вызваться никогда, а с Аннотацией @Override компилятор нам скажет, что: "Я не нашел такого метода в родителях. что-то здесь нечисто". Однако Аннотации могут нести в себе не только смысл "для надежности": в них можно хранить какие-то данные, которые после будут использоваться.
Для начала рассмотрим простейшие аннотации предоставляемые стандартной библиотекой.
- Тип хранения (Retention);
- Тип объекта над которым она указывается (Target).
Тип хранения
- SOURCE - аннотация используется только при написании кода и игнорируется компилятором (т.е. не сохраняется после компиляции). Обычно используется для каких-либо препроцессоров (условно), либо указаний компилятору
- CLASS - аннотация сохраняется после компиляции, однако игнорируется JVM (т.е. не может быть использована во время выполнения). Обычно используется для каких-либо сторонних сервисов, подгружающих ваш код в качестве plug-in приложения
- RUNTIME - аннотация которая сохраняется после компиляции и подгружается JVM (т.е. может использоваться во время выполнения самой программы). Используется в качестве меток в коде, которые напрямую влияют на ход выполнения программы (пример будет рассмотрен в данной статье)
Тип объекта над которым указывается
- ANNOTATION_TYPE - другая аннотация
- CONSTRUCTOR - конструктор класса
- FIELD - поле класса
- LOCAL_VARIABLE - локальная переменная
- METHOD - метод класса
- PACKAGE - описание пакета package
- PARAMETER - параметр метода public void hello(@Annontation String param)<>
- TYPE - указывается над классом
@Override
Retention: SOURCE; Target: METHOD. Данная аннотация показывает, что метод над котором она прописана наследован у родительского класса. Первая аннотация с которой сталкивался каждый начинающий Java-программист, при использовании IDE, которая настойчиво пихает эти @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; Данная аннотация задает тип объекта над которым может указываться создаваемая нами аннотация. Да и она тоже используется для себя же, привыкайте. Думаю, на этом можно завершить ознакомление со стандартными аннотациями библиотеки Java, т.к. остальные используются достаточно редко и, хоть и несут свою пользу, сталкиваться с ними приходится не всем и совершенно необязательно. Если же вы хотите чтобы я рассказал о какой-то конкретной аннотации из стандартной библиотеки (либо, возможно, аннотации типа @NotNull и @Nullable которые в STL не входят) напишите в комментариях - либо вам там ответят добрые пользователи, либо я когда увижу. Если уж много людей будут просить какую-то аннотацию - также внесу её в статью.
Практическое применение RUNTIME аннотаций
Собственно, думаю, хватит теоретической болтавни: давайте перейдем к практике на примере бота. Допустим вы хотите написать бота для какой-то соцсети. У всех крупных сетей, таких как ВК, 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 < //получим массив ; String[] args = message.split(" "); //Для удобства уберем "бот" и отделим команду от аргументов String command = args[1]; String[] nArgs = Arrays.copyOfRange(args, 2, args.length); //Получили command = "(команда)"; nArgs = ; //Данный массив может быть пустым > catch (ArrayIndexOutOfBoundsException e) < //Вывод списка команд или какого-либо сообщения //В случае если просто написать "Бот" >> >
- Сделать if(command.equalsIngnoreCase(". "))
- Сделать switch(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 , Enum . Нельзя написать List
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 = ) 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 команды: привет, пока, помощь. Теперь давайте модифицируем наш MessageListener так, чтобы он как-то с этим работал. Для удобства и скорости работы, будем сразу хранить наши команды в HashMap :
public class MessageListner < //Map который хранит как ключ команду //А как значение функцию которая будет обрабатывать команду private static final MapCOMMANDS = 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 — иначе она просто не будет найдена; m.invoke(LISTENER, nArgs); > catch (ArrayIndexOutOfBoundsException e) < //Вывод списка команд или какого-либо сообщения //В случае если просто написать "Бот" >> > >
Вот собственно и всё, что нужно, чтобы наши команды работали. Теперь добавление новой команды — это не новый if, не новый case, в которых нужно было бы заново переучесть количество аргументов, также пришлось бы переписывать help, добавляя в него новые строки. Теперь же, чтобы добавить команду, нам нужно просто в классе CommandListener добавить новую функцию с аннотацией @Command и всё — команда добавлена, случаи учтены, help дополнен автоматически. Абсолютно бесспорно, что данную задачу можно решить множеством других путей. Да, всё что можно сделать при помощи аннотаций/рефлексий можно сделать и без них, вопрос лишь в удобстве, оптимальности и размерах кода, конечно же, совать Аннотацию везде где есть малейший намек на то, что получится её использовать - тоже не самый рациональный вариант, во всем нужно знать меру =). Но при написании API, Библиотек или программ, в которых возможно повторение однотипного (но не совсем одинакового) кода, аннотации - бесспорно оптимальное решение.
Для чего используются аннотации?
Удобно рассмотреть случаи применения аннотаций с точки зрения возможных значений их свойства RetentionPolicy :
SOURCE – аннотация присутствует только в исходном коде, но не вовлечена в компиляцию. Можно разделить их на две категории:
Первая – аннотации для программиста, а не для программы. Это всевозможные маркеры. Они добавляют аннотируемым элементам некоторую специальную семантику. Более формализованный вариант документации. Примеры – @Immutable и @ThreadSafe из Hibernate.
Вторая категория – инструкции для инструментов разработки. Примеры этой категории, @SuppressWarnings и @Override могут влиять на предупреждения и ошибки компиляции. IntelliJ IDEA умеет понимать @Nullable и @NonNull из Spring Framework, и предупреждать о возможных NullPointerException .
CLASS – самое экзотическое, но при том стандартное значение. Аннотация попадает в байткод .class-файла, но игнорируется загрузчиком классов. В результате такая аннотация недоступна для рефлекшна. Используется для сторонних инструментов, обрабатывающих байткод, например для обфускаторов.
RUNTIME – самое ходовое значение. Цель снабжается метаинформацией, доступной во время выполнения программы. Сама по себе аннотация всё так же не добавляет нового поведения. Для практической пользы runtime-аннотации в программе должен быть исполнен некоторый код процессинга, который прочитает метаинформацию инструментами Reflection API. Такой механизм широко используется во множестве популярных фреймворков: Spring, Hibernate, Jackson.