倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

ラムダ式やストリーム API や新しい日時 API だけじゃない! Java8 のタイプ・アノテーションあの手この手

Java8 ではラムダ式やストリーム API に注目が集まってますが、何気にアノテーションに関しても機能拡張がされているようなので簡単な変更点や使い方を今夜試してみました。

目次

参考

ElementType の追加

java.lang.annotation.ElementType はアノテーションを付加できる箇所を指定する定数(列挙型)です。 Java7 までは

  • PACKAGE ・・・ パッケージ宣言
  • TYPE ・・・ インターフェース、クラス宣言
  • ANOTATION_TYPE ・・・ アノテーション型宣言
  • FIELD ・・・ フィールド宣言
  • CONSTRUCTOR ・・・ コンストラクタ宣言
  • METHOD ・・・ メソッド宣言
  • PARAMETER ・・・ パラメータ(メソッド引数)宣言
  • LOCAL_VARIABLE ・・・ ローカル変数宣言

の8種類がありました。 これらはいずれも何かしらの「宣言 (declaration)」部分だけでしたが、Java8 からは

  • TYPE_PARAMETER ・・・ 型パラメータ宣言
  • TYPE_USE ・・・ 型使用箇所

が追加され、TYPE_USE は今までの宣言箇所とは違って型が使用されているあらゆるところで使用することが出来るようになりました。

TYPE_PARAMETER
ElementType.TYPE_PARAMETER はアノテーションが付加できる箇所として、ジェネリクスの型パラメータ宣言の部分を指定します。 たとえば、以下のようなアノテーションを作った場合

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_PARAMETER)
public @interface OnTypeParam {}

以下のようにアノテーションを付加することが出来ます:

public class MyClass<@OnTypeParam E>{

    public <@OnTypeParam F> F yourMethod(){
        //...
    }
}

まぁ、難しくないですね。 実行時にアノテーションを取得する方法も見ておきましょう。 まず @OnTypeParam アノテーションのリテンション・ポリシー(アノテーション情報をどの段階まで保持しておくか)を RUNTIME にしておきます:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnTypeParam {}

MyClass クラスへのアノテーションの付け方は上記と同じとして、@OnTypeParam アノテーションオブジェクトを取得するには以下のようにします:

import java.lang.reflect.TypeVariable;
import java.lang.reflect.Method;

// クラスの型パラメータ
TypeVariable<Class<MyClas>> tv = MyClass.class.getTypeParameters()[0];  // 型パラメータ1つ目
OnTypeParam tp = tv.getAnnotation(OnTypeParam.class);
System.out.println(tp);    // 「@OnTypeParam()」と表示

// メソッドの型パラメータ
TypeVariable<Method> tv = MyClass.class.getDeclaredMethod("yourMethod")
                                       .getTypeParameters()[0];
OnTypeParam tp = tv.getAnnotation(OnTypeParam.class);
System.out.println(tp);    // 「@OnTypeParam()」と表示

TypeVariable とかなんとかかんとかが面倒ですが、途中にヘンにローカル変数を入れなければあんまり気にする必要はありません。

TYPE_USE
TYPE_USE は型が使われている箇所ならどこにでもアノテーションを追加できるようにする ElementType です。 文章で説明するより具体例を見た方がいいでしょう。 以下のように @OnTypeUse アノテーションを定義しましょう:

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_USE)
public @interface OnTypeUse {}

このとき、以下のようにあちこちこのアノテーションを付加することが出来ます:

import java.util.List;
import java.util.ArrayList;

import javafx.application.Application;
import javafx.stage.Stage;

@OnTypeUse    // 型宣言
public class MyApp<@OnTypeUse E>    // 型パラメータ宣言
        extends @OnTypeUse Application{    // 拡張(or 実装)する型

    @Override
    public void start(@OnTypeUse Stage stage)    // パラメータの型
            throws @OnTypeUse Exception {    // スローする例外の型

        @OnTypeUse Number value = 0;    // ローカル変数の型

        List<@OnTypeUse Integer> list = new @OnTypeUse ArrayList<>();  // コンストラクタ呼び出し
        Integer i = (@OnTypeUse Integer)value;    // 型によるキャスト
    }

    public @OnTypeUse Integer getIndex(){    // 返り値の型
        return 1;
    }

    public <@OnTypeUse F> F getName(){    // ジェネリック・メソッドの型パラメータ宣言
        return null;
    }
}
  • ローカル変数や返り値の型などの場合、プリミティブ型でも同じようにアノテーションを付加することができます。
  • IntelliJ IDEA で試したところ、ジェネリックなメソッドの型パラメータと返り値に同時にアノテーションをつける(上記の例で、getIndex() と getName() のような付け方を同時にする)とコンパイルエラーが表示されますが、実行は可能なので IDEA の方の問題かと。
  • ローカル変数やパラメータ(メソッド引数)宣言に付ける場合、ElementType.LOCAL_VARIABLE, ElementType.PARAMETER との使い分けがちょっとややこしくなりそうな気もします。
  • import 文には付けられません。 

なんかあちこちにアノテーション付けられますね・・・ これで網羅してるかどうかも分かりませんし。 こんなにあちこちに付けられて何に使うかというと、チュートリアルや上記参考記事などを参照して下さい。 どうも外部ツールを使って @NonNull のような追加の型チェックや getter/setter の自動生成などのソースコード・ジェネレーションを行うことができるようです。 これらを試してみないと Java8 の拡張が片手落ちって気もしますが、今回はやりません。

定義済みアノテーション

次は Java8 の標準 API に追加された定義済みアノテーションを見ていきます。 主要パッケージを見たところ、

  • @Repeatable アノテーション(java.lang.annotation パッケージ)
  • @FunctionalInterface アノテーション(java.lang パッケージ)
  • @Native アノテーション

というのが追加された模様。

@Repeatable アノテーション
@Repeatable アノテーションは、1つの要素に同じアノテーションを複数付加したい場合に使用します。 たとえば、以下のように Main クラスのクラス宣言に @Grab アノテーションを2つ追加したいとしましょう:

@Grab("org.apache.commons:commons-math3:3.2")
@Grab("junit:junit:4.11")
public class Main{

    public static void main(String... args){
        // ...
    }
}

今までは Main というクラスに @Grab というアノテーションを付加する場合、1つしかできませんでしたが、Java8 からは出来るようになりました。 ただし、実際にはそれらをまとめた別のアノテーション(以下、コンテナ・アノテーション)に付け替えているような感じです。 つまり、コンテナ・アノテーションを @Grapes アノテーションとして

@Grapes({
    @Grab("org.apache.commons:commons-math3:3.2"),
    @Grab("junit:junit:4.11")
})
public class Main{

    public static void main(String... args){
        // ...
    }
}

と宣言しているのと同じになります。 ではアノテーションの定義方法を見ていきましょう。

まず、複数付加したい @Grab アノテーション の定義:

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Grapes.class)
public @interface Grab {
    String value();
}

このアノテーションに @Repeatable アノテーションを付加します。 この @Repeatable アノテーションには、コンテナ・アノテーションとなる @Grapes アノテーションのクラスを指定します。 リテンション・ポリシーを指定しているのは後のためですが、通常は必須ではありません。

次は、コンテナ・アノテーションとなる @Grapes アノテーションの定義:

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Grapes {
    Grab[] value();    // @Grab アノテーションの配列を返す value() メソッド
}

コンテナ・アノテーションには value() メソッドを定義し、複数追加したいアノテーション @Grab の配列を返すようにします。 少々注意が必要なのは、アノテーション定義に付加している @Target, @Retention アノテーションの指定は @Grab に指定したものと同じでなければいけないところ。 まぁ、違ってたらコンパイラに怒られるだけですが。 以上で1つの要素に複数の @Grab アノテーションを付加できるようになります。 同じようなアノテーション定義を2つ書く必要があるのがちょっと面倒。 こういうのをアノテーション使ったソースコード/ジェネレーションでなんとかしてほしい(笑)

ちなみに、実行時にこれらのアノテーションを取得したい場合は以下のように、まず @Grapes アノテーションを取得してから、value() メソッドで @Grab アノテーションの配列を取得します:

@Grab("org.apache.commons:commons-lang3:3.3.2")
@Grab("junit:junit:4.11")
public class Main{

    public static void main(String... args){
        // まずは @Grapes アノテーションを取得
        Grapes grapes = Main.class.getDeclaredAnnotation(Grapes.class);

        // Grapes#value() によって @Grab アノテーションの配列を取得
        for(Grab grab : grapes.value()){
            System.out.println(grab.value());
        }
    }
}

まぁ、別に難しくはないですね。

@FunctionalInterface アノテーション
付加した型が SAM (Single Abstract Method) 型であること、つまり抽象メソッドが1つしかないインターフェースもしくは抽象クラスであることをコンパイル時にチェックします。 Java8 では SAM 型はラムダ式を代入できるという意味で特別扱いされてます。

@Native アノテーション
ネイティブコード(C/C++ でしょうね)から参照される定数フィールドに付加して、ヘッダファイルの生成のヒントにするらしいです。 らしいです。


Java8 で追加されたアノテーション関連の機能を見てきましたが、あんまり具体的に役立ちそうな使い方は扱ってなかったので、「だからどうした?」とつっこまれそう・・・ これらのアノテーションの機能は @Override 見たいにバグを減らすのにものすごく貢献しそうなものだそうなので、積極的に使っていきたいところ。

デートTIME

デートTIME