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— использование устаревших APIunchecked— непроверенные операции с genericsrawtypes— использование raw-типов вместо genericsserial— отсутствие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, и т.д.) StringClassили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— для аннотаций, читаемых из байткода инструментами, но не нужных в runtimeRUNTIME— для аннотаций, обрабатываемых через рефлексию во время выполнения
@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() { }
Для повторяемой аннотации необходимо:
- Объявить контейнерную аннотацию с элементом
value(), возвращающим массив повторяемой аннотации - Пометить повторяемую аннотацию
@Repeatable, указав класс контейнера - Контейнер должен иметь такую же или более широкую политику
@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 и многих других полагаются на аннотации для конфигурации и определения поведения.