- JAVA
Javaアノテーション一覧と作り方|@Overrideから自作まで
Javaのアノテーションとは、クラス・メソッド・フィールドなどに @名前 の形で付ける「注釈(追加情報)」です。 アノテーション自体は処理をしません。付けられた情報を、コンパイラ・フレームワーク・自分で書いたコードが読み取って、チェックや自動処理に使います。
この記事では、標準アノテーション5種とメタアノテーション5種の一覧、@SuppressWarnings に指定できる値、自作アノテーションをリフレクションで読んで簡易バリデーションを動かす手順、@Retention を RUNTIME にし忘れて値が取れない例、Spring・Lombok・JUnit でよく見るアノテーションの早見表までを扱います。標準 API だけで書けるコードはすべて OpenJDK 21.0.10 でコンパイル・実行し、出力とエラー文をそのまま載せています。Java 25 との違いは公式 API ドキュメントで確認して注記しました。
Javaアノテーションの早見表
標準アノテーション(java.lang)
| アノテーション | 付ける場所 | 役割 | 付けたときの効果 |
|---|---|---|---|
@Override |
メソッド | 親のメソッドを上書きしていることを示す | 上書きになっていないとコンパイルエラー |
@Deprecated |
クラス・メソッド・フィールドなど | 非推奨であることを示す | 使った側に警告が出る |
@Deprecated(since="2.0", forRemoval=true) |
同上 | 将来削除する予定であることを示す | より強い [removal] 警告が既定で出る |
@SuppressWarnings("キー") |
クラス・メソッド・変数など | 指定した種類の警告を出さないようにする | その範囲の警告が消える |
@FunctionalInterface |
インターフェース | 関数型インターフェース(抽象メソッドが1つ)であることを示す | 抽象メソッドが1つでないとコンパイルエラー |
@SafeVarargs |
可変長引数のメソッド・コンストラクタ | ジェネリクスの可変長引数を安全に扱っていると宣言する | 宣言側・呼び出し側の unchecked 警告が消える |
メタアノテーション(java.lang.annotation)
メタアノテーションは、アノテーションを作るときに、そのアノテーション自身に付けるアノテーションです。
| メタアノテーション | 決めること | 指定する値 | 省略したとき |
|---|---|---|---|
@Retention |
情報をいつまで残すか | SOURCE / CLASS / RUNTIME |
CLASS(実行時には読めない) |
@Target |
どこに付けられるか | TYPE、FIELD、METHOD など |
すべての宣言に付けられる(型の使用箇所を除く) |
@Documented |
Javadoc に載せるか | なし | Javadoc に載らない |
@Inherited |
子クラスに引き継ぐか | なし | 引き継がれない |
@Repeatable |
同じ場所に複数回付けられるか | 入れ物になるアノテーションのクラス | 1回だけ |
自作アノテーションの最小形
@Retention(RetentionPolicy.RUNTIME) // 実行時に読めるようにする
@Target(ElementType.FIELD) // フィールドに付ける
@interface NotBlank {
String message() default "必須項目です";
}
「@interface で宣言し、@Retention(RUNTIME) と @Target を付ける」が自作の基本形です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaアノテーション一覧と作り方|@Overrideから自作まで」
標準アノテーション5種の使い方
@Overrideと@FunctionalInterface:書き間違いをコンパイルエラーで知らせる
この2つは、「そのつもりで書いた」ことをコンパイラに確認してもらうアノテーションです。わざと間違えたコードをコンパイルします。
public class Std {
static class Task {
String title = "掃除";
@Override public String tostring() { return title; } // 綴り違い
}
@FunctionalInterface
interface Converter {
String convert(String s);
String revert(String s); // 抽象メソッドが2つ
}
}
コンパイル結果(Java 21.0.10、javac Std.java):
Std.java:7: error: method does not override or implement a method from a supertype
@Override public String tostring() { return title; } // 綴り違い
^
Std.java:10: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
Converter is not a functional interface
multiple non-overriding abstract methods found in interface Converter
2 errors
@Override がなければ、tostring() は「新しい別のメソッド」としてコンパイルが通り、toString() は上書きされないまま動きます。@Override を付けておくと、綴りや引数の違いにその場で気づけます。 オーバーライドの詳しいルールは別記事「Javaオーバーライドとオーバーロードの違い|@Overrideの役割」、関数型インターフェースは「Java ラムダ式の使い方|関数型インターフェース全43種の一覧と実行結果」で扱っています。
@Deprecated:非推奨と「削除予定」を伝える
@Deprecated には2つの要素があります。
| 要素 | 型 | 意味 | 既定値 |
|---|---|---|---|
since |
String |
いつから非推奨か(バージョン) | "" |
forRemoval |
boolean |
将来のバージョンで削除する予定か | false |
警告の出方の違いを、警告の出るコードで確かめます。
import java.util.ArrayList;
import java.util.List;
class OldApi {
@Deprecated
static void print(String s) { System.out.println(s); }
@Deprecated(since = "2.0", forRemoval = true)
static void printAll(List<String> list) { list.forEach(System.out::println); }
}
public class Dep {
@SafeVarargs
static <T> List<T> listOf(T... items) { return new ArrayList<>(List.of(items)); }
static <T> List<T> listOfNoAnno(T... items) { return new ArrayList<>(List.of(items)); }
public static void main(String[] args) {
OldApi.print("a");
OldApi.printAll(List.of("b", "c"));
List raw = new ArrayList(); // raw 型
raw.add("d");
List<String> typed = raw; // unchecked
System.out.println(typed + " " + listOf("e", "f") + listOfNoAnno(List.of("g")));
}
}
オプションなしでコンパイルした結果(Java 21.0.10、javac Dep.java):
Dep.java:20: warning: [removal] printAll(List<String>) in OldApi has been deprecated and marked for removal
OldApi.printAll(List.of("b", "c"));
^
Note: Dep.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Note: Dep.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
1 warning
forRemoval = true のほうは、オプションなしでも行番号つきの [removal] 警告が出ます。 通常の @Deprecated は「非推奨の API を使っています」という Note だけで、場所を知るには -Xlint:deprecation が必要です。
javac -Xlint:all Dep.java でコンパイルすると、9件の警告が出ました。種類ごとに抜粋します。
Dep.java:16: warning: [unchecked] Possible heap pollution from parameterized vararg type T
static <T> List<T> listOfNoAnno(T... items) { return new ArrayList<>(List.of(items)); }
^
Dep.java:19: warning: [deprecation] print(String) in OldApi has been deprecated
OldApi.print("a");
^
Dep.java:20: warning: [removal] printAll(List<String>) in OldApi has been deprecated and marked for removal
OldApi.printAll(List.of("b", "c"));
^
Dep.java:21: warning: [rawtypes] found raw type: List
List raw = new ArrayList(); // raw 型
^
Dep.java:22: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
raw.add("d");
^
Dep.java:23: warning: [unchecked] unchecked conversion
List<String> typed = raw; // unchecked
^
Dep.java:24: warning: [unchecked] unchecked generic array creation for varargs parameter of type List<String>[]
System.out.println(typed + " " + listOf("e", "f") + listOfNoAnno(List.of("g")));
^
警告の先頭にある [deprecation]・[removal]・[rawtypes]・[unchecked] が、そのまま @SuppressWarnings に指定するキーになります。
非推奨にするときは、Javadoc の @deprecated タグに「代わりに何を使うか」を書いておくと、使う側が移行しやすくなります。
/**
* @deprecated 2.0 から {@link #printAll(List)} の代わりに {@code Printer.printLines} を使ってください。
*/
@Deprecated(since = "2.0", forRemoval = true)
static void printAll(List<String> list) { ... }
@SuppressWarnings:確認済みの警告だけを消す
上の警告を、内容を確認したうえで @SuppressWarnings で抑えた版です。
import java.util.ArrayList;
import java.util.List;
class OldApi {
@Deprecated
static void print(String s) { System.out.println(s); }
@Deprecated(since = "2.0", forRemoval = true)
static void printAll(List<String> list) { list.forEach(System.out::println); }
}
public class DepFixed {
@SafeVarargs
static <T> List<T> listOf(T... items) { return new ArrayList<>(List.of(items)); }
@SuppressWarnings("deprecation") // 通常の非推奨
static void callOld() { OldApi.print("a"); }
@SuppressWarnings("removal") // forRemoval = true の非推奨
static void callRemoval() { OldApi.printAll(List.of("b", "c")); }
@SuppressWarnings({"rawtypes", "unchecked"}) // 複数は配列で
static List<String> legacy() {
List raw = new ArrayList();
raw.add("d");
return raw;
}
public static void main(String[] args) {
callOld();
callRemoval();
System.out.println(legacy() + " " + listOf("e", "f"));
}
}
javac -Xlint:all DepFixed.java の結果と実行結果(Java 21.0.10):
DepFixed.java:14: warning: [varargs] Varargs method could cause heap pollution from non-reifiable varargs parameter items
static <T> List<T> listOf(T... items) { return new ArrayList<>(List.of(items)); }
^
1 warning
a
b
c
[d] [e, f]
9件あった警告が1件になりました。残った [varargs] は、@SafeVarargs を付けたメソッドの中で可変長引数の配列を別のメソッド(List.of)へ渡している箇所を、-Xlint:varargs が知らせているものです。@SafeVarargs は「このメソッドは配列を安全に扱っている」という作者の宣言なので、中身を確認できるメソッドにだけ付けます(付けられるのは static・final・private のメソッドとコンストラクタです)。
@SuppressWarnings を使うときのポイントは3つです。
"deprecation"と"removal"は別のキーです。forRemoval = trueの API は"removal"で抑えます。- できるだけ狭い範囲に付けます。 クラス全体ではなくメソッドや変数宣言に付けると、新しく増えた警告に気づけます。
- 付けるときは、「なぜ抑えてよいか」をコメントに1行残しておくと、後から読む人が判断しやすくなります。
@SuppressWarningsに指定できる値の一覧
Java 21.0.10 で javac --help-lint を実行して表示されたキーのうち、ソースコードの警告としてよく使うものをまとめました。
| キー | 抑える警告 | 出る場面の例 |
|---|---|---|
deprecation |
非推奨 API の使用 | @Deprecated の付いたメソッドを呼ぶ |
removal |
削除予定 API の使用 | @Deprecated(forRemoval = true) のメソッドを呼ぶ |
unchecked |
型の確認ができない操作 | raw 型から List<String> への代入、ジェネリクスへのキャスト |
rawtypes |
raw 型の使用 | List list = new ArrayList(); |
varargs |
可変長引数の扱い | ジェネリクスの可変長引数の配列をほかへ渡す |
fallthrough |
switch の case で break がない |
次の case へそのまま進む |
serial |
Serializable クラスに serialVersionUID がない |
例外クラスを自作したとき |
cast |
不要なキャスト | (String) "abc" |
finally |
正常に終わらない finally |
finally の中で return |
static |
インスタンス経由の static メンバー参照 | obj.staticMethod() |
try |
try-with-resources まわり | 使われないリソース変数 |
overrides |
オーバーライドに関する注意 | equals だけを上書きして hashCode がない |
divzero |
定数 0 での整数の割り算 | x / 0 |
empty |
if の直後の空文 |
if (ok); |
this-escape |
コンストラクタから上書き可能なメソッドを呼ぶ | 子クラスの初期化前に動く可能性(Java 21 で追加) |
preview |
プレビュー機能の使用 | --enable-preview で使う構文 |
"unused" など、この一覧にないキーは、IDE(Eclipse、IntelliJ IDEA など)が独自に解釈するものです。javac は知らないキーを無視します。お使いの JDK での最新の一覧は javac --help-lint で確認できます。
メタアノテーション5種で「どんなアノテーションか」を決める
@Retention:情報をいつまで残すか
| 値 | 残る範囲 | リフレクションで読めるか | 例 |
|---|---|---|---|
RetentionPolicy.SOURCE |
ソースコードまで。コンパイルで消える | 読めない | @Override、@SuppressWarnings、Lombok の @Getter |
RetentionPolicy.CLASS |
.class ファイルまで。省略時はこれ |
読めない | バイトコードを処理するツール向け |
RetentionPolicy.RUNTIME |
実行時まで | 読める | @Deprecated、@FunctionalInterface、Spring や JUnit のアノテーション |
実行時に自分のコードやフレームワークで読みたいアノテーションは、RUNTIME を指定します。
@Target:どこに付けられるか
ElementType の値 |
付けられる場所 |
|---|---|
TYPE |
クラス、インターフェース、enum、record、アノテーション |
FIELD |
フィールド(enum の定数を含む) |
METHOD |
メソッド |
PARAMETER |
メソッド・コンストラクタの引数 |
CONSTRUCTOR |
コンストラクタ |
LOCAL_VARIABLE |
ローカル変数 |
ANNOTATION_TYPE |
アノテーション(メタアノテーションを作るとき) |
PACKAGE |
パッケージ(package-info.java) |
TYPE_PARAMETER |
型パラメータ(<T> の T) |
TYPE_USE |
型を書く場所すべて(List<@NonNull String> など) |
MODULE |
モジュール(module-info.java) |
RECORD_COMPONENT |
record のコンポーネント |
複数に付けたいときは @Target({ElementType.FIELD, ElementType.METHOD}) と配列で書きます。
決まりに合わないときのコンパイルエラー
@Target の範囲外に付けた場合などのエラー文を、まとめて確認します。
import java.lang.annotation.*;
import java.util.List;
public class TargetErr {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface OnlyMethod { }
@interface BadElement {
List<String> names(); // 使えない型
}
@interface NeedMax { int max(); }
@OnlyMethod // クラスに付けた
static class A { }
@NeedMax // 必須の要素を指定していない
static class B { }
@OnlyMethod @OnlyMethod // 同じものを2回
void twice() { }
}
コンパイル結果(Java 21.0.10。メッセージ行のみ抜粋):
TargetErr.java:21: error: OnlyMethod is not a repeatable annotation interface
TargetErr.java:15: error: annotation interface not applicable to this kind of declaration
TargetErr.java:18: error: annotation @NeedMax is missing a default value for the element 'max'
TargetErr.java:10: error: invalid type for annotation interface element
4 errors
| エラー文 | 原因 | 直し方 |
|---|---|---|
annotation interface not applicable to this kind of declaration |
@Target にない場所へ付けた |
付ける場所を変える、または @Target に追加する |
is missing a default value for the element 'max' |
default のない要素を指定していない |
@NeedMax(max = 10) と書く、または宣言側に default を付ける |
is not a repeatable annotation interface |
同じアノテーションを2回付けた | @Repeatable を付ける(後述) |
invalid type for annotation interface element |
要素に使えない型を書いた | 下の表の型にする |
アノテーションの要素(message() のような宣言)に使える型は決まっています。
| 使える型 | 例 |
|---|---|
| 基本型 | int max();、boolean enabled(); |
String |
String message(); |
Class |
Class<? extends Throwable> expected(); |
| enum | Level level(); |
| ほかのアノテーション | Tag tag(); |
| 上記の1次元配列 | String[] names(); |
List や Object、Integer などのラッパー型は使えません。また、値に null は指定できないので、「指定なし」を表したいときは空文字 "" などを default にします。
@Inheritedと@Repeatableを動かして確かめる
import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.Arrays;
public class MetaDemo {
// ---- @Inherited:親クラスに付けたものが子クラスからも取れる ----
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited
@interface Audited { }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
@interface Plain { }
@Audited @Plain static class BaseService { }
static class StockService extends BaseService { }
@Audited interface Marker { }
static class Impl implements Marker { }
// ---- @Repeatable:同じアノテーションを複数回付けられる ----
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@Repeatable(Tags.class)
@interface Tag { String value(); }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface Tags { Tag[] value(); } // 入れ物になるアノテーション
@Tag("fast") @Tag("db")
void twoTags() { }
@Tag("single")
void oneTag() { }
public static void main(String[] args) throws Exception {
System.out.println("子クラスから @Audited(Inheritedあり): " + StockService.class.isAnnotationPresent(Audited.class));
System.out.println("子クラスから @Plain (Inheritedなし): " + StockService.class.isAnnotationPresent(Plain.class));
System.out.println("インターフェース経由の @Audited : " + Impl.class.isAnnotationPresent(Audited.class));
System.out.println("getDeclaredAnnotations(子) : " + StockService.class.getDeclaredAnnotations().length);
Method two = MetaDemo.class.getDeclaredMethod("twoTags");
Method one = MetaDemo.class.getDeclaredMethod("oneTag");
System.out.println("2つ付けた getAnnotation(Tag) : " + two.getAnnotation(Tag.class));
System.out.println("2つ付けた getAnnotationsByType(Tag) : " + Arrays.toString(two.getAnnotationsByType(Tag.class)));
System.out.println("2つ付けた getAnnotation(Tags) : " + two.getAnnotation(Tags.class));
System.out.println("1つ付けた getAnnotation(Tag) : " + one.getAnnotation(Tag.class));
}
}
実行結果(Java 21.0.10):
子クラスから @Audited(Inheritedあり): true
子クラスから @Plain (Inheritedなし): false
インターフェース経由の @Audited : false
getDeclaredAnnotations(子) : 0
2つ付けた getAnnotation(Tag) : null
2つ付けた getAnnotationsByType(Tag) : [@MetaDemo.Tag("fast"), @MetaDemo.Tag("db")]
2つ付けた getAnnotation(Tags) : @MetaDemo.Tags({@MetaDemo.Tag("fast"), @MetaDemo.Tag("db")})
1つ付けた getAnnotation(Tag) : @MetaDemo.Tag("single")
読み取れるポイントです。
@Inheritedが効くのは「クラスの継承」だけです。 インターフェースに付けたアノテーションは、実装クラスからは取れません(3行目がfalse)。メソッドやフィールドに付けたアノテーションにも効きません。getDeclaredAnnotations()は、そのクラスに直接付いているものだけを返します(4行目が0)。@Repeatableで2つ以上付けると、内部では入れ物(@Tags)に包まれます。 そのためgetAnnotation(Tag.class)はnullになります。繰り返しアノテーションはgetAnnotationsByType()で読むと、1つのときも複数のときも同じ書き方で取得できます。
@Documented は、付けたアノテーションが Javadoc の出力に表示されるようにするものです(この記事では Javadoc の生成は行っていません。公式 API ドキュメントの説明に基づきます)。公開 API の一部として読み手に伝えたいアノテーションに付けます。
自作アノテーションで簡易バリデーションを作る
アノテーションを自作する流れは、次の3ステップです。
| ステップ | やること | この例では |
|---|---|---|
| 1. 定義する | @interface でアノテーションを宣言する |
@NotBlank、@Range、@MaxLength |
| 2. 付ける | クラスやフィールドにアノテーションを書く | 注文フォーム OrderForm のフィールド |
| 3. 読む | リフレクションでアノテーションを取得して処理する | validate() メソッド |
アノテーションは情報を付けるだけなので、3の「読む側」を書いて初めて動きます。
コピーして動かせる完成形
Validate.java に保存して、そのまま実行できます。外部ライブラリは不要です。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class Validate {
// ---- 自作アノテーション ----
@Retention(RetentionPolicy.RUNTIME) // 実行時にリフレクションで読めるようにする
@Target(ElementType.FIELD) // フィールドにだけ付けられる
@interface NotBlank {
String message() default "必須項目です";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Range {
int min() default 0;
int max(); // default がないので指定必須
String message() default "{min}〜{max} の範囲で入力してください";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface MaxLength {
int value(); // value という名前なら @MaxLength(20) と書ける
}
// ---- アノテーションを付ける側 ----
static class OrderForm {
@NotBlank(message = "商品名を入力してください")
@MaxLength(10)
private final String itemName;
@Range(min = 1, max = 99)
private final int quantity;
@NotBlank
private final String memberId;
private final String memo; // アノテーションなし=チェックしない
OrderForm(String itemName, int quantity, String memberId, String memo) {
this.itemName = itemName;
this.quantity = quantity;
this.memberId = memberId;
this.memo = memo;
}
}
// ---- アノテーションを読む側(リフレクション) ----
static List<String> validate(Object target) throws IllegalAccessException {
List<String> errors = new ArrayList<>();
for (Field f : target.getClass().getDeclaredFields()) {
f.setAccessible(true); // private フィールドを読めるようにする
Object value = f.get(target);
NotBlank notBlank = f.getAnnotation(NotBlank.class);
if (notBlank != null && (value == null || value.toString().isBlank())) {
errors.add(f.getName() + ": " + notBlank.message());
}
MaxLength maxLength = f.getAnnotation(MaxLength.class);
if (maxLength != null && value != null && value.toString().length() > maxLength.value()) {
errors.add(f.getName() + ": " + maxLength.value() + "文字以内で入力してください");
}
Range range = f.getAnnotation(Range.class);
if (range != null && value instanceof Integer n && (n < range.min() || n > range.max())) {
errors.add(f.getName() + ": " + range.message()
.replace("{min}", String.valueOf(range.min()))
.replace("{max}", String.valueOf(range.max())));
}
}
return errors;
}
public static void main(String[] args) throws Exception {
OrderForm ok = new OrderForm("ノート", 3, "M-001", "");
OrderForm ng = new OrderForm("とても長い名前のノートブック", 120, " ", null);
System.out.println("正しい入力: " + validate(ok));
System.out.println("直したい入力:");
validate(ng).forEach(e -> System.out.println(" - " + e));
// アノテーションの値を直接読む
Field q = OrderForm.class.getDeclaredField("quantity");
Range r = q.getAnnotation(Range.class);
System.out.println(r.annotationType().getSimpleName() + ": min=" + r.min() + ", max=" + r.max());
}
}
実行コマンドと結果(Java 21.0.10):
javac -encoding UTF-8 Validate.java
java Validate
正しい入力: []
直したい入力:
- itemName: 10文字以内で入力してください
- quantity: 1〜99 の範囲で入力してください
- memberId: 必須項目です
Range: min=1, max=99
OrderForm には if 文を1つも書いていませんが、フィールドにアノテーションを付けるだけでチェックの内容が決まり、validate() が共通の処理として働いています。 別のフォームクラスを作っても、同じ validate() をそのまま使えます。
書き方のポイント
| 書き方 | 意味 |
|---|---|
String message() default "必須項目です"; |
要素の宣言。メソッドのような形で書き、default で既定値を付ける |
int max(); |
default がない要素は、付ける側で必ず指定する |
int value(); |
要素名が value で、指定するのがそれ1つだけなら @MaxLength(10) と名前を省略できる |
@Range(min = 1, max = 99) |
複数の要素は 名前 = 値 をカンマで区切る |
@NotBlank |
すべての要素に default があれば、かっこごと省略できる |
f.getAnnotation(NotBlank.class) |
付いていればアノテーションのオブジェクト、付いていなければ null |
notBlank.message() |
要素の値は、メソッド呼び出しの形で読む |
リフレクションでアノテーションを読むメソッドは次のとおりです。Class・Field・Method・Constructor・Parameter で共通して使えます。
| メソッド | 返すもの |
|---|---|
isAnnotationPresent(X.class) |
付いているかどうか(boolean) |
getAnnotation(X.class) |
そのアノテーション。なければ null |
getAnnotations() |
付いているすべて(@Inherited で引き継いだものを含む) |
getDeclaredAnnotations() |
直接付いているものだけ |
getAnnotationsByType(X.class) |
繰り返しアノテーションを配列で |
なお、入力チェックをアノテーションで行う標準的な仕様として Jakarta Bean Validation(@NotBlank、@Size、@Min、@Max など)があり、Hibernate Validator がその実装として広く使われています。仕組みを理解する題材として自作し、アプリケーションでは既存のライブラリを選ぶ、という使い分けがおすすめです(この記事の環境では外部ライブラリを取得できないため、Bean Validation は実行していません)。
フレームワークのアノテーションも同じ仕組み
JUnit の @Test や Spring の @Component も、基本は同じ流れです。アノテーションを目印にクラスやメソッドをリフレクションで探し、要素の値を読んで処理を切り替えています。 メソッドに付けたアノテーションは Method の getAnnotation() で、フィールドと同じ書き方で読めます。この流れを一度自分で書いておくと、フレームワークの動きを想像しやすくなります。
@RetentionをRUNTIMEにし忘れて値が取れない例
自作アノテーションでいちばん多いつまずきが、@Retention を書き忘れて、getAnnotation() が null を返すケースです。コンパイルも通り、エラーも出ないので、原因に気づきにくいところです。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class RetentionMiss {
@interface NoRetention { } // @Retention を書き忘れた(=CLASS)
@Retention(RetentionPolicy.SOURCE) @interface SourceOnly { }
@Retention(RetentionPolicy.CLASS) @interface ClassOnly { }
@Retention(RetentionPolicy.RUNTIME) @interface AtRuntime { }
@NoRetention @SourceOnly @ClassOnly @AtRuntime
static class Target { }
public static void main(String[] args) {
System.out.println("NoRetention: " + Target.class.getAnnotation(NoRetention.class));
System.out.println("SourceOnly : " + Target.class.getAnnotation(SourceOnly.class));
System.out.println("ClassOnly : " + Target.class.getAnnotation(ClassOnly.class));
System.out.println("AtRuntime : " + Target.class.getAnnotation(AtRuntime.class));
System.out.println("isAnnotationPresent(NoRetention): " + Target.class.isAnnotationPresent(NoRetention.class));
System.out.println("getAnnotations().length: " + Target.class.getAnnotations().length);
}
}
実行結果(Java 21.0.10):
NoRetention: null
SourceOnly : null
ClassOnly : null
AtRuntime : @RetentionMiss.AtRuntime()
isAnnotationPresent(NoRetention): false
getAnnotations().length: 1
4つ付けたのに、実行時に取れたのは RUNTIME の1つだけです。 @Retention を書かなかった NoRetention は、CLASS を指定したものと同じ結果になりました。
.class ファイルの中を javap -v で見ると、違いがはっきりします(出力から該当部分を抜粋)。
RuntimeVisibleAnnotations:
0: #14()
RetentionMiss$AtRuntime
RuntimeInvisibleAnnotations:
0: #16()
RetentionMiss$NoRetention
1: #17()
RUNTIME のものは RuntimeVisibleAnnotations(実行時に見える)に、書き忘れたものと CLASS のものは RuntimeInvisibleAnnotations(実行時に見えない)に入っています。SOURCE のものは .class ファイルに残っていません。
| 直す前 | 直した後 |
|---|---|
@interface NotBlank { ... } |
@Retention(RetentionPolicy.RUNTIME) を付ける |
getAnnotation() が null、バリデーションが何も検出しない |
アノテーションが取得でき、チェックが働く |
「付けたのに効かない」ときは、まず @Retention(RetentionPolicy.RUNTIME) を確認します。 あわせて確認したい点を表にまとめます。
| 症状 | 確認すること |
|---|---|
getAnnotation() が null |
@Retention(RUNTIME) が付いているか |
| 子クラスから取れない | @Inherited が付いているか(クラスの継承のみ有効) |
複数付けたら null になった |
getAnnotationsByType() を使っているか |
| 親のフィールドのアノテーションが読めない | getDeclaredFields() はそのクラスの分だけ。getSuperclass() でさかのぼる |
private フィールドの値が読めない |
setAccessible(true) を呼んでいるか |
| 付けようとするとコンパイルエラー | @Target に付けたい場所が含まれているか |
よく見るライブラリのアノテーション早見表
フレームワークやライブラリのアノテーションを、役割ごとにまとめました。この章の表は各ライブラリの公式ドキュメントに基づく整理で、この記事の環境では外部ライブラリを取得できないため実行していません。 細かな仕様は、お使いのバージョンの公式ドキュメントで確認してください。
Spring Framework / Spring Boot
| アノテーション | 付ける場所 | 役割 |
|---|---|---|
@SpringBootApplication |
起動クラス | 自動設定とコンポーネントスキャンを有効にする |
@Component |
クラス | DI コンテナに登録する(汎用) |
@Service / @Repository / @Controller |
クラス | 役割を表す @Component の仲間 |
@RestController |
クラス | 戻り値をそのままレスポンス本文(JSON など)にするコントローラ |
@Autowired |
コンストラクタ・フィールド | 依存するオブジェクトを注入する(コンストラクタが1つなら省略できる) |
@Configuration / @Bean |
クラス / メソッド | Java コードで Bean を定義する |
@GetMapping / @PostMapping |
メソッド | URL と HTTP メソッドを対応付ける |
@PathVariable / @RequestParam / @RequestBody |
引数 | URL の一部・クエリ・リクエスト本文を引数に受け取る |
@Value |
フィールド・引数 | 設定値を注入する |
@Transactional |
クラス・メソッド | トランザクションの範囲を宣言する |
Lombok
| アノテーション | 役割 |
|---|---|
@Getter / @Setter |
ゲッター・セッターを生成する |
@ToString / @EqualsAndHashCode |
toString()、equals()・hashCode() を生成する |
@NoArgsConstructor / @AllArgsConstructor / @RequiredArgsConstructor |
コンストラクタを生成する |
@Data |
ゲッター・セッター・toString などをまとめて生成する |
@Value |
変更できない(イミュータブルな)クラスにする |
@Builder |
ビルダーパターンのコードを生成する |
@Slf4j |
ログ用の log フィールドを生成する |
Lombok のアノテーションは SOURCE で、コンパイル時にコードを生成します。Spring の @Value と Lombok の @Value は名前が同じで別物なので、import 文で見分けます。展開後のコードは別記事「Lombokアノテーション一覧|展開後コードと使い分け早見表」にまとめています。
JUnit(JUnit Jupiter)
| アノテーション | 役割 |
|---|---|
@Test |
テストメソッドであることを示す |
@BeforeEach / @AfterEach |
各テストの前・後に実行する |
@BeforeAll / @AfterAll |
クラス全体で最初・最後に1回実行する |
@DisplayName |
テストの表示名を付ける |
@ParameterizedTest |
引数を変えて同じテストを繰り返す |
@Nested |
テストクラスを入れ子にして整理する |
@Disabled |
テストを一時的に実行対象から外す |
@Tag |
テストに分類用のタグを付ける |
3つを仕組みで比べる
| 読むタイミング | 読むもの | Retention | |
|---|---|---|---|
| Lombok | コンパイル時 | アノテーションプロセッサ | SOURCE |
| Spring | 実行時(起動時) | DI コンテナがリフレクションで読む | RUNTIME |
| JUnit | 実行時(テスト実行時) | テストエンジンがリフレクションで読む | RUNTIME |
@Override など標準 |
コンパイル時 | コンパイラ | SOURCE(@Override、@SuppressWarnings) |
「誰が、いつ読むアノテーションか」を意識すると、初めて見るアノテーションでも役割を推測しやすくなります。
Java 25での違い(公式で確認・未実行)
Java SE 25 の公式 API ドキュメント(java.lang パッケージ)を確認したところ、アノテーションインターフェースは Deprecated・FunctionalInterface・Override・SafeVarargs・SuppressWarnings の5つで、Java 21 と同じでした。この記事の内容(標準5種、メタアノテーション、自作とリフレクション)は、Java 25 でも同じ書き方で使えます。 -Xlint のキーは JDK のバージョンで増えることがあるので、お使いの JDK で javac --help-lint を実行して確認してください(この記事の一覧は Java 21.0.10 の出力に基づきます)。
よくある質問
Q. アノテーションとコメントは何が違いますか?
A. コメントは人が読むためのもので、コンパイラやプログラムからは読めません。アノテーションは決まった形式の情報で、コンパイラ(@Override のチェックなど)や、実行時のプログラム(リフレクション)から読み取って処理に使えます。
Q. 自作アノテーションを付けても何も起きないのはなぜですか?
A. アノテーションは情報を付けるだけで、処理は「読む側」のコードが行います。読む側を書いているのに動かないときは、@Retention(RetentionPolicy.RUNTIME) が付いているかを確認します。省略時は CLASS になり、getAnnotation() が null を返します。
Q. @Deprecatedを付けたメソッドは使えなくなりますか?
A. 使えます。コンパイル時に警告が出るだけで、動作は変わりません。forRemoval = true を付けると「将来削除する予定」という意味になり、[removal] 警告が既定で表示されます。代わりの方法を Javadoc の @deprecated タグに書いておくと、使う側が移行しやすくなります。
Q. @interfaceとinterfaceは関係がありますか?
A. @interface はアノテーションを宣言するためのキーワードで、宣言されたアノテーションは自動的に java.lang.annotation.Annotation インターフェースを継承します。そのため要素をメソッドの形で宣言し、notBlank.message() のようにメソッド呼び出しで値を読みます。通常のインターフェースのように implements して使うものではありません。
まとめ
- アノテーションは
@名前で付ける追加情報。 それ自体は処理をせず、コンパイラ・フレームワーク・自分のコードが読んで使う - 標準は5種:
@Override、@Deprecated、@SuppressWarnings、@FunctionalInterface、@SafeVarargs @Deprecated(forRemoval = true)は[removal]警告になり、@SuppressWarnings("removal")で抑える("deprecation"とは別のキー)- 自作は
@interfaceで宣言し、@Retention(RUNTIME)と@Targetを付けるのが基本形 @Retentionを省略するとCLASSになり、getAnnotation()がnullを返す。 効かないときはまずここを確認する@Inheritedはクラスの継承だけに有効、@RepeatableはgetAnnotationsByType()で読む- Lombok はコンパイル時、Spring と JUnit は実行時に読む。 「誰がいつ読むか」で役割を整理できる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaアノテーション一覧と作り方|@Overrideから自作まで」
株式会社RJCでは、Javaのアノテーションとその仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。