为什么缺少注解不会导致classNotFoundException
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. 访问缺少的注解
让我们更改 B 类,使其 反射性地检索A 信息:
@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。