Аннотации в 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<Integer, String> testMap = new HashMap<>();
Если подобного легаси кода много, то вы вряд ли захотите вносить изменения, поскольку это влечет за собой много регрессионного тестирования. В этом случае к классу можно добавить аннотацию @SuppressWarning
, чтобы логи не загромождались избыточными предупреждениями.
@SuppressWarnings({"rawtypes", "unchecked"})
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 {
void printString(String testString);
}
Если в интерфейс 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<String>... testStringLists) {
for (List<String> testStringList : 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("*******");
List<String> testStringList1 = Arrays.asList("One", "Two");
List<String> 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<String>
private void printStringSafeVarargs(List<String>... testStringLists) {
^
./com/reflectoring/SafeVarargsTest.java:52: warning: [unchecked] unchecked generic array creation for varargs parameter of type List<String>[]
test.printStringSafeVarargs(testStringList1, testStringList2);
^
2 warnings
Добавив аннотацию @SafeVarargs
, мы можем избавиться от этого предупреждения:
@SafeVarargs
private void printStringSafeVarargs(List<String>... 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{
String name() default "ABC";
String city() default "XYZ";
}
Создадим класс 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{
String name() default "ABC";
String city() default "XYZ";
}
@Repeatable
@Repeatable
позволяет использовать аннотацию несколько раз на одном методе, классе или поле. Для использования @Repeatable
— аннотации необходимо создать аннотацию-контейнер, которая хранит значение в виде массива исходных аннотаций:}
@Target(ElementType.TYPE)
@Repeatable(RepeatableCompanies.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatableCompany {
String name() default "Name_1";
String city() default "City_1";
}
@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 {
String value() default "ABC";
}
Создайте класс, использующий аннотацию:
@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{
String name() default "ABC";
String city() default "XYZ";
}
Давайте создадим класс 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 {
Class<AnnotatedMethods> annotatedMethodsClass = 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. Регистрация для всех желающих по ссылке.