1. 概要

このチュートリアルでは、Javaプログラミング言語の一見奇妙な機能について説明します。注釈がなくても、実行時に例外は発生しません。

次に、この動作を管理する理由とルール、およびそのようなルールの例外を確認するために、さらに深く掘り下げます。

2. クイックリフレッシャー

おなじみのJavaの例から始めましょう。 クラスAがあり、次にAに依存するクラスBがあります。

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は実行時に注釈情報を保持します。 その後、クラスBAで注釈を付けます。

@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. クラストークンによる注釈

さらに面白くするために、 クラス>属性:

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

上に示したように、このアノテーションには次の名前の属性があります価値のリターンタイプクラス> 。 その属性の引数として、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が表示されます。

4. 結論

この記事では、クラスのバイナリ表現の一部であるにもかかわらず、実行時にいくつかのアノテーションが存在しない可能性があることを確認しました。

いつものように、すべての例はGitHubから入手できます。