Перейти к основному содержимому

Java Annotation Attribute Value Restrictions

· 5 мин. чтения

1. Overview

These days, it's hard to imagine Java without annotations, a powerful tool in the Java language.

Java provides a set of built-in annotations . Additionally, there are plenty of annotations from different libraries. We can even define and process our own annotations. We can tune these annotations with attribute values, however, these attribute values have limitations. Particularly, an annotation attribute value must be a constant expression .

In this tutorial, we're going to learn some reasons for that limitation and look under the hood of the JVM to explain it better. We'll also take a look at some examples of problems and solutions involving annotation attribute values.

2. Java Annotation Attributes Under the Hood

Let's consider how Java class files store annotation attributes. Java has a special structure for it called element_value . This structure stores a particular annotation attribute.

The structure element_value can store values of four different types:

  • a constant from the pool of constants
  • a class literal
  • a nested annotation
  • an array of values

So, a constant from an annotation attribute is a compile-time constant . Otherwise, the compiler wouldn't know what value it should put into the constant pool and use as an annotation attribute.

The Java specification defines operations producing constant expressions . If we apply these operations to compile-time constants, we'll get compile-time constants.

Предположим, у нас есть аннотация @Marker со значением атрибута :

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Marker {
String value();
}

Например, этот код компилируется без ошибок:

@Marker(Example.ATTRIBUTE_FOO + Example.ATTRIBUTE_BAR)
public class Example {
static final String ATTRIBUTE_FOO = "foo";
static final String ATTRIBUTE_BAR = "bar";

// ...
}

Здесь мы определяем атрибут аннотации как объединение двух строк. Оператор конкатенации создает константное выражение.

3. Использование статического инициализатора

Рассмотрим константу, инициализированную в статическом блоке:

@Marker(Example.ATTRIBUTE_FOO)
public class Example {
static final String[] ATTRIBUTES = {"foo", "Bar"};
static final String ATTRIBUTE_FOO;

static {
ATTRIBUTE_FOO = ATTRIBUTES[0];
}

// ...
}

Он инициализирует поле в статическом блоке и пытается использовать это поле в качестве атрибута аннотации. Такой подход приводит к ошибке компиляции.

Во-первых, переменная ATTRIBUTE_FOO имеет модификаторы static и final , но компилятор не может вычислить это поле. Приложение вычисляет его во время выполнения.

Во- вторых, атрибуты аннотации должны иметь точное значение, прежде чем JVM загрузит класс . Однако при запуске статического инициализатора класс уже загружен. Таким образом, это ограничение имеет смысл.

Та же ошибка появляется при инициализации поля. Этот код неверен по той же причине:

@Marker(Example.ATTRIBUTE_FOO)
public class Example {
static final String[] ATTRIBUTES = {"foo", "Bar"};
static final String ATTRIBUTE_FOO = ATTRIBUTES[0];

// ...
}

Как JVM инициализирует ATTRIBUTE_FOO ? Оператор доступа к массиву ATTRIBUTES[0] выполняется в инициализаторе класса. Итак, ATTRIBUTE_FOO — это константа времени выполнения. Он не определен во время компиляции.

4. Константа массива как атрибут аннотации

Рассмотрим атрибут аннотации массива:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Marker {
String[] value();
}

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

@Marker(value = Example.ATTRIBUTES)
public class Example {
static final String[] ATTRIBUTES = {"foo", "bar"};

// ...
}

Во-первых, хотя модификатор final защищает ссылку от изменения, мы все равно можем изменять элементы массива .

Во- вторых, литералы массива не могут быть константами времени выполнения. JVM устанавливает каждый элемент в статическом инициализаторе — ограничение, которое мы описали ранее.

Наконец, файл класса хранит значения каждого элемента этого массива. Итак, компилятор вычисляет каждый элемент массива атрибутов, и это происходит во время компиляции.

Таким образом, мы можем каждый раз указывать только атрибут массива:

@Marker(value = {"foo", "bar"})
public class Example {
// ...
}

Мы по-прежнему можем использовать константу в качестве примитивного элемента атрибута массива.

5. Аннотации в интерфейсе маркера: почему это не работает?

Итак, если атрибут аннотации является массивом, мы должны повторять его каждый раз. Но хотелось бы избежать этого копипаста. Почему бы нам не сделать нашу аннотацию @Inherited ? Мы могли бы добавить нашу аннотацию к интерфейсу маркера :

@Marker(value = {"foo", "bar"})
public interface MarkerInterface {
}

Затем мы могли бы реализовать классы, которым требуется эта аннотация:

public class Example implements MarkerInterface {
// ...
}

Этот подход не сработает . Код скомпилируется без ошибок. Однако Java не поддерживает наследование аннотаций от interfaces , даже если аннотации сами содержат аннотацию @Inherited . Таким образом, класс, реализующий интерфейс маркера, не будет наследовать аннотацию.

Причиной этого является проблема множественного наследования . Действительно, если несколько интерфейсов имеют одну и ту же аннотацию, Java не может выбрать один.

Таким образом, мы не можем избежать этого копипаста с маркерным интерфейсом.

6. Элемент массива как атрибут аннотации

Предположим, у нас есть константа массива, и мы используем эту константу в качестве атрибута аннотации:

@Marker(Example.ATTRIBUTES[0])
public class Example {
static final String[] ATTRIBUTES = {"Foo", "Bar"};
// ...
}

Этот код не будет компилироваться. Параметры аннотации должны быть константой времени компиляции. Но, как мы уже говорили, массив не является константой времени компиляции .

Более того, выражение доступа к массиву не является константным выражением .

Что, если бы у нас был список вместо массива? Вызовы методов не относятся к константным выражениям. Таким образом, использование метода get класса List приводит к той же ошибке.

Вместо этого мы должны явно ссылаться на константу:

@Marker(Example.ATTRIBUTE_FOO)
public class Example {
static final String ATTRIBUTE_FOO = "Foo";
static final String[] ATTRIBUTES = {ATTRIBUTE_FOO, "Bar"};
// ...
}

Таким образом, мы указываем значение атрибута аннотации в строковой константе, и компилятор Java может однозначно найти значение атрибута.

7. Заключение

В этой статье мы рассмотрели ограничения параметров аннотации. Мы рассмотрели несколько примеров задач с атрибутами аннотаций. Мы также обсудили внутреннее устройство JVM в контексте этих ограничений.

Во всех примерах мы использовали одни и те же классы для констант и аннотаций. Однако все эти ограничения сохраняются для случаев, когда константа происходит из другого класса.