Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.x. annotations

annotations

Материалы

ТипСсылка
Документссылка
Видеоссылка

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

Аннотации используются для:

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

Синтаксис использования

В простейшей форме аннотация выглядит так:

@Override
void myMethod() { ... }

Символ @ указывает компилятору, что далее следует аннотация. Аннотация может включать элементы — именованные или неименованные параметры со значениями:

@Author(
    name = "Иван Петров",
    date = "2025-01-15"
)
class MyClass { ... }

Если аннотация не имеет элементов, скобки можно опустить, как в примере с @Override.

Если у аннотации есть только один элемент с именем value, имя можно опустить:

@SuppressWarnings("unchecked")
void myMethod() { ... }

Можно применить несколько аннотаций к одному объявлению:

@Author(name = "Иван Петров")
@Reviewer(name = "Мария Сидорова")
class MyClass { ... }

Начиная с Java 8, аннотации можно применять не только к объявлениям, но и к использованиям типов:

// Создание объекта
new @Interned MyObject();

// Приведение типа
myString = (@NonNull String) str;

// Реализация интерфейса
class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }

// Объявление исключений
void monitorTemperature() throws @Critical TemperatureException { ... }

Встроенные аннотации

Java предоставляет набор предопределённых аннотаций в пакетах java.lang и java.lang.annotation.

@Override

Аннотация @Override информирует компилятор, что метод предназначен для переопределения метода суперкласса:

class Animal {
    void makeSound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof!");
    }
}

Если метод, помеченный @Override, на самом деле не переопределяет метод суперкласса (например, из-за опечатки в имени), компилятор выдаст ошибку. Это защищает от неочевидных багов.

@Deprecated

Аннотация @Deprecated указывает, что помеченный элемент устарел и не должен использоваться:

/**
 * @deprecated Используйте {@link #newMethod()} вместо этого метода.
 */
@Deprecated
void oldMethod() { ... }

Компилятор выдаёт предупреждение при использовании устаревших элементов. Рекомендуется также использовать Javadoc-тег @deprecated для документирования альтернативы.

Начиная с Java 9, аннотация @Deprecated имеет два дополнительных элемента:

@Deprecated(since = "9", forRemoval = true)
void legacyMethod() { ... }
  • since — указывает версию, с которой элемент устарел
  • forRemoval — если true, элемент планируется к удалению в будущих версиях

@SuppressWarnings

Аннотация @SuppressWarnings указывает компилятору подавить определённые предупреждения:

@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
    object.deprecatedMethod();  // Предупреждение подавлено
}

Можно подавить несколько категорий предупреждений:

@SuppressWarnings({"unchecked", "deprecation"})
void myMethod() { ... }

Основные категории предупреждений:

  • deprecation — использование устаревших API
  • unchecked — непроверенные операции с generics
  • rawtypes — использование raw-типов вместо generics
  • serial — отсутствие serialVersionUID в сериализуемом классе

Совет: Применяйте @SuppressWarnings к минимально возможной области (метод, переменная), а не к целому классу. Не используйте @SuppressWarnings("all") — это скрывает важные предупреждения.

@SafeVarargs

Аннотация @SafeVarargs применяется к методам и конструкторам, подавляя предупреждения о потенциально небезопасных операциях с varargs-параметрами:

@SafeVarargs
static <T> List<T> asList(T... elements) {
    return Arrays.asList(elements);
}

Аннотацию можно применять только к методам, которые нельзя переопределить:

  • static методы
  • final методы
  • private методы (начиная с Java 9)
  • Конструкторы

@FunctionalInterface

Аннотация @FunctionalInterface указывает, что интерфейс предназначен для использования в качестве функционального интерфейса — интерфейса с единственным абстрактным методом:

@FunctionalInterface
interface Calculator {
    int calculate(int x, int y);
    
    // default-методы не считаются
    default void printResult(int result) {
        System.out.println("Result: " + result);
    }
}

Функциональные интерфейсы можно использовать с лямбда-выражениями:

Calculator addition = (x, y) -> x + y;
Calculator multiplication = (x, y) -> x * y;

System.out.println(addition.calculate(5, 3));       // 8
System.out.println(multiplication.calculate(5, 3)); // 15

Аннотация не обязательна для создания функционального интерфейса, но она защищает от случайного добавления второго абстрактного метода.

Объявление аннотаций

Для создания собственной аннотации используется ключевое слово @interface:

public @interface MyAnnotation {
    String author();
    String date();
    int revision() default 1;
    String[] reviewers() default {};
}

Объявление аннотации похоже на объявление интерфейса, но ключевому слову interface предшествует символ @. Технически аннотации — это особая форма интерфейса.

Элементы аннотаций

Методы, объявленные в теле аннотации, называются элементами. Они определяют параметры аннотации:

public @interface Author {
    String name();
    String date() default "N/A";
}

Ограничения для элементов:

  • Не могут иметь параметров
  • Не могут объявлять исключения (throws)
  • Возвращаемый тип ограничен:
    • Примитивные типы (int, boolean, и т.д.)
    • String
    • Class или Class<T>
    • Enum-типы
    • Другие аннотации
    • Массивы перечисленных выше типов
public @interface ComplexAnnotation {
    int count();
    String name();
    Class<?> targetClass();
    ElementType[] targets();
    Deprecated nested();        // Вложенная аннотация
    String[] tags() default {}; // Массив с default-значением
}

Виды аннотаций

По количеству элементов аннотации делятся на три вида:

Маркерные аннотации — без элементов:

public @interface Preliminary { }

// Использование
@Preliminary
public class UnfinishedClass { }

Одноэлементные аннотации — с одним элементом. По соглашению, элемент называется value:

public @interface Copyright {
    String value();
}

// Использование — имя "value" можно опустить
@Copyright("2025 My Company")
public class MyClass { }

Многоэлементные аннотации — с несколькими элементами:

@Author(name = "Иван", date = "2025-01-15")
public class MyClass { }

Мета-аннотации

Мета-аннотации — это аннотации, применяемые к другим аннотациям. Они определяют, как пользовательская аннотация будет себя вести.

@Retention

Определяет, как долго аннотация будет сохраняться:

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation { }

Возможные значения RetentionPolicy:

ПолитикаОписание
SOURCEАннотация сохраняется только в исходном коде, отбрасывается компилятором
CLASSСохраняется в .class-файле, но недоступна во время выполнения (по умолчанию)
RUNTIMEСохраняется в .class-файле и доступна через рефлексию во время выполнения

Выбирайте минимально необходимый уровень:

  • SOURCE — для аннотаций, обрабатываемых только компилятором или процессорами аннотаций
  • CLASS — для аннотаций, читаемых из байткода инструментами, но не нужных в runtime
  • RUNTIME — для аннотаций, обрабатываемых через рефлексию во время выполнения

@Target

Определяет, к каким элементам программы можно применять аннотацию:

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface Transactional { }

Возможные значения ElementType:

ТипПрименяется к
TYPEКлассы, интерфейсы, enum, record
FIELDПоля (включая константы enum)
METHODМетоды
PARAMETERПараметры методов
CONSTRUCTORКонструкторы
LOCAL_VARIABLEЛокальные переменные
ANNOTATION_TYPEДругие аннотации
PACKAGEПакеты (в package-info.java)
TYPE_PARAMETERПараметры типов (class MyClass<@Ann T>)
TYPE_USEЛюбое использование типа
MODULEМодули (в module-info.java)
RECORD_COMPONENTКомпоненты record

Если @Target не указан, аннотация может применяться к любому объявлению (кроме параметров типов).

@Documented

Указывает, что аннотация должна быть включена в Javadoc-документацию:

@Documented
@Target(ElementType.METHOD)
public @interface Beta { }

По умолчанию аннотации не отображаются в документации. Используйте @Documented для важных публичных аннотаций.

@Inherited

Указывает, что аннотация наследуется подклассами:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Secured { }

@Secured
public class BaseService { }

// UserService также считается @Secured
public class UserService extends BaseService { }

Без @Inherited подклассы не наследуют аннотацию суперкласса. Важно: @Inherited работает только для классов, не для интерфейсов и методов.

@Repeatable

Позволяет применять одну и ту же аннотацию несколько раз к одному элементу (начиная с Java 8):

// Контейнерная аннотация
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
    Schedule[] value();
}

// Повторяемая аннотация
@Repeatable(Schedules.class)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedule {
    String dayOfWeek();
    String time();
}

// Использование
@Schedule(dayOfWeek = "Monday", time = "09:00")
@Schedule(dayOfWeek = "Friday", time = "15:00")
void weeklyMeeting() { }

Для повторяемой аннотации необходимо:

  1. Объявить контейнерную аннотацию с элементом value(), возвращающим массив повторяемой аннотации
  2. Пометить повторяемую аннотацию @Repeatable, указав класс контейнера
  3. Контейнер должен иметь такую же или более широкую политику @Retention

Type Annotations

Начиная с Java 8, аннотации можно применять везде, где используется тип. Для этого аннотация должна иметь @Target(ElementType.TYPE_USE):

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface NonNull { }

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

// Создание объекта
@NonNull String name = new @Interned String("hello");

// Приведение типа
String str = (@NonNull String) value;

// Реализация интерфейса
class MyList implements @Readonly List<@NonNull String> { }

// Параметры типов
Map<@NonNull String, @NonNull Integer> map = new HashMap<>();

// Границы типов
class Folder<F extends @Existing File> { }

// Исключения
void process() throws @Critical IOException { }

// Массивы
@NonNull String @Nullable [] array;  // Nullable массив NonNull строк

Type annotations предназначены для использования с инструментами статического анализа кода (например, Checker Framework), которые могут проверять дополнительные ограничения типов во время компиляции.

Аннотация с TYPE_PARAMETER применяется к объявлениям параметров типов:

class MyClass<@TypeParam T> { }

Чтение аннотаций через рефлексию

Аннотации с RetentionPolicy.RUNTIME доступны во время выполнения через Reflection API:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
    String description() default "";
}
public class MyTestClass {
    @Test(description = "Проверка сложения")
    public void testAddition() { }
    
    @Test
    public void testSubtraction() { }
    
    public void regularMethod() { }
}

Получение аннотаций:

import java.lang.reflect.Method;

public class AnnotationProcessor {
    public static void main(String[] args) {
        Class<?> clazz = MyTestClass.class;
        
        for (Method method : clazz.getDeclaredMethods()) {
            // Проверка наличия аннотации
            if (method.isAnnotationPresent(Test.class)) {
                // Получение аннотации
                Test test = method.getAnnotation(Test.class);
                
                System.out.println("Найден тестовый метод: " + method.getName());
                System.out.println("Описание: " + test.description());
            }
        }
    }
}

Основные методы для работы с аннотациями:

МетодОписание
isAnnotationPresent(Class)Проверяет наличие аннотации
getAnnotation(Class)Возвращает аннотацию или null
getAnnotations()Возвращает все аннотации
getDeclaredAnnotations()Возвращает только явно объявленные аннотации (без унаследованных)
getAnnotationsByType(Class)Возвращает все экземпляры повторяемой аннотации

Для type annotations используйте методы getAnnotatedReturnType(), getAnnotatedParameterTypes() и другие в классе Method:

Method method = MyClass.class.getMethod("myMethod");
AnnotatedType returnType = method.getAnnotatedReturnType();
Annotation[] annotations = returnType.getAnnotations();

Пример: создание простого тестового фреймворка

Рассмотрим практический пример создания минимального тестового фреймворка:

import java.lang.annotation.*;

// Аннотация для пометки тестовых методов
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SimpleTest {
    String name() default "";
    boolean enabled() default true;
}

// Аннотация для ожидаемых исключений
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExpectedException {
    Class<? extends Throwable> value();
}
public class Calculator {
    public int divide(int a, int b) {
        return a / b;
    }
}
public class CalculatorTest {
    private Calculator calc = new Calculator();
    
    @SimpleTest(name = "Деление положительных чисел")
    public void testDivision() {
        assert calc.divide(10, 2) == 5 : "10 / 2 должно быть 5";
    }
    
    @SimpleTest(name = "Деление на ноль")
    @ExpectedException(ArithmeticException.class)
    public void testDivisionByZero() {
        calc.divide(10, 0);
    }
    
    @SimpleTest(enabled = false)
    public void skippedTest() {
        // Этот тест не будет запущен
    }
}
import java.lang.reflect.Method;

public class TestRunner {
    public static void main(String[] args) throws Exception {
        Class<?> testClass = CalculatorTest.class;
        Object testInstance = testClass.getDeclaredConstructor().newInstance();
        
        int passed = 0, failed = 0, skipped = 0;
        
        for (Method method : testClass.getDeclaredMethods()) {
            if (!method.isAnnotationPresent(SimpleTest.class)) {
                continue;
            }
            
            SimpleTest test = method.getAnnotation(SimpleTest.class);
            String testName = test.name().isEmpty() ? method.getName() : test.name();
            
            if (!test.enabled()) {
                System.out.println("⏭ Пропущен: " + testName);
                skipped++;
                continue;
            }
            
            try {
                method.invoke(testInstance);
                
                // Если ожидалось исключение, но его не было — тест провален
                if (method.isAnnotationPresent(ExpectedException.class)) {
                    System.out.println("✗ Провален: " + testName + " (ожидалось исключение)");
                    failed++;
                } else {
                    System.out.println("✓ Пройден: " + testName);
                    passed++;
                }
            } catch (Exception e) {
                Throwable cause = e.getCause();
                ExpectedException expected = method.getAnnotation(ExpectedException.class);
                
                if (expected != null && expected.value().isInstance(cause)) {
                    System.out.println("✓ Пройден: " + testName);
                    passed++;
                } else {
                    System.out.println("✗ Провален: " + testName + " (" + cause + ")");
                    failed++;
                }
            }
        }
        
        System.out.println("\nРезультаты: " + passed + " пройдено, " 
                          + failed + " провалено, " + skipped + " пропущено");
    }
}

Вывод программы:

✓ Пройден: Деление положительных чисел
✓ Пройден: Деление на ноль
⏭ Пропущен: skippedTest

Результаты: 2 пройдено, 0 провалено, 1 пропущено

Резюме

Аннотации — мощный инструмент Java для добавления метаданных в код:

  • Используйте встроенные аннотации (@Override, @Deprecated, @SuppressWarnings, @FunctionalInterface) для улучшения качества кода
  • Создавайте собственные аннотации с помощью @interface для специфичных задач
  • Контролируйте поведение аннотаций через мета-аннотации (@Retention, @Target, @Documented, @Inherited, @Repeatable)
  • Используйте type annotations для статического анализа и расширенной проверки типов
  • Читайте аннотации через Reflection API во время выполнения

Аннотации широко используются в современных Java-фреймворках: Spring, Hibernate, JPA, JUnit и многих других полагаются на аннотации для конфигурации и определения поведения.