1. 概述

在本教程中,我们将熟悉 Java 编程语言中一个看似奇怪的功能:缺少注释不会在运行时导致任何异常。

然后,我们将更深入地了解控制这种行为的原因和规则以及这些规则的例外情况。

2. 快速回顾一下

让我们从一个熟悉的 Java 示例开始。有 A 类,然后是 B 类,它依赖于 A

public class A {
}

public class B {
    public static void main(String[] args) {
        System.out.println(new A());
    }
}

现在,如果我们编译这些类并运行编译后的 B ,它将在控制台上为我们打印一条消息:

>> javac A.java
>> javac B.java
>> java B
A@d716361

但是,如果我们删除已编译的 A .class 文件并重新运行类 B ,我们将看到ClassNotFoundException 引起的 NoClassDefFoundError

>> rm A.class
>> java B
Exception in thread "main" java.lang.NoClassDefFoundError: A
        at B.main(B.java:3)
Caused by: java.lang.ClassNotFoundException: A
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
        ... 1 more

发生这种情况是因为类加载器在运行时找不到类文件,即使它在编译期间就在那里 。这是许多 Java 开发人员所期望的正常行为。

3. 缺少注释

现在,让我们看看在相同情况下注释会发生什么。为此,我们将 A 类更改为注释:

@Retention(RetentionPolicy.RUNTIME)
public @interface A {
}

如上所示,Java在运行时会保留注解信息。之后,是时候用 A 来注释类 B 了:

@A
public class B {
    public static void main(String[] args) {
        System.out.println("It worked!");
    }
}

接下来,让我们编译并运行这些类:

>> javac A.java
>> javac B.java
>> java B
It worked!

因此,我们看到 B 成功在控制台上打印了它的消息,这是有道理的,因为所有内容都被编译并连接在一起得很好。

现在,让我们删除 A 的类文件:

>> rm A.class
>> java B
It worked!

如上所示, 即使缺少注解类文件,注解类运行也没有任何异常

3.1.使用类标记进行注释

为了让它更有趣,让我们引入另一个具有 Class<?> 属性的注释:

@Retention(RetentionPolicy.RUNTIME)
public @interface C {
    Class<?> value();
}

如上所示,该注解有一个名为 value 的属性,其返回类型为 Class<?> 。作为该属性的参数,我们添加另一个名为 D 的空类:

public class D {
}

现在,我们将使用这个新注释来注释 B 类:

@A
@C(D.class)
public class B {
    public static void main(String[] args) {
        System.out.println("It worked!");
    }
}

当所有类文件都存在时,一切都应该正常工作。但是,当我们只删除 D 类文件而不删除其他文件时会发生什么呢?让我们来了解一下:

>> rm D.class
>> java B
It worked!

如上所示,尽管运行时没有 D ,但一切仍然正常!因此,除了注释之外, 属性引用的类标记也不需要在运行时存在

3.2. Java 语言规范

因此,我们看到一些具有运行时保留的注释在运行时丢失,但带注释的类运行完美。尽管听起来有些意外,但根据Java 语言规范第 9.6.4.2 节,这种行为实际上是完全正常的:

注释可能仅存在于源代码中,也可能以类或接口的二进制形式存在。以二进制形式存在的注释 在运行时可能通过 Java SE 平台的反射库可用,也可能不可用

此外, JLS §13.5.7条目还指出:

添加或删除注释对于 Java 编程语言中程序的二进制表示的正确链接没有影响。

最重要的是,运行时不会因缺少注释而引发异常,因为 JLS 允许这样做

3.3.访问丢失的注释

让我们以反射方式检索 A 信息的方式更改 B 类:

@A
public class B {
    public static void main(String[] args) {
        System.out.println(A.class.getSimpleName());
    }
}

如果我们编译并运行它们,一切都会好起来的:

>> javac A.java
>> javac B.java
>> java B
A

现在,如果我们删除 A 类文件并运行 B ,我们将看到由 ClassNotFoundException 引起的相同 NoClassDefFoundError

Exception in thread "main" java.lang.NoClassDefFoundError: A
        at B.main(B.java:5)
Caused by: java.lang.ClassNotFoundException: A
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
        ... 1 more

根据 JLS 的说法,注释不必在运行时可用。但是, 当其他代码读取该注释并对其执行某些操作(就像我们所做的那样)时,该注释必须在运行时存在 。否则,我们会看到 ClassNotFoundException

4。结论

在本文中,我们看到了一些注释如何在运行时缺失,即使它们是类的二进制表示的一部分。

与往常一样,所有示例都可以 在 GitHub 上找到。