Hatena::ブログ(Diary)

プログラマーの脳みそ このページをアンテナに追加 RSSフィード

2018-02-14

ジェネリクスと配列

| 09:46 | ジェネリクスと配列を含むブックマーク

Javaジェネリクス一般配列と混ぜてはいけないとされるが、混ぜて用いた場合に何が問題となるのか。

歴史的な問題

Javaが1995年に登場した当時、Java配列はあったがジェネリクスはなかった。

ジェネリクスを含む型システムの理論的な整備は、1990年代から2000年代にかけてのJavaバージョンアップの時期に並行して行われていた。これは1995年当初のJavaになぜより良いジェネリクスを搭載した形でリリースされなかったのか?ということにひとつの答えを示すだろう。つまり、1995年当時にはジェネリクス(Java5に搭載されたような変性を含むもの)は未来の技術であって、まだ理論的に固まっていないものであった、というわけだ。

Java言語仕様にも記述されているが

Historically, wildcards are a direct descendant of the work by Atsushi Igarashi and Mirko Viroli. Readers interested in a more comprehensive discussion should refer to On Variance-Based Subtyping for Parametric Types by Atsushi Igarashi and Mirko Viroli, in the Proceedings of the 16th European Conference on Object Oriented Programming (ECOOP 2002). This work itself builds upon earlier work by Kresten Thorup and Mads Torgersen (Unifying Genericity, ECOOP 99), as well as a long tradition of work on declaration based variance that goes back to Pierre America's work on POOL (OOPSLA 89).

Javaジェネリクスに現れるワイルドカード、そしてそれによって表現される変性については2002年のAtsushi Igarashi と Mirko Viroliの論文が元になっている。なお、Java5 がリリースされたのは2004年9月30日のことであった。

Java配列にはC言語配列の影響が色濃く見える。配列を表す[]が型の側にも変数の側にもつけて宣言できるのもその影響の一端であろう。

String[] array1;
String array2[];

配列のString型はString型ではない。StringはStringを間接的に扱う。型を間接的に扱うのだから、ジェネリクス的である。C言語配列が作られたとき、配列ジェネリクスであるという理解でもって設計されたわけではなかろう。しかし、現代から振り返ってみれば、配列は言語に組み込みの機能を限定したジェネリクスである、といえる。

配列ジェネリクスのようなもの

Javaでは参照型は親の型の変数に子の型を代入することはキャストなしに行うことができる。

Object o = new String("hoge");

これはリスコフの置換原則(Liskov, Barbara; Wing, Jeannette 1993年7月16日 Family Values: A Behavioral Notion of Subtyping)として知られる。

ごく端的に言えば、子は親の機能を代替できなくてはならない。代替できる前提において、子を親の型扱いすることができる。こうした変数の型に対して子や孫の型を代入できる関係を「共変性」(covariance)という。Javaの型システムの根幹をなす重要な概念だ。

しかし、型を間接的に用いるジェネリクスでは、変数を単純に共変とすることができない。

ArrayList<String> stringList = new ArrayList<String>();
ArrayList<Object> objectList = new ArrayList<Object>();
objectList = stringList; // コンパイルエラー

なぜか。

objectList = stringList; // 仮に出来たとする
objectList.add(new Object()); // stringList にObjectが格納される

ArrayList<Object>は

  • get すると Object が返る
  • Object を add できる

ArrayList<String>は

  • get すると String が返る
  • String を add できる

ArrayList<String>はgetするとStringを返すわけだが、これはObjectであるわけで、ArrayList<Object>のgetの機能を満たす。しかし、ArrayList<String>はStringしかaddすることができず、Objectをaddすることができない。

つまり、ArrayList<String>はArrayList<Object>が提供する機能をすべて代替できていない。なので子になることはできない。このため、ArrayList<String>のようなパラメタライズドタイプ(parameterized type)は通常の変数の共変性とは異なり非変性とされているのである。Java5でジェネリクスが導入されたことにより、Java言語は変数の変性について複雑さを増した。

「型を間接的に用いるジェネリクスでは、変数を単純に共変とすることができない」と言った理由はここにある。そして配列もまたジェネリクスのようなものであった。なので同じ話題がある。

String[] stringArray = new String[10];
Object[] objectArray = new Object[10];
objectArray = stringArray; // コンパイルエラーにはならない
objectArray[0] = new Object();

上記コードはコンパイルエラーにはならないが、実行すると以下のように例外が出る

Exception in thread "main" java.lang.ArrayStoreException: java.lang.Object

この例外については、出ることが保証されており、1995年当時にJavaの実装者がこの問題を知らなかったわけではない。

しかし、ジェネリクスの変性についての論文が2002年なのであって、1995年当時のJavaに、配列型の変数にだけ共変性を持たせないという言語仕様を求めるのは時系列的に無茶というものだろう。

かくしてJava変数は、通常の参照型も配列型も共変性となった。(さらに言えば配列型もObject型に代入可能だったりするのでなお配列型だけ共変としないというわけにはいかなかったのだろう)

Javaの型システムはこの点で欠陥をはらんでいるのである。

配列の構文

枕の話が長くなったが、配列というのは間接的に型を扱うものであるからして、ジェネリクス的なものである。しかし、時系列的な都合もあり、Java配列ジェネリクスに統合されていない。Javaの型システムには配列ジェネリクスという似たような別物が含まれることとなった。より後発のScalaなどでは配列ジェネリクスに統合されている。

というわけで、Javaではジェネリクス配列を混ぜて使うとよろしくない。しかし、じゃあ具体的にどうよろしくないのだろうか?筆者も「混ぜて使うな!」までで話を終わらせてしまってばかりで具体的にどうよろしくないか、までは深入りしたことはなかった。本稿はそれを探るというもので、実用上は「混ぜて使うな!」で十分である :-P

まず、JavaのList<String>のようなパラメタライズドタイプ(parameterized type)は配列にして変数宣言することができる。

List<String>[] listStringArray;

このあたりの根拠Java言語仕様から探すとなかなか大変なのだが、概ね 8.3. Field Declarationsのあたりに記載があって、大雑把に構文を抜粋すると

FieldDeclaration:

 {FieldModifier} UnannType VariableDeclaratorList ;


UnannType:

 UnannPrimitiveType

 UnannReferenceType


UnannReferenceType:

 UnannClassOrInterfaceType

 UnannTypeVariable

 UnannArrayType


UnannClassOrInterfaceType:

 UnannClassType

 UnannInterfaceType


UnannClassType:

 Identifier [TypeArguments]

 UnannClassOrInterfaceType . {Annotation} Identifier [TypeArguments]


UnannArrayType:

 UnannPrimitiveType Dims

 UnannClassOrInterfaceType Dims

 UnannTypeVariable Dims


Dims:

 {Annotation} [ ] {{Annotation} [ ]}

要するに、UnannArrayType つまり配列は UnannClassOrInterfaceType (クラスorインターフェース)に [] をつけることが許されていて、UnannClassOrInterfaceType は UnannClassType (クラス)とかで、UnannClassType は

Identifier [TypeArguments] と識別子に加え型引数をつけることが許されている、つまり、パラメタライズドタイプを配列にして変数宣言することが構文上できる。[ ]は任意の意味合いだ。

ところが、配列をnewするときの右辺側が問題である。

FieldDeclaration:

 {FieldModifier} UnannType VariableDeclaratorList ;


VariableDeclaratorList:

 VariableDeclarator {, VariableDeclarator}


VariableDeclarator:

 VariableDeclaratorId [= VariableInitializer]

この = VariableInitializer の部分が変数初期化子で

VariableInitializer:

 Expression

 ArrayInitializer

Expression つまり式か、ArrayInitializer 配列初期化子を置くことができる。

まずはExpressionから見てみよう。15.8. Primary Expressionsに定義が載っていて

Primary:

 PrimaryNoNewArray

 ArrayCreationExpression

ここでは ArrayCreationExpression 配列生成式を見ていく。15.10.1. Array Creation Expressionsが該当の節だ。

ArrayCreationExpression:

 new PrimitiveType DimExprs [Dims]

 new ClassOrInterfaceType DimExprs [Dims]

 new PrimitiveType Dims ArrayInitializer

 new ClassOrInterfaceType Dims ArrayInitializer


DimExprs:

 DimExpr {DimExpr}


DimExpr:

 {Annotation} [ Expression ]

ここで、単に new ClassOrInterfaceType DimExprs [Dims] とあるので、パラメタライズドタイプの配列宣言できそうに見えるが、注釈がついている。

The rules above imply that the element type in an array creation expression cannot be a parameterized type, unless all type arguments to the parameterized type are unbounded wildcards.

パラメタライズドタイプは駄目、変数は駄目、ワイルドカードは駄目、というわけである。つまり、これらはnew で配列を生成できない。

2018.02.15 追記

指摘いただいて気付いたが盛大に誤訳していた。「全ての型変数境界を持たないワイルドカードでない限りnewできない」と訳すべき内容で、例えば

ArrayList<?>[] list = new ArrayList<?>[10];

といったコードはコンパイル可能。これは恐らくJava5以前のraw型において

ArrayList[] list = new ArrayList[10];

が可能なことに対応付けられる措置であると思う。

追記ここまで


というわけでVariableInitializerの定義まで戻って

VariableInitializer:

 Expression

 ArrayInitializer

今度は ArrayInitializer について見ていこう。10.6. Array Initializers

ArrayInitializer:

 { [VariableInitializerList] [,] }


VariableInitializerList:

 VariableInitializer {, VariableInitializer}


VariableInitializer:

 Expression

 ArrayInitializer

Javaの構文の細かい所ではあるが、配列初期化にはいくつか方法があって

String[] array = new String[10];

といった場合は先の ArrayCreationExpression 配列生成式となる。ArrayInitializer 配列初期化子は

String[] array = {"hoge", "piyo"};

といった場合の {} の部分で、これは変数宣言時のみ使用可能で、一般的な式としては用いることができない。

さてこの配列生成式だが、この節にはパラメタライズドタイプの配列生成式が駄目ということは書かれていない。しかし、冒頭に

An array initializer may be specified in a field declaration (§8.3, §9.3) or local variable declaration (§14.4), or as part of an array creation expression (§15.10.1), to create an array and provide some initial values.

といった記述があり、先のArrayCreationExpression 配列生成式などの一部として用いるものである、とあるのでそちらに準じるのだろうか。

言語仕様上、ArrayInitializer 配列初期化子でパラメタライズドタイプが用いれないことについては根拠がはっきりしなかったが、Oracle JDK 9.0.4 を用いて確認を行ったがコンパイルエラーとなることは確認できた。言語仕様の記述の対応を探すのは今後の課題である。


なお、配列生成式でパラメタライズドタイプの配列を作ろうとした場合は以下のようなコンパイルエラーが出る。参考まで。

Hoge.java:44: エラー: 汎用配列を作成します

List<String>[] listStringArray = {};

パラメタライズドタイプの配列生成方法

こうしたことから、パラメタライズドタイプの配列変数型としては宣言できるものの、配列インスタンス生成は行えないように見える。しかし、実際にはインスタンス生成をする抜け道がひとつある。可変長引数である。

static <T> T[] toArray(final T ... ts) {return ts;}

このようなメソッド宣言して

List<String> list = new ArrayList<>();
list.add("hoge");
List<String>[] listStringArray = toArray(list, list, list);

System.out.println(listStringArray.length);
System.out.println(listStringArray[0]);

このように用いるとパラメタライズドタイプの配列インスタンスを作ることができる。

配列の共変性に関わる問題

さて、Java配列は歴史的経緯から共変な変数であることは冒頭述べた。

パラメタライズドタイプの場合、これはどうなるか。

List<String>[] listStringArray = toArray();
List<Object>[] listObjectArray = toArray();
listObjectArray = listStringArray; // コンパイルエラー

これは要するに List<Object> に List<String> が代入できないため、 List<Object> にも List<String>が代入できないのである。これだと一見うまくいっているように見える。しかし

List<String>[] listStringArray = toArray(new ArrayList<String>());
List<? extends Object>[] listObjectArray = toArray(new ArrayList<Object>());
listObjectArray = listStringArray; // OK
listObjectArray[0] = new ArrayList<Object>();

List<String>を代入可能なList<? extends Object>の配列を用意すると、この代入可能に引きずられてList<? extends Object>にList<String>が代入できてしまう。ここまでは配列と同等なのだが、イレイジャの都合もあって実行時にArrayStoreExceptionが出ない。

とはいえ、List<? extends Object>であるためadd(new Object());はコンパイルエラーとなる(この理由は拙稿 ジェネリクスの代入互換のカラクリ あたりを参照して欲しい。ワイルドカードは入出力を制限することで共変性や反変性をもたせることが出来る)

listObjectArray[0].add(new Object()); // コンパイルエラー

基本的に、配列の共変性を引きずるので、X に Y が代入可能なら X に Yが代入可能と判断される。

変数の場合は境界によって代入可能となっていれば同じように配列も代入可能となる。

static <T1, T2 extends T1> void foo() {
	T1[] t1a = null;
	T2[] t2a = null;
	t1a = t2a;
}

特に使い道は思いつかないが。

追記 配列をバインド

逆パターンを書き忘れていたので追記。

変数配列をバインドすることはできるか? できるのである。

List<int[]> x = new ArrayList<int[]>();
x.add(new int[] {1, 2, 3});
int[] intArray = x.get(0);

Java9時点では型変数にプリミティブ型をバインドすることは出来ないが、int[]は配列なので参照型である。参照型だからバインドすることができる。

しかし、配列の共変性は相変わらず問題になる。

List<Object[]> y1 = new ArrayList<Object[]>();
List<String[]> y2 = new ArrayList<String[]>();
y1 = y2; // NG
y1.add(new String[] {});

y1 = y2 はパラメタライズドタイプの非変性によってコンパイルエラーとなる。しかし、y1.add(new String[] {})は配列の共変性によりコンパイルエラーとはならない。

むしろプリミティブ型配列であれば共変性の問題が無い分だけ安全かもしれない。共変性が問題となるケースに気をつければ挙動自体には問題はない。どうしても使いたいケースが生じた場合は十分に注意されたし。

まとめ

言語仕様から感じられることとしては、パラメタライズドタイプの配列や、型変数配列ワイルドカード配列は基本的に使わせたくないのだろう。

トラックバック - http://d.hatena.ne.jp/Nagise/20180214

2017-12-24

Java Generics Hell - new T()

| 23:52 |  Java Generics Hell - new T()を含むブックマーク

Java Generics Hell アドベントカレンダー 24日目。

読者の推奨スキルとしてはOCJP Silverぐらいを想定している。

今回はJavaジェネリクスでは型変数を用いてnew T()できないけど特に問題ないという話。

コンストラクタの形

コンストラクタは、その内部でthisが使えるしインスタンスフィールドへのアクセスもインスタンスメソッドへのアクセスも出来る。そこからするとインスタンススコープのメソッドのように見えるが、インスタンスに所属しているわけではない。


インスタンスメソッドを呼び出すにはインスタンスを指定するか、自分のインスタンスメソッド呼び出しでthis.を省略できる、というシチュエーションでなくてはならない。逆に言えば通常のclassのnewはインスタンスなしで呼び出せるわけで、所属としてはclassのstaticに相当する。クラス名を指定する必要があるが、インスタンスを指定する必要はない、という独特のポジションである。


なので、継承を用いてinterfaceや親クラスによってコンストラクタ引数の形を規定することは出来ない。通常の継承の埒外にいるわけである。コンストラクタはリスコフの置換原則の範囲外で、親クラスのコンストラクタと同様に子クラスのコンストラクタが呼べる必要はない。


こうした前提を踏まえると、型変数のような任意の型に対して、統一的なnewをしたいというのはJavaの型システムの範疇で考えると無理であるし、無茶な要求と言える。Javaあたりの世代プログラム言語ではコンストラクタ引数がどのようなものか、指定することが出来ないし、指定したければ継承の範囲を超えた別のプログラミングパラダイムが必要になる。


デフォルトコンストラクタ

さて、そんなわけで汎用にコンストラクタの形状を定めるというのは無理で、どうしてもやりたければ、インスタンスを生成するクラスというものを用意するという迂遠なやり方をする必要がある。迂遠というか面倒くさいわけだが、こうした生成を行うクラスを別途用意する(ビルダークラスとかファクトリークラスという呼ばれ方をする)ことで対処できるといえば出来る問題である。いわゆるボイラープレートというやつで、定型句がわさわさ出てきて煩わしいという話はあるにせよ、だ。


しかし、特定のシチュエーションでは決まったコンストラクタの型をしているという前提を置くことが、ある程度妥当性を持つことがある。デフォルトコンストラクタである。

インスタンスの生成を行うメソッド

デフォルトコンストラクタとは要するに引数なしのコンストラクタで、単なるデータを保持するようなクラスの場合、概ねデフォルトコンストラクタを持つという前提を想定してよいケースがある。例えばO/Rマッパーのようなケースで、所定の型のデフォルトコンストラクタインスタンスを作ってリフレクションでデータを詰めて返したい、そのメソッドジェネリクスで具象型を指定したい、というわけである。

public class ORMapper {
	public <T> T select(String query) { ... } // これでは生成できない
}

この場合、Javaでやるなら先に挙げたようにビルダークラスを使う必要がある。そして、Java8以降を前提とするならば、ビルダークラスはラムダ式ないしメソッド参照でよい。引数java.util.function.Supplier<T>を用いよう。

public class ORMapper {
	public <T> T select(Supplier<T> builder, String query) {
		T ret = builder.get();
		// (略) 詰め込む処理
		return ret;
	}
}

こうした場合、呼び出し側は

ORMapper mapper = new ORMapper();
Hoge hoge = mapper.select(Hoge::new, "select * from hoge");

といった具合である。今後のJavaのリリースで変数宣言時の型推論の導入が検討されている。それが導入されると次のように書けるようになるだろう。

ORMapper mapper = new ORMapper();
var hoge = mapper.select(Hoge::new, "select * from hoge");

C#の場合

さて、引き合いによく出されるC#では実行時にバインドされた型変数の型が引き回される。そして、new制約というデフォルトコンストラクタを持つことを型変数の制約とする特殊機能を使って(型クラスのような汎用機能ではないので過渡期の対処法だと思っておいた方が良いと思う)型変数でのnewを実現している。

https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/new-constraint

class ItemFactory<T> where T : new()
{
    public T GetNewItem()
    {
        return new T();
    }
}

ここでさっきのJavaのORMapperの例をC#にしてみると

public class ORMapper {
	public T Select<T>(String query) where T : new()
	{
		T ret = new T();
		// (略) 詰め込む処理
		return ret;
	}  
}

といった形になるだろう(筆者はC#は不案内なので誤りがあればご指摘願いたい)

この時、呼び出し側は

ORMapper mapper = new ORMapper();
var hoge = mapper.select<Hoge>("select * from hoge");

といった形になるだろう。

Java版(変数宣言時の型推論あり)と比較してみよう。

ORMapper mapper = new ORMapper();
var hoge = mapper.select(Hoge::new, "select * from hoge");

違いとしては、C#側ではメソッドスコープの型変数に対するバインドを明記する必要があり、代わりにnew制約を使うので引数にビルダークラスを取らなくて良い。Java側ではメソッドスコープの型変数型推論で済ませる代わりにnew制約がないため引数Hoge::newを渡す必要がある。概ね、Hogeの位置が変わる程度で大差がない。

まとめ

  • new制約が便利に見えるが、言うほど劇的な利便性が得られるわけでもない
  • Javaでnew制約の代わりにTから実行時の型をとってリフレクションでnewInstance()しようというのは筋の悪いアプローチ
  • 筋の悪いアプローチをしようとして出来ないからといってイレイジャに八つ当たりするのは見当違いではないか

イレイジャのせいにする言説もしばしば見かけるが、いささか見当違いと言えるだろう。何を問題視しているか冷静に検討したいところだ。

トラックバック - http://d.hatena.ne.jp/Nagise/20171224

2017-12-22

Java Generics Hell - イレイジャ

| 23:02 |  Java Generics Hell - イレイジャを含むブックマーク

Java Generics Hell アドベントカレンダー 22日目。

読者の推奨スキルとしてはOCJP Silverぐらいを想定している。

メソッドオーバーロード

Javaジェネリクスのイレイジャについて語るには、まずメソッドのオーバーロードについて語らねばなるまい。

メソッドのオーバーロードとは、同名で引数の型違いのメソッドのことである。

public class Hoge {
	public void foo() {}
	public void foo(String s) {}
}

Javaではなぜ同名のメソッド宣言することが出来るのであろうか?コンパイラが、あるいはJavaランタイムであるJavaVMが、メソッドを特定するときに

によってどのメソッドを呼ぶか特定しているのである。

これはリフレクションAPIにも現れていて、Methodを取得しようとした場合には

Class<Hoge> clazz = Hoge.class;
Method method = clazz.getMethod("foo", String.class);
System.out.println(method);

といったように

とする必要がある。

プログラム言語によってはオーバーロードを許していないものもある。オーバーロードを許さない言語では、メソッドを特定するのは楽で、

だけが分かればよい。メソッドを呼び出すためにいちいち引数のクラスの羅列を用意する必要はなくなる。

イレイジャ

さて、オーバーロードについて振り返った。ジェネリクスのイレイジャを理解するにはオーバーロードについて振り返っておく必要があるのだ。Javaではメソッドを特定するために

が必要だと言ったが、この「引数の型の並び」はイレイジャである必要がある。

ここで、イレイジャの定義についてJava言語仕様より抜粋してみよう。

4.6 型のイレイジャ

型のイレイジャ(type erasuer:型消去)とは,型(パラメータ化型や型変数を含む)から型(パラメータ化型や型変数を含まない)への対応付けである。型Tのイレイジャは|T|と表記される。イレイジャの対応付けは以下のように定義される。

パラメータ化型(§4.5) G<T1, …,Tn>のイレイジャは|G|である。

・ネストされた型T.Cのイレイジャは|T|.Cである。

配列型Tのイレイジャは|T|である。

・型変数(§4.4)のイレイジャは,その最も左端の境界におけるイレイジャである。

・その他の型すべてのイレイジャは,その型自身である。

メソッドシグネチャsのイレイジャは,sと同じ名前,およびs中で指定されたすべての形式的パラメータ型のイレイジャからなるシグネチャとなる。

パラメータ化型」という単語が出てくるが本稿シリーズでは「パラメタライズドタイプ」と呼んでいる。つまり、List<String>のような型のことである。なので、先程のメソッドを特定するための条件は正確には

  • 属するクラスの完全修飾名
  • メソッド
  • 引数の型のイレイジャの並び

ということになる。

このことによる直接的なデメリットは、引数のイレイジャが同じになるメソッドオーバーロードが許されないということである。

これがもし、メソッドを特定するために

となっていれば、引数の型のさらにパラメータの違いによってメソッドの区別が出来ることになる。もちろん、そのためにコンパイラランタイムはいちいちメソッドを特定するためにそれだけの情報を参照する必要が生じるということでもある。

歴史的な経緯を言えば、Javaは1.4から5の間でジェネリクスが追加されたのであった。この時、メソッドを特定する仕組みを旧来のものと互換をとった。それがイレイジャ方式というわけである。

イレイジャ方式のメリットは、互換性のキープもあるが、リフレクションなどで扱う際に煩雑になりすぎない点がある。これは実行時の型エラーと表裏一体のものではあるが、JavaVM上でより拡張された型システムを構築する場合(つまりそのような拡張された言語を作るような場合)にやりやすいということでもある。

よくある誤解

Javaメソッドの特定の際にイレイジャを用いて特定するということは前節で述べたが、Javaclassファイルに「イレイジャしか格納されていない」わけではない

これは、引数にList<String>を取るメソッドがあったとして

public class Hoge {
  public void foo(List<String> list) {}
}

これをコンパイルしてHoge.classファイルを作る。

次に、このHoge.classファイルだけをもってきてクラスパスを通し、別のクラスからこのメソッドfooを呼び出す。

public class Main {
  public static void main(String[] args) {
    Hoge hoge = new Hoge();
    List<String> list = new ArrayList<String>();
    hoge.foo(list);  // OK

    List<Integer> list2 = new ArrayList<Integer>();
    hoge.foo(list2);  // NG
  }
}

この時、hoge.foo(list);はコンパイルが通るとして、パラメタライズドタイプのパラメータが異なるhoge.foo(list2);はコンパイルエラーとなる。classファイルにfooの引数がList<String>のイレイジャ|List|だけが残されているのだとしたら、引数に誤ってList<Integer>を渡そうとした場合にコンパイルエラーに出来ないはずである。

しかし、現実にはコンパイルエラーになる。classファイルにはしっかりとパラメタライズドタイプのパラメータは保持されているし、リフレクションでこのパラメータを読み出すことも出来る。

2017-12-20

Java Generics Hell - ブリッジメソッド

| 23:37 |  Java Generics Hell - ブリッジメソッドを含むブックマーク

Java Generics Hell アドベントカレンダー 20日目。

読者の推奨スキルとしてはOCJP Silverぐらいを想定している。

共変戻り値

Java5以降ではメソッドをオーバーライドするときに、戻り値をより具体的な型としてオーバーライドすることが許されている。

public interface Parent {
    Number getValue();
}

このjava.lang.Number型を返すgetValue()メソッドをChild型でオーバーライドするときにNumberの子であるInteger型にすることができる。

public class Child implements Parent {
    public Integer getValue() {
        return 0;
    }
}

これが共変戻り値だ。

ジェネリクスを用いている場合に、継承でバインドすると同様の共変戻り値となることがある。

public interface Parent<T> {
    T getValue();
}
public class Child implements Parent<Integer> {
    public Integer getValue() {
        return 0;
    }
}

bridge メソッド

これらのケースで、Child型のclassファイルを覗くと面白いものが見える。

>javap Child.class

Compiled from "Child.java"

public class Child implements Parent<java.lang.Integer> {

public Child();

public java.lang.Integer getValue();

public java.lang.Object getValue();

}

getValue()がふたつあるのが分かるだろうか。Eclipseclassファイルビューアだと以下のように見える。

// Method descriptor #24 ()Ljava/lang/Object;

// Stack: 1, Locals: 1

public bridge synthetic java.lang.Object getValue();

0 aload_0 [this]

1 invokevirtual Child.getValue() : java.lang.Integer [25]

4 areturn

Line numbers:

[pc: 0, line: 1]

bridge syntheticと記載があるのが分かるだろうか。これが今日のテーマ、ブリッジメソッド(bridge method)だ。

引数のブリッジ

さて、共変戻り値の例を挙げたが、型変数引数にとる場合でも同様の事例が発生する。

public interface Parent<T> {
	void xxx(T t);
}
public class Child implements Parent<String> {
	@Override
	public void xxx(String t) {
	}
}

ここで型変数Tは境界がないので、Parentのメソッドxxxの実際のシグニチャはxxx(Object)となっている。

Childでは型変数継承でバインドしてStringとしているので、Parentのメソッドxxx(Object)をオーバーライドしてxxx(String)としてしまっている。

Javaでは呼び出すメソッドを決めるときに「メソッド名」と「引数の型」が必要だった(より正確には引数の型のイレイジャとなる。拙稿贖罪のイレイジャを参照されたし)。「メソッド名」が同一で「引数の型」が異なる場合、オーバーロードとなることはJavaの初歩で習うことだ。しかし、ここではxxx(Object)とxxx(String)となっている。

そこで、こうしたケースで、もとのメソッドシグネチャであるxxx(Object)を「ブリッジメソッド」として、xxx(String)を呼び出す実装をコンパイラが作り出す。

リフレクション

こうしたブリッジメソッドはリフレクションでメソッド一覧を取得したようなときにも現れる。

そこでjava.lang.reflect.Methodクラスにはブリッジメソッドであることを判別するためのisBridge()が用意されている。

また、似たような話題としてisSynthetic()というメソッドも用意されている。これは合成(synthetic)されたメソッドにつくフラグで、このブリッジメソッドの他にもコンパイラによって生成されたメソッドに立てられる。

Java言語仕様第3版 13.1 Javaバイナリの形式 (299ページ)から抜粋すると

コンパイラによって導入された,ソースコード中に対応する構造のない任意の構造は,デフオルトのコン

ストラクタとクラス初期化メソッドを除いてすべて,合成(synthetic)されたものである旨記録されなけ

ればならない。

と書かれている。先のclassファイルビューアにあったbridge syntheticの記載の意味はコンパイラによって作られたブリッジメソッドというフラグだったというわけだ。

ブリッジメソッドの衝突

ところでブリッジメソッドが衝突するとどうなるのか?

public interface Parent<T> {
	void xxx(T t);
	void xxx(String s);
}
class Child implements Parent<String> {
	@Override
	public void xxx(String t) {
	}
}

この場合はこのままコンパイルに成功する。ちょっと面白い現象だ。

まとめ

2017-12-19

Java Generics Hell - 内部クラスと型変数のスコープ

| 20:04 |  Java Generics Hell - 内部クラスと型変数のスコープを含むブックマーク

Java Generics Hell アドベントカレンダー 19日目。

読者の推奨スキルとしてはOCJP Silverぐらいを想定している。

Javaでは1ファイルにトップレベルのpublicなclassはひとつしか置けないが、入れ子になったクラスなどを用意することができる。種類がいくつかあるので後に整理するが、内部クラスの場合、外側のインスタンススコープの型変数が内部クラスの内側でも有効となる。

今回はそのあたりを整理してみよう。Javaのクラス宣言の種類については拙稿 Javaのクラス宣言5種+αで以前に取り上げているので参考にされたい。

static

内部クラスの話の前に、そもそも論としてクラスのインスタンススコープの型変数は、staticなメソッドや、フィールドでは用いることは出来ない。なぜなら、インスタンスをスコープとしているからだ。(トートロジー的な)

なお、コンストラクタインスタンスメソッド扱いで型変数を使える。その他、static初期化ブロックはstatic扱いなので型変数を使えない。インスタンス初期化ブロックであればインスタンスの型変数を用いることができる。

トップレベルの同居したクラス

Javaではpublic/protectedなトップレベルのclassは .java ファイルにひとつしか置けないが、パッケージプライベートなclassは複数置くことができる。なお、privateなトップレベルクラスはそもそも作れない。例えばTopLevel.javaというファイルに以下の内容を記述するとコンパイルすることが出来、TopLevel.class と TopLevel2.class が作られる。

public class TopLevel<T> {
}
class TopLevel2 {
}

上記例のTopLevel2はトップレベルなので、当然ながら相互に型変数のスコープは独立している。つまり、考慮しなくて良い。

ネストしたクラス

ネストしたクラス (Nested class) は、class 内部に

である。

これらは、名前が「外側のクラス名. ネストしたクラス名」で扱われるが、基本的にトップレベルクラスと同等である。

public class TopLevel<T> {
	static class Nest1 {
	}
	interface Nest2 {
	}
	enum Nest3 {
	}
}

違いといえば、アクセスレベルをパッケージプライベートやprivateにすることができる。private宣言すると内部的にclassを作るものの、外部に露出させないことが可能だ。

これらもトップレベルクラスと同等で、外側のTopLevelの型変数Tをこれらネストしたクラス内で用いることは出来ない。

内部クラス

内部クラス(Inner class)は外部クラスのインスタンスに紐付く。そのため、外側のクラスのインスタンスの型変数を用いることができる。

public class TopLevel<T> {
	class Inner {
		T t;
		void hoge(T t) {}
	}
}

しかし、内部クラスといえども、内部クラスのstaticメソッドではTopLevelのTを扱うことは出来ない。

なお、内部クラスが3段以上になった場合、3段目では、1番外側のクラスで宣言された型変数、2段目で宣言された型変数の両方を用いることができる。実用上はそういうケースはなかなか発生しないとは思うが。

ローカル内部クラス

ローカル内部クラス(Local inner class)はメソッドコンストラクタなどのブロック内で宣言されるクラス。このクラスはブロックがスコープとなるのだが、そのブロックで有効な外側の型変数はこのクラス内でも用いることができる。

public class TopLevel<T> {
	/** staticメソッド */
	static <S> void staticMethod() {
		class LocalInnerClass {
			S s; // メソッドスコープのSは使える
			// T t; インスタンススコープのTはNG
		}
	}
	/** インスタンスメソッド */
	<I> void instanceMethod() {
		class LocalInnerClass {
			I i; // メソッドスコープのIが使える
			T t; // インスタンススコープのTも使える
		}
	}
}

上記例では、staticメソッドではメソッドスコープのSは使えるが、インスタンススコープのTは使えない。対して、インスタンスメソッドでは両方を用いることができる。

無名クラス

無名クラス(匿名クラスとも呼ばれる)(Anonymous class)は主に抽象クラスやinterfaceなどに対して名前を付けずにその場で実装を書くもの。

これもローカル内部クラス同様にそのブロックで有効な外側の型変数はこのクラス内でも用いることができる。

public class TopLevel<T> {
	/** staticメソッド */
	static <S> void staticMethod() {
		Runnable run = new Runnable(){
			S s; // メソッドスコープのSは使える
			// T t; インスタンススコープのTはNG
			@Override
			public void run() {
				S s2;
			}
		};
	}
	/** インスタンスメソッド */
	<I> void instanceMethod() {
		Runnable run = new Runnable(){
			I i; // メソッドスコープのIが使える
			T t; // インスタンススコープのTも使える
			@Override
			public void run() {
				I i2;
				T t2;
			}
		};
	}
}

余談

ローカル内部クラスや無名クラスの中に内部クラスを作ると、外側のブロックで有効なメソッドスコープの型変数を内部クラスで用いることができる。

public class TopLevel<T> {
	/** staticメソッド */
	static <S> void staticMethod() {
		/** ローカル内部クラス */
		class LocalInnerClass {
			/** 内部クラス */
			class InnerClass {
				S s;
			}
		}
	}
}

内部クラスは、外側のクラスで利用可能な型変数は使える、ぐらいに理解しておくと良いだろう。え?こんな使い方することはないって?

Connection: close