如果可重复注解不重复了

嗨我亲爱的朋友们,好久不见。

000E347B836CD5A72D57AEE88C706E35.jpg

今天让我来 随便找个主题水一水 聊聊这个大家既熟悉又陌生的主题:可重复注解。

可重复注解

对于注解的定义与可重复注解的定义我在这里就不再过多的赘述了,网络上很容易就能搜索到相关内容。

其实有关于可重复注解的内容并不多,网络上大多数的帖子对它的评价基本上就是 一个Java8对于注解的语法糖。实际上这种说法并没有错,以为它确实是在生成代码的时候起到了一个语法糖的作用。让我们来看一个简单的例子:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Elements {
    Element[] value();
}

@Repeatable(Elements.class)
@interface Element {
    int value();
}

@Element(1)
@Element(2)
public class Test {
}
复制代码

对于上面的这两个注解定义,你肯定认为,最后的class Test中的代码编译完成后肯定是这样的:

@Elements({
        @Element(1),
        @Element(2)
})
public class Test {}

复制代码

让我们通过IDEA的字节码工具查看一下:

// class version 52.0 (52)
// access flags 0x21
public class test/JustTest1 {

  // compiled from: JustTest1.java

  @Ltest/Elements;(value={@Ltest/Element;(value=1), @Ltest/Element;(value=2)})

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 8 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Ltest/JustTest1; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
    
    // main省略...
}
复制代码

虽然我并不怎么擅长看字节码,但是我看到了这么一行:

@Ltest/Elements;(value={@Ltest/Element;(value=1), @Ltest/Element;(value=2)})
复制代码

大概猜也能猜出来,这就是说明两个 @Element 被编译为了一个 @Elements。 让我们再从代码里验证一下:

@Element(1)
@Element(2)
public class JustTest1 {
    public static void main(String[] args) {
        final Elements elements = JustTest1.class.getAnnotation(Elements.class);
        System.out.println(elements);
        System.out.println();
        for (Element element : elements.value()) {
            System.out.println(element);
        }
    }
}
复制代码

得到的控制台输入:

@test.Elements(value=[@test.Element(value=1), @test.Element(value=2)])

@test.Element(value=1)
@test.Element(value=2)
复制代码

很明显,这验证了@Repeatable就是语法糖的说法。那么感谢大家的阅读,本篇文章到此结束了。

...

...

DFB255C44F6307A0E521898117A4558C.gif

好吧,开个玩笑,我们继续。

不重复的可重复注解

OK,基于上述的代码,我们只进行一个小小的修改:

@Element(1)
public class JustTest1 {
    public static void main(String[] args) {
        final Elements elements = JustTest1.class.getAnnotation(Elements.class);
        System.out.println(elements);
        System.out.println();
        for (Element element : elements.value()) {
            System.out.println(element);
        }
    }
}
复制代码

可以看到,我们删掉了一个可重复注解,只保留了一个。那么你可以猜猜看,这次控制台会输出什么呢?是只会输出一个值吗?

让我们来运行一下:

null

java.lang.NullPointerException
	at test.JustTest1.main(JustTest1.java:12)

复制代码

92D0085C18B41DFBC762BECEB6A2AC88.gif

结果是出人意料的,至少当我遇到这个问题的时候,我是蒙圈的。我们可以看到,当可重复注解不再重复的时候,我们便不能直接获取容器注解了。

oh!怎么会这样,这难道不只是一个语法糖吗?让我们来看看字节码是什么样的:

public class test/JustTest1 {

  // compiled from: JustTest1.java

  @Ltest/Element;(value=1) // invisible

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Ltest/JustTest1; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

    // main省略...
}
复制代码

可以看到这么一行:

@Ltest/Element;(value=1) // invisible
复制代码

很明显,这次就没有@Elements的存在了,而且字节码工具还很贴心的标记了一个 // invisible, 因为我没有为 @Element 定义 @Retention(...), 因此它的默认保留级别是只在字节码的 RetentionPolicy.CLASS.

很明显,当你仅使用了一个可重复注解中的元素注解的时候,它会直接把这个注解放上去,而不是根据容器注解进行简单的“编译魔法”后再放上去了。

0B11628B7C35FEF3CDF140FC7F6E60CA.jpg

获取可重复注解

那么新的问题出现了,我应该如何获取任意数量的可重复注解呢?

E944EBED8B226B88907F19E5529EC666.gif

首先,让我们把元素注解的保留级别修改为 RUNTIME:

@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Elements.class)
@interface Element {
    int value();
}
复制代码

然后,这次我们不再使用 getAnnotaiton, 而是使用 getAnnotationsByType

@Element(1)
public class JustTest1 {
    public static void main(String[] args) {
        final Element[] elements = JustTest1.class.getAnnotationsByType(Element.class);
        System.out.println(elements.length);
        System.out.println();
        for (Element element : elements) {
            System.out.println(element);
        }
    }
}
复制代码

这一次的控制台输出:

1

@test.Element(value=1)
复制代码

好耶! D6615CBF8C6E61D32F82628CBF0557E3.jpg

由此可见,假若你想要获取任意数量的可重复注解元素,你应当使用 getAnnotationsByType 而不是 getAnnotation, 因为可重复注解并不是任何情况下都会变成可重复的。

但是很显然,这样做是有缺点的,就是假若你的容器注解(比如@Elements)上存在一些其他有默认值的属性,你可能需要通过代码进行判断,而不能直接获取它了。

当然,你可能在想,“假如我的容器注解有一些没有默认值的属性怎么办?”这一点实际上JVM已经替你想好了。在使用IDEA的时候,假如你为容器注解增加了其他没有默认值的属性,就像这样:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Elements {
    Element[] value();
    String name();
}
复制代码

实际上,这样是不会编译成功的,你会在 @Element@Repeatable 中得到警告:

image.png

Container annotation 'test.Elements' does not have a default value for 'name'

简单来说就是,JVM在告诉你:“嘿,你想要的容器 ‘test.Elements’ 中有一个叫做 ‘name’ 的属性没有默认值,所以它不能作为容器 ”

奇妙组合

这时候,让我们来看看下面这段代码:

@Element(1)
@Elements(@Element(2))
public class JustTest1 {
    
        public static void main(String[] args) {
        final Element[] elements = JustTest1.class.getAnnotationsByType(Element.class);
        for (Element element : elements) {
            System.out.println(element);
        }
    }
    
}
复制代码

动动你聪明的小脑筋,这段代码的执行结果是什么?如果你已经有了答案,再来看看下面这段:

@Element(1)
@Element(2)
@Elements(@Element(3))
public class JustTest1 {
    public static void main(String[] args) {
        final Element[] elements = JustTest1.class.getAnnotationsByType(Element.class);
        for (Element element : elements) {
            System.out.println(element);
        }
    }
}
复制代码

这段的结果又是如何呢?

...
...

OK,首先第一段代码的结果:

@test.Element(value=1)
@test.Element(value=2)
复制代码

而第二段则会直接出现编译错误:

image.png

Container annotation 'test.Elements' must not be present at the same time as the element it contains

直译的话就是:

容器注解'test.Elements'不能与它包含的元素同时出现

实际上就是因为上面的两个@Element出现了重复,同样也会编译产生 @Elements, 而这样就会对与手写的 @Elements 产生冲突。

怎么样,注解,很神奇吧?

注解很神奇吧.jpg

JLS

其实对于可重复注解需要使用 getAnnotationsByType 这件事,很多帖子都提到了,只不过很少有人会说为什么,也很少有人会告诉你如果直接用 getAnnotation 会踩到什么坑。(当然也有可能只有我才会笨笨到踩坑吧)

FFB9EEC744E8EEEAD4429FD7C68EAFDB.jpg

不过这时候,就会有所思考:为什么Java不直接把Repeatable作为彻底的语法糖使用, 而是要分开单独元素标记的时候和多个元素标记呢?

其实简单思考一下,大概也能猜到其中一二。从我个人理解来看,对于注解,注解标记在一个 AnnotatedElement 元素上的时候,一种类型的注解有且只能有一个注解在上面,所有只有当你的可重复注解元素大于1的时候,JVM才会帮你把它变成一个容器中的子元素。

但是官方是怎么解释于定义的呢?Repeatable 源代码的文档注释如下:

/**
 * The annotation type {@code java.lang.annotation.Repeatable} is
 * used to indicate that the annotation type whose declaration it
 * (meta-)annotates is <em>repeatable</em>. The value of
 * {@code @Repeatable} indicates the <em>containing annotation
 * type</em> for the repeatable annotation type.
 *
 * @since 1.8
 * @jls 9.6 Annotation Types
 * @jls 9.7 Annotations
 */
复制代码

暂且不管原文内容,我们可以注意到下面的两个标志:

@jls 9.6 Annotation Types
@jls 9.7 Annotations
复制代码

如果我没有猜错的话,jls 应该是 Java® Language Specification 的缩写,也就是 "Java语言规范"。


⚠️注意!下文枯燥警告!⚠️

8C89E09CC8AEF89BE2261C945EBD230D.jpg


秉承着求知的态度,我带着我小学水平的英语能力点开了 JLSJLS 9.6 Annotation TypesJLS 9.7 Annotations,准备看看有没有这方面的相关说明。

首先我们能够最先找到的与可重复注解相关的说明是 JLS 9.6.3. Repeatable Annotation Types, 在这里面,我们可以看到针对于可重复注解长达2000多个字母的详细定义。 在 9.6.3 中提到了很多可重复的元素注解(T) 和它的容器注解 (TC) 之间的各种关联关系,例如当 TC@Target 为什么的时候,T@Target 将会被约束为什么之类的。其中也包括这样的描述:

  1. T is applicable to at least the same kinds of program element as TC (§9.6.4.1). Specifically, if the kinds of program element where T is applicable are denoted by the set m1, and the kinds of program element where TC is applicable are denoted by the set m2, then each kind in m2 must occur in m1, except that:
  • ..... ..... .....
  • ..... ..... .....
  • ..... ..... .....

This clause implements the policy that an annotation type may be repeatable on only some of the kinds of program element where it is applicable.

简单来说,元素类型 T 并不一定能够在所有的目标上都是可重复的, T 可以存在于一些容器类型 TC 所不支持的目标上。


接下来一个长篇的详细描述是 JLS 9.7.5. Multiple Annotations of the Same Type 顾名思义,这一篇的内容主要是在讲在 “同一类型的多个注解”。

首先,开篇的一句话:

It is a compile-time error if multiple annotations of the same type T appear in a declaration context or type context, unless T is repeatable (§9.6.3) and both T and the containing annotation type of T are applicable in the declaration context or type context (§9.6.4.1).

是再说如果出现了相同的注解,除非它符合前文一些规范,是一个可重复注解,否则就要编译报错。

让我们继续往下看:

If a declaration context or type context has multiple annotations of a repeatable annotation type T, then it is as if the context has no explicitly declared annotations of type T and one implicitly declared annotation of the containing annotation type of T.

这段说的大概是,如果在同一个地方标注了多个可重复注解的元素注解 T ,那么,这个地方实际上就没有 T, 取而代之的是一个隐形的容器注解 TC, 并将所有的 T 包含在这个 TC 中.

从这里我们大概就已经能得到结果了:只有标记了多个可重复的 T, 才会由 T 转而诞生容器 TC, 也就是T转生了。

但是无妨,我们接着往下看:

The implicitly declared annotation is called the container annotation, and the multiple annotations of type T which appeared in the context are called the base annotations. The elements of the (array-typed) value element of the container annotation are all the base annotations in the left-to-right order in which they appeared in the context.

这一段提到了 TTC 的称呼以及转化的一些规则。作为可重复元素的 T 被称为 base annotations, 也就是 根注解 / 基注解 / 基本注解 / 基础注解 之类的。 OH SHIT,看来我之前一直叫他 “元素注解”,是叫错名字了。

作为根注解 T 的容器的 TC 则被称为 container annotation, 也就是 容器注解看来这个我没叫错。

这里提到了容器注解 TC 的value元素的顺序是按照源代码中标记顺序的由左向右顺序排列的。

OK, 再往下一段:

It is a compile-time error if, in a declaration context or type context, there are multiple annotations of a repeatable annotation type T and any annotations of the containing annotation type of T.

这一段提到,如果 TC 和 多个 T 同时存在,那么编译报错。是的,这说的就是上面 奇妙组合 里我们的第二段代码。

至于为什么不允许这种写法,下文也给出了解释:

If this code was legal, then multiple levels of containment would be needed: first the annotations of type Foo would be contained by an implicitly declared container annotation of type FooContainer, then that annotation and the explicitly declared annotation of type FooContainer would be contained in yet another implicitly declared annotation. This complexity is undesirable in the judgment of the designers of the Java programming language. Another approach, treating the annotations of type Foo as if they had occurred alongside @Foo(2) in the explicit @FooContainer annotation, is undesirable because it could change how reflective programs interpret the @FooContainer annotation.

这一长篇配套的示例代码是:

@Foo(0) @Foo(1) @FooContainer({@Foo(2)})
class A {}
复制代码

这段解释中说,假若要使上述代码合法化,那么就说明,@Foo(0)@Foo(1) 需要一个隐式的 @FooContainer 来组合他们,而这时候又存在了多个 @FooContainer, 因此需要为 @FooContainer 也准备一个更外层的容器来容纳多个 @FooContainer...

"根据Java编程语言设计者的判断,这种复杂性是不可取的", 继而便不允许这种操作。

那么有没有对 奇妙组合 里第一段的描述呢?是有的。我们接着往下看。接下来的内容中包含着两段代码示例:

This rule is designed to allow the following code:

@Foo(1) @FooContainer({@Foo(2)})
class A {}
复制代码

With only one annotation of the repeatable annotation type Foo, no container annotation is implicitly declared, even if FooContainer is the containing annotation type of Foo. However, repeating the annotation of type FooContainer, as in:

@Foo(1) @FooContainer({@Foo(2)}) @FooContainer({@Foo(3)})
class A {}
复制代码

is prohibited, even if FooContainer is repeatable with a containing annotation type of its own. It is obtuse to repeat annotations which are themselves containers when an annotation of the underlying repeatable type is present.

上面是在讲假如你显示标记了一个根注解 @Foo一个容器注解 @FooContainer 的行为是被允许的,但是标注多个容器注解的行为是不被允许的。

收尾

呼~累死了,这篇文章今天就到这儿吧。也许你会觉得我的这篇文章很有用,也许你会觉得很无聊,但是无论如何,如果你能够对 可重复注解 有了一个全新的认知,那么便是我的荣幸。

说实话,今天是我第一次阅读JLS,用的翻译软件是 DeepL.

至少,对我来讲也是一种成长,不是么~

也没什么想说的了,结束。

DFC4B1222A34870AC1287BCCA60BBD35.jpg

猜你喜欢

转载自juejin.im/post/7031079854674018340