1. 概述
如今,很难想象 Java 没有注释这个 Java 语言中的强大工具。
Java提供了一组内置的注释。此外,还有来自不同库的大量注释。我们甚至可以定义和处理我们自己的注释。我们可以使用属性值调整这些注释,但是这些属性值有局限性。特别地, 注释属性值必须是常量表达式 。
在本教程中,我们将了解该限制的一些原因,并深入 JVM 的内部,以更好地解释它。我们还将看一些涉及注释属性值的问题和解决方案的示例。
2. 底层的 Java 注解属性
让我们考虑一下Java类文件如何存储注释属性。 Java 有一个特殊的结构,称为 element_value 。该结构存储特定的注释属性。
结构体 element_value 可以存储四种不同类型的值:
- 常量池中的常量
- 类文字
- 嵌套注释
- 值数组
因此,注释属性中的常量是编译时常量。否则,编译器将不知道应该将什么值放入常量池并用作注释属性。
Java 规范定义了生成常量表达式的操作。如果我们将这些操作应用于编译时常量,我们将得到编译时常量。
假设我们有一个注释 @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 不支持从接口继承注解 ,即使注解本身具有 @Inherited 注解。因此,实现标记接口的类不会继承注释。
造成这种情况的原因就是多重继承的问题 。确实,如果多个接口具有相同的注释,Java 无法选择其中之一。
因此,我们无法避免使用标记界面进行复制粘贴。
6. 数组元素作为注释属性
假设我们有一个数组常量,并且我们使用该常量作为注释属性:
@Marker(Example.ATTRIBUTES[0])
public class Example {
static final String[] ATTRIBUTES = {"Foo", "Bar"};
// ...
}
这段代码无法编译。注释参数必须是编译时常量。但是,正如我们之前考虑的, 数组不是编译时常量 。
此外, 数组访问表达式不是常量表达式 。
如果我们有一个 列表 而不是数组怎么办?方法调用不属于常量表达式。因此,使用 List 类的 get 方法会导致相同的错误。
相反,我们应该显式引用一个常量:
@Marker(Example.ATTRIBUTE_FOO)
public class Example {
static final String ATTRIBUTE_FOO = "Foo";
static final String[] ATTRIBUTES = {ATTRIBUTE_FOO, "Bar"};
// ...
}
这样,我们在字符串常量中指定注解属性值,Java编译器就可以明确地找到该属性值。
七、结论
在本文中,我们研究了注释参数的局限性。我们考虑了注释属性问题的一些示例。我们还在这些限制的背景下讨论了 JVM 内部结构。
在所有示例中,我们对常量和注释使用相同的类。然而,所有这些限制都适用于常量来自另一个类的情况。