argius note

プログラミング関連

Java SE 8 (3) - 新しい言語機能

このエントリーでは、(2)で触れた「ラムダ式」「メソッド参照」以外の新しい言語機能についてまとめています。

ラムダ式」「メソッド参照」については、(2)を参照してください。

(2014-03-21追記)APIドキュメントのリンクを差し替えました。

  • 目次
    • 仮想拡張メソッド
    • 実行時のパラメーター名へのアクセス
    • 型への注釈
    • 一般化されたターゲット型付けの型推論
    • 繰り返しアノテーション

仮想拡張メソッド

仮想拡張メソッドは、インターフェイスの仮想メソッドに、デフォルトの実装を与えることができる機能です。
これまでは、インターフェイスを実装する場合、インターフェイスで宣言されたメソッドを必ず実装する必要がありましたが、Java8ではインターフェイスの仮想メソッドに、デフォルトの実装を施すことが可能になりました。
デフォルト実装があれば、必ずしもオーバーライドをする必要がなくなり、デフォルト実装のままでは都合が悪いのであればオーバーライドすることもできます。
さらに、インターフェイスでstaticメソッドを定義することもできるようになりました。

  • サンプル: 仮想拡張メソッドの例
interface Interface1 {
    default void call() {
        System.out.println("Interface1.call (default)");
    }
    static void call0() {
        System.out.println("Interface1.call0 (static)");
    }
}
class Class1 implements Interface1 {
    // call()をオーバーライドしない
}
class Class2 implements Interface1 {
    // call()をオーバーライドする
    @Override
    public void call() {
        System.out.println("Class2.call (overridden)");
    }
}
class UsingVirtualExtensionMethod {
    public static void main(String[] args) {
        Interface1 o1 = new Class1();
        o1.call(); // => Interface1.call (default)
        Interface1 o2 = new Class2();
        o2.call(); // => Class2.call (overridden)
        Interface1.call0(); // => Interface1.call0 (static)
    }
}

この機能のサポートによって、階層が大きくて複雑な、例えばコレクションフレームワークのようなAPIでは、最小限の修正で拡張ができるようになりました。例えば、Collection#removeIfメソッドは、Collectionクラスに追加しただけでも、すべての実装クラスで仕様できます。(実際は、ArrayListなどでオーバーライドしています。)
staticメソッドは、直接インターフェイスにファクトリーメソッドを追加することができますね。


実行時のパラメーター名へのアクセス

リフレクションで使用できる情報として、オプションでクラスファイルにパラメーター名を埋め込むことができるようになりました。クラスファイルフォーマットでは、バージョン52.0からサポートされた属性です。

この情報はコンパイル時に設定しておく必要がありますが、設定しておけばランタイム情報として取得することができるので、例えばサブクラスのスケルトンを自動生成するような場合に、スーパークラスソースコード無しでパラメーター名まで継承させることができます。



実際に使ってみましょう。
まず、次の2つのクラスを作ってコンパイルしておきます。このとき、local/B.javaコンパイルするときに-parametersオプションを付けてコンパイルします。

  • サンプルコード1
// local/A.java
package local;
public final class A {
    public A(String init) { }
    public void targetMethod(String title, int number, String content) { }
}
// local/B.java
package local;
public final class B {
    public B(String init) { }
    public void targetMethod(String title, int number, String content) { }
}
$ javac local/A.java
$ javac -parameters local/B.java

これらのクラスをCLASSPATHに通しておき、次のコードを実行します。

  • サンプルコード2
// import java.lang.reflect.Constructor;
// import java.lang.reflect.Executable;
// import java.lang.reflect.Method;
// import java.lang.reflect.Parameter;
Class<?> ca = local.A.class;
Class<?> cb = local.B.class;
Executable[] executables = new Executable[4];
try {
    executables[0] = ca.getConstructor(String.class);
    executables[1] = ca.getMethod("targetMethod", String.class, int.class, String.class);
    executables[2] = cb.getConstructor(String.class);
    executables[3] = cb.getMethod("targetMethod", String.class, int.class, String.class);
} catch (NoSuchMethodException ex) {
    throw new RuntimeException(ex);
}
for (Executable executable : executables) {
    System.out.printf("class = [%s], method = [%s]%n",
                      executable.getDeclaringClass().getName(), executable.getName());
    int i = 0;
    for (Parameter p : executable.getParameters())
        System.out.printf("  parameter %d: type = [%s], name = [%s]%n",
                          ++i, p.getType(), p.getName());
}

Java8では、メソッドパラメーターを表すjava.lang.reflect.Parameterクラスが新規追加されています。これを使えば、メソッドの属性としてパラメーターの情報を取得することができます。

また、ConstructorクラスとMethodクラスの共通のスーパークラスjava.lang.reflect.Executableが追加されています。

  • 実行結果
class = [local.A], method = [local.A]
  parameter 1: type = [class java.lang.String], name = [arg0]
class = [local.A], method = [targetMethod]
  parameter 1: type = [class java.lang.String], name = [arg0]
  parameter 2: type = [int], name = [arg1]
  parameter 3: type = [class java.lang.String], name = [arg2]
class = [local.B], method = [local.B]
  parameter 1: type = [class java.lang.String], name = [init]
class = [local.B], method = [targetMethod]
  parameter 1: type = [class java.lang.String], name = [title]
  parameter 2: type = [int], name = [number]
  parameter 3: type = [class java.lang.String], name = [content]

型への注釈

総称型の型パラメーターにも、注釈を付けることができるようになりました。

具体的には、列挙型java.lang.annotation.ElementTypeTYPE_PARAMETERTYPE_USEが追加されました。TYPE_PARAMETERは総称型の型パラメーターに付加できる要素を、TYPE_USEは宣言に限らず型全般に付加できる(ローカル変数の型など)要素を表します。

  • サンプルコード
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.List;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE_PARAMETER})
@interface AnnotationForTypeParameter {
    public String value() default "default";
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE_USE})
@interface AnnotationForTypeUse {
    public String value() default "default";
}
// UTP = UsingTypeParameter
@AnnotationForTypeUse
class UTP<@AnnotationForTypeParameter @AnnotationForTypeUse T, R> extends @AnnotationForTypeUse Object {
    public static void main(String[] args) {
        @AnnotationForTypeUse @SuppressWarnings("unused")
        List<@AnnotationForTypeUse String> f = Arrays.asList();
        for (TypeVariable<Class<UTP>> tv : UTP.class.getTypeParameters())
            System.out.printf("type %s: %s%n", tv, Arrays.toString(tv.getAnnotations()));
   }
}
  • 実行結果
$ java8 UTP
type T: [@AnnotationForTypeParameter(value=default), @AnnotationForTypeUse(value=default)]
type R: []
$ 

一般化されたターゲット型付けの型推論

総称型の型推論は、一部のケースで上手く動作しないものがありました。

私も、自己参照を持つ総称クラスを作ったときにこの問題に突き当たりました。
JEPの説明でも、コンスセルのような再帰的なデータ構造を例として挙げています。


次のコードをJDK7のjavacコンパイルすると、

import java.util.*;
class A<T> {
    static <T> T foo() { return null; }
    public static void main(String[] args) {
        A<String> o = A.foo();
        List<String> a = new ArrayList<>();
        Collections.fill(a, A.foo());
    }
}

Collections.fillは総称メソッドになっていて(下記エラーメッセージ参照)、A.foo()の総称型TはStringになってくれても良さそうです。ところが...

A.java:7: エラー: クラス Collectionsのメソッド fillは指定された型に適用できません。
        Collections.fill(a, A.foo());
                   ^
  期待値: List<? super T>,T
  検出値: List<String>,Object
  理由: 実引数List<String>はメソッド呼出変換によってList<? super Object>に変換できません
  Tが型変数の場合:
    メソッド <T>fill(List<? super T>,T)で宣言されているT extends Object
エラー1個

A.foo()の返却値の推論で決定された型は、Objectになってしまい、List<String>は指定できない、と怒られてしまいます。
この場合、Java7までは、メソッドの呼び出し側に、

Collections.fill(a, A.<String>foo());

のように、型パラメーターを明示的に指定すれば、期待通りにコンパイルされます。


Java8では、このケースでも型推論が利くようになっています。


繰り返し注釈

Java8では、1つの要素に複数の注釈を設定することができるようになりました。


以下の例では、1つのプログラム要素(ここでは型=ElementType.TYPE)に対して、同じ注釈を2つ付けています。

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@interface AnnotationForType { 
    public String value();
}
@AnnotationForType("A")
@AnnotationForType("B")
class A { }

これをJava7でコンパイルすると、

$ javac repeating.java 
A.java:11: エラー: 注釈が重複しています
@AnnotationForType("B")
^
エラー1個
$ 

となり、コンパイルできません。


Java8では、注釈@Repeatableが新しく追加されました。これを使って配列を格納するための繰り返し注釈*1を定義することで、1つの要素に複数の同じ型の注釈が指定できるようになりました。
@Repeatableで繰り返し注釈を指定し、繰り返し注釈にはこの配列を返す値要素メソッド(valueメソッド)を宣言します。

次の例では、@AnnotationForTypeコンテナーとして@ArrayOfAnnotationForTypeを定義しています。 実行時に繰り返し注釈を参照するには、java.lang.reflect.AnnotatedElement#getAnnotationsByTypeを使います。

  • サンプル
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@interface ArrayOfAnnotationForType { 
    AnnotationForType[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ArrayOfAnnotationForType.class)
@Target({ElementType.TYPE})
@interface AnnotationForType { 
    String value();
}
@AnnotationForType("A")
@AnnotationForType("B")
class A {
    public static void main(String... args) {
        ArrayOfAnnotationForType[] a
            = A.class.getAnnotationsByType(ArrayOfAnnotationForType.class);
        for (ArrayOfAnnotationForType o : a)
            System.out.println(o);
    }
}
  • 実行結果
$ java A
@ArrayOfAnnotationForType(value=[@AnnotationForType(value=A), @AnnotationForType(value=B)])
$ 







*1:javacのメッセージではコンテナー注釈となっています。