Аннотации в Java: @Override, встроенные аннотации и как написать свою

Аннотации в Java: @Override, встроенные аннотации и как написать свою Полезное

Аннотация в Java — это метаданные, прикрепленные к объявлению (классу, методу, полю, параметру, пакету и т. д.) в форме @Имя или @Имя(параметры). Сама аннотация ничего не выполняет: смысл ей придает инструмент, который ее читает, — компилятор, процессор аннотаций при сборке или код через рефлексию во время работы программы.

Ниже — как @Override ловит тихую ошибку, какие аннотации встроены в язык, что делают @Retention и @Target и как написать и прочитать свою аннотацию. Все примеры проверены на OpenJDK 25.0.4 (Java 25 LTS) 23.09.2026.

Аннотация, Javadoc-тег и комментарий — не одно и то же

Что Пример Кто читает Влияет на компиляцию
Аннотация @Override, @Deprecated компилятор, процессоры, рефлексия да, может дать ошибку или предупреждение
Javadoc-тег @author, @param, @deprecated внутри /** ... */ утилита javadoc нет
Комментарий // переопределяем только человек нет

@author и @param — не аннотации, а Javadoc-теги внутри комментария документации. Аннотация @Deprecated (с большой буквы) и тег @deprecated (с маленькой) — тоже разные сущности, их обычно ставят вместе.

@Override: минимальный пример ошибки, которую он ловит

Классическая ошибка: в equals написали параметр Point вместо Object. Это не переопределение, а перегрузка — новый метод рядом со старым.

public class Point {
    private final int x, y;

    public Point(int x, int y) { this.x = x; this.y = y; }

    // Ошибка: параметр Point, а не Object - это перегрузка, не переопределение
    public boolean equals(Point other) {
        return other != null && x == other.x && y == other.y;
    }

    public static void main(String[] args) {
        Object a = new Point(1, 2);
        Object b = new Point(1, 2);
        System.out.println(a.equals(b));
        System.out.println(java.util.Set.of(a).contains(b));
    }
}

Код компилируется без единого предупреждения, а java Point.java печатает:

false
false

Для переменных типа Object вызывается Object.equals(Object), сравнивающий ссылки, — наш метод не участвует. Теперь добавим @Override к тому же методу:

    @Override
    public boolean equals(Point other) {
        return other != null && x == other.x && y == other.y;
    }

Компилятор останавливает сборку:

Point.java:6: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

Исправление — правильная сигнатура и парный hashCode, чтобы объект корректно работал в HashSet и HashMap:

@Override
public boolean equals(Object o) {
    return o instanceof Point p && x == p.x && y == p.y;
}

@Override
public int hashCode() {
    return java.util.Objects.hash(x, y);
}

Тот же main теперь печатает true и true.

Что важно знать про @Override

  • Аннотация необязательна: она лишь просит компилятор проверить намерение.
  • Ставится только на методы. С Java 6 ее можно ставить и на реализацию метода интерфейса.
  • Статический метод переопределить нельзя, только скрыть. Если в родителе static void destroyEvil(), а в наследнике обычный void destroyEvil(), javac 25 выдаст cannot override с пояснением overridden method is static (и без @Override).

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

Аннотация Куда ставится Что делает компилятор
@Override метод ошибка, если метод ничего не переопределяет
@Deprecated почти любое объявление предупреждение при использовании
@SuppressWarnings класс, метод, поле, переменная и др. глушит указанные предупреждения в этой области
@FunctionalInterface интерфейс ошибка, если абстрактный метод не ровно один
@SafeVarargs конструктор, static/final/private метод с varargs глушит предупреждения о небезопасных generic varargs

@Deprecated и forRemoval

С Java 9 у @Deprecated есть параметры since и forRemoval:

public class Legacy {
    /** @deprecated используйте total(int...) */
    @Deprecated(since = "2.0", forRemoval = true)
    public static int sum(int a, int b) { return a + b; }
}

Вызов Legacy.sum(2, 3) из другого класса компилируется, но javac сразу показывает предупреждение:

App.java:3: warning: [removal] sum(int,int) in Legacy has been deprecated and marked for removal

Без forRemoval = true по умолчанию будет только короткая заметка компилятора Use.java uses or overrides a deprecated API, а подробности появятся с флагом -Xlint:deprecation. Сам метод работает как обычно.

@SuppressWarnings: глушить точечно

Если вы проверили, что небезопасное приведение корректно, предупреждение можно заглушить — в минимальной области, на одной переменной, а не на всем классе:

static List<String> fromCache(Object cached) {
    // В кеш кладем только List<String>, приведение проверено логикой выше
    @SuppressWarnings("unchecked")
    List<String> list = (List<String>) cached;
    return list;
}

Без аннотации javac -Xlint:unchecked показал бы warning: [unchecked] unchecked cast, с ней — тишина. Частые ключи: "unchecked", "deprecation", "removal", "rawtypes". Набор ключей помимо unchecked и deprecation зависит от компилятора и IDE, лишний ключ javac просто игнорирует.

@FunctionalInterface

Помечает интерфейс, который задуман под лямбды. Если случайно добавить второй абстрактный метод, javac 25 ответит:

Calc.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Calc is not a functional interface
    multiple non-overriding abstract methods found in interface Calc

Мета-аннотации: @Retention и @Target

Мета-аннотации ставятся на объявление другой аннотации. Две главные решают, где она живет и куда ее можно ставить.

RetentionPolicy Где сохраняется Видна через рефлексию
SOURCE только в исходнике, компилятор ее отбрасывает (так устроен @Override) нет
CLASS записывается в .class-файл; значение по умолчанию нет
RUNTIME в .class-файле и доступна JVM при выполнении да

@Target ограничивает место: ElementType.FIELD, METHOD, TYPE, PARAMETER и другие. Еще есть @Documented (попадет в Javadoc), @Inherited (аннотация класса наследуется подклассами) и @Repeatable (можно ставить несколько раз).

Своя аннотация и чтение через рефлексию

Аннотация @Range для числовых полей и маленький валидатор, файл Validator.java целиком:

import java.lang.annotation.*;
import java.lang.reflect.Field;
import java.util.*;

@Retention(RetentionPolicy.RUNTIME)   // сохранить до времени выполнения
@Target(ElementType.FIELD)             // разрешить только на полях
@interface Range {
    int min() default 0;
    int max();
    String message() default "значение вне диапазона";
}

class User {
    @Range(min = 14, max = 120, message = "возраст от 14 до 120")
    int age;

    @Range(max = 10)
    int rating;

    User(int age, int rating) { this.age = age; this.rating = rating; }
}

public class Validator {
    static List<String> validate(Object obj) throws IllegalAccessException {
        List<String> errors = new ArrayList<>();
        for (Field field : obj.getClass().getDeclaredFields()) {
            Range range = field.getAnnotation(Range.class);
            if (range == null) continue;          // поле без аннотации
            int value = field.getInt(obj);
            if (value < range.min() || value > range.max()) {
                errors.add(field.getName() + "=" + value + ": " + range.message());
            }
        }
        return errors;
    }

    public static void main(String[] args) throws IllegalAccessException {
        System.out.println(validate(new User(25, 7)));
        System.out.println(validate(new User(9, 11)));
    }
}

Запуск java Validator.java выводит:

[]
[age=9: возраст от 14 до 120, rating=11: значение вне диапазона]

Разбор по шагам:

  1. @interface Range объявляет тип аннотации. Ее параметры — методы без тела: max() обязателен, у остальных есть default.
  2. @Target(ElementType.FIELD) запрещает ставить @Range на метод — javac ответит annotation interface not applicable to this kind of declaration.
  3. field.getAnnotation(Range.class) возвращает аннотацию или null, если ее нет.
  4. Логика проверки — в валидаторе, аннотация только хранит правила.

Поля здесь без модификатора private, а классы в одном пакете, поэтому getInt работает напрямую. Для private-полей понадобится field.setAccessible(true), а в модульных приложениях еще и открытый для рефлексии пакет.

Частая ошибка: забыли RUNTIME

Уберем RUNTIME и напишем @Retention(RetentionPolicy.CLASS) (то же самое будет, если @Retention не писать вовсе). Код скомпилируется, но вывод станет таким:

[]
[]

Валидация молча перестала работать: getAnnotation возвращает null, аннотация не дожила до выполнения. Для чтения рефлексией нужен RetentionPolicy.RUNTIME.

Аннотации в тестах: JUnit 4 и JUnit Jupiter

Аннотации тестов не входят в язык — это классы библиотеки JUnit, их читает тестовый движок. Имена в двух линейках различаются:

Задача JUnit 4 JUnit Jupiter (JUnit 5 и новее)
Пометить тест @Test @Test (другой пакет: org.junit.jupiter.api)
Перед каждым тестом @Before @BeforeEach
Один раз перед всеми @BeforeClass (static) @BeforeAll (static по умолчанию)
После каждого / после всех @After / @AfterClass @AfterEach / @AfterAll
Отключить тест @Ignore @Disabled
Ожидаемое исключение @Test(expected = ...) assertThrows(...) в теле теста

Смешивать аннотации двух линеек в одном классе нельзя: движок Jupiter не увидит @Before из JUnit 4, и метод подготовки просто не вызовется.

Выводы

  • Аннотация — это метаданные: поведение появляется только у того, кто ее читает (компилятор, процессор, фреймворк).
  • @Override необязателен, но превращает тихую ошибку сигнатуры (перегрузку вместо переопределения) в ошибку компиляции.
  • Встроенные аннотации @Deprecated, @SuppressWarnings, @FunctionalInterface работают на этапе компиляции; @SuppressWarnings ставьте в минимальную область.
  • @Retention решает, доживет ли аннотация до выполнения; для чтения рефлексией нужен RUNTIME, иначе getAnnotation вернет null.
  • Javadoc-теги @author, @param, @deprecated — не аннотации, а разметка документации.

Где применяется / связь с практикой

На аннотациях построены Spring (@Component, @Autowired), JPA и Hibernate (@Entity, @Id), Bean Validation (@NotNull), JUnit, Lombok. Понимание @Retention, @Target и рефлексии объясняет, почему фреймворк «видит» одни аннотации и не видит другие, и помогает читать ошибки конфигурации.

Освойте тему на практике

Системно пройти основы языка, ООП и работу с библиотеками можно на курсе Java Developer. Basic. Чтобы посмотреть формат занятий, подойдут бесплатные открытые уроки Otus.

FAQ

Можно ли поставить @Override на поле или конструктор?
Нет, только на метод. На другом объявлении javac выдаст ошибку компиляции.

Замедляют ли аннотации программу?
Сами по себе нет, это данные в .class-файле. Затраты дает чтение рефлексией, поэтому фреймворки обычно читают аннотации при старте и кешируют результат.

Чем обработка аннотаций при сборке отличается от рефлексии?
Процессор аннотаций (javax.annotation.processing) работает внутри javac и может генерировать новый код, как Lombok или MapStruct. Рефлексия читает аннотации уже во время выполнения и требует RetentionPolicy.RUNTIME.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)