Аннотация в Java — это метаданные, прикрепленные к объявлению (классу, методу, полю, параметру, пакету и т. д.) в форме @Имя или @Имя(параметры). Сама аннотация ничего не выполняет: смысл ей придает инструмент, который ее читает, — компилятор, процессор аннотаций при сборке или код через рефлексию во время работы программы.
Содержание
- Аннотация, Javadoc-тег и комментарий — не одно и то же
- @Override: минимальный пример ошибки, которую он ловит
- Встроенные аннотации Java
- Мета-аннотации: @Retention и @Target
- Своя аннотация и чтение через рефлексию
- Аннотации в тестах: JUnit 4 и JUnit Jupiter
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — как @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: значение вне диапазона]
Разбор по шагам:
@interface Rangeобъявляет тип аннотации. Ее параметры — методы без тела:max()обязателен, у остальных естьdefault.@Target(ElementType.FIELD)запрещает ставить@Rangeна метод — javac ответитannotation interface not applicable to this kind of declaration.field.getAnnotation(Range.class)возвращает аннотацию илиnull, если ее нет.- Логика проверки — в валидаторе, аннотация только хранит правила.
Поля здесь без модификатора 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.



