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.ElementType
にTYPE_PARAMETER
とTYPE_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)]) $
- シリーズ目次
- Java SE 8 (1) - 概要と一覧
- Java SE 8 (2) - ラムダ式、メソッド参照、ストリーム
- Java SE 8 (3) - 新しい言語機能 (このエントリー)
- Java SE 8 (4) - 新しいAPIと改良されたAPI
- Java SE 8 (5) - プラットフォーム、セキュリティー、他