Java Generics Hell - 共変ワイルドカード
Java Generics Hell アドベントカレンダー 13日目。
- 前回(11日目) 型変数の境界
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
変性
パラメタライズドタイプについては以前に書いたが、ワイルドカードについては後回しということにしていた。
<>の内部については非変性であるということは何度か述べてきたが、特定の制限下で共変性、あるいは反変性をもたせることは出来るだろうか?
共変性
パラメタライズドタイプが共変である場合、何が問題になるのだったか再確認しよう。以下ではclass Aを親とし、class Bが子供という関係とする。パラメタライズドタイプを共変とするとき? extends を用いて以下のように記述する。
List<B> listB = new ArrayList<B>(); List<? exnteds A> listExA = listB; // 代入可能
ここでList<B>型の機能性について確認しておこう。
- listB は B型を格納することが出来る
- listB から B型を取り出すことができる
比較用に List<A>型のlistAがあったとして、この機能性についても確認しよう。
- listA は A型を格納することが出来る
- listA から A型を取り出すことができる
となるのであった。問題となるのはA型を格納の部分で、リスコフの置換原則に従うならList<A>型を継承するならList<A>型の機能すべてを子は満足させないといけないのであった。List<B>はList<A>の機能を満足させることができない。B型しか格納することが出来ず、A型を格納できないのだから。よってList<B>型をList<A>型の子として暗黙に代入可能としてしまうと問題がおきるのであった。
ここで、制約、つまり、格納する機能をそもそも使わないという前提をおいたら整合性が取れるのではないか?つまり
- listExA は格納することが出来ない
- listExA から B型を取り出すことができる
という条件であれば、listBをlistExAに代入できても矛盾は生じない。
格納することが出来ないとはどういうことか?これは引数に型変数が用いられているものを用いないということだ。例えばjava.util.Listであれば
public boolean add(E e) { ... }
は引数に型変数Eが用いられているので listExA.add( ... ) を使わせなければいい。
矛盾するものを使わせなければ矛盾しないということになる。
キャプチャ
さて、おおまかな方針は述べたものの、実は多分に方便が混ざっていて正確ではない。概要を踏まえて詳細に入ろう。
List<? extends A>型では型変数Eは? extends Aとなる。Aというわけではない。この? extends Aはキャプチャ(capture)と呼ばれる。このキャプチャという用語、コンパイルエラーで見かけることがある。
The method add(capture#1-of ? extends A) in the type ArrayList
is not applicable for the arguments (A)
キャプチャ ? extends Aの性質は以下のようなものになる。
- A型 を ? extends A に変換できない
- ? extends A はA型に暗黙に安全に変換できる
このため、List<? extends A>型のadd(E)はEにキャプチャ? extends Aがバインドされ、addの引数に? extends A型に変換可能なものしか受け付けない。しかし、A型は? extends Aに変換できないのでaddの引数にA型は渡せない、というわけだ。ただし、add()に何も渡せないかというと例外がある。
? extends A にキャスト可能なのはnullリテラルだ。前回 nullは全ての型にキャスト可能という話をしたが、キャプチャに対してもキャスト可能である。なのでadd(null)だけは可能となっている。
ここで、? extends A はA型に暗黙に安全に変換できるので、getについては
A a = listExA.get(0);
といった形で使用することができる。
共変ワイルドカードの意義
List<? extends A>型にはList<A>型やList<B>型を代入することが出来る。これをメソッドの引数に用いるとどうなるか。
public void hoge(List<? extends A> list) { ... }
このようにすると、hogeメソッドにはList<A>を渡すこともList<B>を渡すこともできるようになる。
hogeメソッド内部では、get()を用いることはできても、add()などで引数のListに変更を加えることはしない。
メソッド内部では、共変ワイルドカードで渡されたオブジェクトから値を取ることが出来る。
これにより、引数の型を柔軟にしつつ、型の安全性を保つことができる。これが ? extends Aの重要な意義である。
Java Generics Hell - 型変数の境界
Java Generics Hell アドベントカレンダー 11日目。
- 前回(8日目) インスタンススコープのジェネリクス
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
週末サボりました。すいません。
型変数の境界
ここまでは話を簡単にするために型変数の境界については触れずにきた。
しかし、型変数の境界についてはそこまで難しい話ではない。その型変数が何かを継承していることを制約としてつけることができる、というだけである。
public class Hoge<T extends Piyo> { }
ここで、TはPiyo型かもしくは、Piyo型を継承した何かでなくてはならない。この時、class Hogeの中で型変数T型の変数はPiyoを継承していることが保証されるので、Piyoに定義されるメソッドを呼び出すことができる。これを型変数の境界という。なお、指定できるのはextendsだけである。
public class Piyo { public void xxx(); } public class Hoge<T extends Piyo> { T piyo; public void foo() { piyo.xxx(); // ← Piyo#xxx()が使える } }
この場合、Hoge型を外から見た場合は(パラメタライズドタイプの場合のこと)、TはバインドされたPiyoの具象型として見える。
対して、Hoge型の内部としては、Tはあくまで「Piyoを継承した何か」だと思って扱わなければならない。
Tが具体的に何の型であるかinstanceofして分岐したい、と考えたのなら、それは設計がマズいと考えるべきである。端的にはうまく抽象化が出来ていないということだ。
interfaceの多重実装
Javaはclassの単一継承、interfaceの多重実装、という継承ポリシーだが、ジェネリクスで継承階層を考える際はclassかinterfaceかは区別する必要はほぼない。継承に際してclassの宣言時、interfaceの宣言時の書き方が異なるからか、この点で余計な混乱をしている人を見かけたので敢えて書くが、「継承階層」を考える際は一緒くたでいい。一緒くたで考えるとJavaの継承階層というのは、多重継承ということになる。
型変数の境界には、複数のinterfaceを継承していることを制約とすることもできる。
public class Hoge<T extends Cloneable & AutoCloseable> { }
記法は以上の通りで & で複数のinterfaceを並べることができる。
再帰的な宣言
型変数の境界にパラメタライズドタイプを用いることも出来る。
public class Hoge<T extends Comparable<String>> { }
パラメタライズドタイプのバインドに宣言中の型変数を使うことも出来る。
public class Hoge<T extends Comparable<T>> { }
ついでに挙げれば宣言中のcalss 自体も使うことが出来る。
public class Enum<E extends Enum<E>> { }
java.lang.Voidの憂鬱
型変数のバインドに際して、「使用しない」というケースが稀にある。こうした時、一般にはjava.lang.Void型で型変数を潰す。
Map<String, Void> map = new HashMap<String, Void>();
java.lang.Voidはvoidのラッパー型なので、型変数にvoidを指定したいときにも用いられる。しかし、JavaのVoidはあくまでもただのclassでしかなく、特別な型というわけではない。そのため、型変数に境界が指定されている場合、Voidをバインドすることができない。
public class Hoge<T extends Piyo> { } Hoge<Void> hoge = new Hoge<Void>(); // ← NG
これはVoidがextends Piyoではないことによる。こうした型変数に境界があるケースで、かつ、Voidのようなもので潰すことが想定されるような場合は、自力でVoid用のクラスを定義してやる必要がある。Scalaなどでは「全ての子」に相当する型が規定されているのでこうした苦労がない。
ちなみに、Javaのnullは構文的には全ての型にキャスト可能なnull型の唯一のリテラルという扱いで、イメージ的にはnull型は全ての型の子ということになる。しかしこのnull型(null type)というのは言語仕様上現れはするものの、Javaのコードでこのnull型を明示的に扱うことはできない(リフレクションでも扱えない)ので型変数をnull型で潰すような使い方はできない。通常はnull型というのは概念上のもの、ぐらいに思っていて良いだろう。(JVMの中とかでは現れるかもしれない)
Java Generics Hell - インスタンススコープのジェネリクス
Java Generics Hell アドベントカレンダー 8日目。
- 前回(7日目) メソッドスコープのジェネリクス
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
ようやくクラスの型変数の話にたどり着いた。なかなか話題が多くて大変なのである……。
インスタンススコープ
ここのところしつこくJavaのジェネリクスの型変数には2種類のスコープがあることを挙げてきた。
- メソッドの範囲で有効な型変数
- クラスのインスタンスの範囲で有効な型変数
前回、メソッドスコープのジェネリクスについて取り上げた。入門書などではあまり触れられていない構文かもしれないが、機能的にはメソッドスコープのほうが単純だと思うので先にメソッドスコープを取り上げた次第である。
メソッドスコープではメソッドの IN / OUT つまり、引数と戻り値を型変数を用いて関連性を示すことができるということを書いた。インスタンススコープではあるクラスのインスタンスに対しての IN / OUTの範囲で型変数を用いて関連性を示すことができる。つまり、あるインスタンスを外から眺めた場合に
といったところで型変数によって汎化することができる。エンクロージングインスタンスというのは内部クラスのインスタンスのことで、気になる人は拙稿 Javaのクラス宣言5種+αあたりを参照して欲しい。通常はあまり意識することはないだろう。
クラスの内部のコードとしては、ローカル変数宣言などでも型変数を用いることができるし、型変数へのバインドに型変数を用いることもできる。
ジェネリクスの事例
ジェネリックなクラスの「利用」に関してはjava.util.Listなどのコレクション・フレームワークがよく挙げられる。オブジェクトを保持する目的の汎用ライブラリだ。また、Java8で導入されたStream APIでも多用されている。いわゆる関数型プログラミングを行うライブラリといったところだろうか。
ジェネリクスについて学ぶ際、どこを目標にするかという問題がある。
- 型変数のあるメソッド、型変数のあるクラスの既存ライブラリを使うことができる
- 型変数のあるメソッド、型変数のあるクラスを設計することができる
入門レベルだとまずは前者の「利用」が目的となるだろう。しかし、「利用」を確実なものとしようとすれば、メソッド宣言、クラス宣言から型変数がどういう意味合いで宣言されているか読み解ける必要が生じるだろう。そうしたコードリーディングと、自身のプログラミングの試行錯誤でもってジェネリックなメソッド、クラスを設計することができるようになるだろう。
まずは利用、そこから設計に進むのが順当だろうと思う。本稿では軽く、型変数を用いたクラス設計について大雑把な指針を示しておこう。
ジェネリクスで汎化するにあたって、その型変数の個別の具体的な機能に依存することはできない。ここまででまだ取り上げていないが型変数の境界を利用することで、継承階層によってある程度対象となるクラスを具体化することもできるが、境界がない場合は「全てのクラスに対して行える操作」の範囲で型変数の型を取り扱わなければならない。
具体的には
- 正確にはjava.lang.Objectに宣言されるメソッド
- 参照の代入
- 参照の有無 (要するにnullかそうでないか)
ということになる。
ジェネリクスの事例としてコレクションフレームワークが挙げられるのは、まさにこの「参照の代入」「参照の有無」という機能を中心としているのでジェネリクスの題材として具合が良いのである。
データ抽象
言語によっては予め言語に特別な意味を持った型が組み込まれている。Javaでもプリミティブ型や配列といった特別な型が存在する。
プログラマが独自に定義できるデータ型、つまるところJavaでいうクラスのようなものだけでその言語全ての型が取り扱われるならば、それらは同じように抽象的に扱えるわけである。そうした概念を「データ抽象」という。
データ抽象とジェネリクスは関連が深い。抽象化されているからジェネリクスの型変数で抽象的に同じように扱えるというわけである。しかし、Javaの場合はプリミティブ型や配列は特殊な型として言語設計されているため、これらをジェネリクスで一般的なクラスと同列に扱うことができない。
演算子オーバーロード
データ抽象と関連深いテーマに演算子オーバーロードがある。プログラマによって+や-といった「演算子」を型ごとに独自に定義することができる機能である。
C++ではintなどのプリミティブ型に対してもジェネリックなコードを書くことが出来るが、こうした型に対して行える「抽象的な操作」こそ、演算子なのである。つまり、intの値が2つ与えられた時、ふたつを「足す」というコードを書くことが出来るが、同じようにdouble型に対しても「足す」ことが出来る。同じ操作が出来るから、ジェネリクスによって汎化させることができる。
この「同じ操作」を表現するためのインターフェースとして現れるのが「演算子オーバーロード」である。単なるメソッド呼び出しのシンタックスシュガーというわけではない。
Javaの初期に演算子オーバーロードが導入されなかった理由としては、初期のJavaにジェネリクスが間に合わなかったことが挙げられるだろう。ジェネリクスなしでの演算子オーバーロードはコードの見た目を整えるシンタックスシュガー的な意味合いだけになってしまう。初期のJavaの設計段階で掲げられたKeep it simpleという標語を考えれば、データ抽象的な意味合いを持たない演算子オーバーロードなどはまさにKeep it simpleのために捨てられるような機能であったと思う。
今のところ、Javaでもプリミティブ型に対してもジェネリクスで扱えるように拡張しようという計画はあるが、演算子オーバーロードを導入しようという話は聞かない。
まとめ
- インスタンススコープのジェネリクスではインスタンスのメソッド操作、フィールド操作といった広い範囲で型の関連性を示すことが出来る
- 型変数で扱う型といえども、それらに対して「同じ操作」が出来る前提で抽象的な設計をする
- もっと根源にある「同じ操作」とは「代入」であり、他にもnullとの比較やjava.lang.Objectに定義されるequalsやhashCodeなどを用いれる
- このためコレクションフレームワークと相性がよい
- 型変数の境界を用いればより具体的な型で抽象化するようなこともできる(後に取り上げる予定)
- データ抽象という概念と、そのための演算子オーバーロード(Javaでは導入されていない)
次回は型変数の境界あたりを取り上げたい。
Java Generics Hell メソッドスコープのジェネリクス
Java Generics Hell アドベントカレンダー 7日目。
- 前回(6日目)ジェネリクスの構文
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
前回、構文の話をしていたが、そのなかでメソッドスコープのジェネリクスをいったんおいていた。
今回はそのあたりを取り上げる。
ジェネリクスとスコープ
前回のおさらいとして、Javaのジェネリクスには2つのスコープがある話をしていた。
- メソッドの範囲で有効な型変数
- クラスのインスタンスの範囲で有効な型変数
まずジェネリクスのそもそも論になるが、ある処理の塊に型変数という「型」を表す変数を導入し、処理の塊をいろんな型で扱えるようにしよう、ということになる。ここで「処理の塊」としてJavaのジェネリクスでは「メソッド」か「インスタンス」か、ということになる。
メソッドスコープの型変数は
で宣言することができ、その内部を範囲として型変数を用いることが出来る。インスタンス・メソッドや、コンストラクタの場合は、クラスのインスタンスをスコープとする型変数と、そのメソッドだけを有効範囲とするメソッドスコープの型変数が混在することがある。内部クラスや無名クラス、ラムダ式などを考慮すると型変数の有効範囲というのはもう少しややこしいのだが、まずはおいておこう。
過去のセッション資料で取り上げた図を挙げておこう。メソッドスコープの型変数は、メソッドのIN / OUTにおいてどことどこが同じ型なのかを表すことになる。
構文
簡単に構文を説明しておこう。標準APIから抜粋する
public static <T> Set<T> singleton(T o) { // 略 }
https://docs.oracle.com/javase/jp/9/docs/api/java/util/Collections.html#singleton-T-
public static の後の<T>が型変数宣言である。次のSet<T>は戻り値の型、singletonはメソッド名、その後の()は引数の宣言である。要するに通常のメソッド宣言の戻り値の手前の部分に<T>といったように山括弧で型変数を宣言する。複数の型変数の場合はカンマで区切る。
使用する場合は、通常はバインド型が推論されるのでそのまま呼び出せば済む。
Set<String> set = Collections.singleton("hoge");
型変数にバインドする型を明示する場合はメソッドの手前に山括弧で指定する。
Set<String> set = Collections.<String>singleton("hoge");
インスタンスメソッドの呼び出しの場合、メソッド名だけで呼び出そうとするとバインドを明示できない。この場合はthis.を補う必要がある。staticメソッドで自クラス内のメソッドを呼ぶ場合も似たような感じで、クラス名.を補わなければならない。このあたりは構文上ちょっと苦しい。
しかし、コンストラクタでのメソッドスコープ型変数のバインドの場合、newでのバインドとかぶるので無理やり位置を変えて対処したのだろう。まぁコンストラクタのメソッドスコープの型変数というのは構文上可能だが使い道はまずない。
Hoge<Integer> hoge = new <String>Hoge<Integer>();
メソッドスコープの型変数の例
Javaの標準APIからメソッドスコープの型変数の例を挙げよう。java.util.Collectionsクラスからとなる。このクラスはListなどのコレクションフレームワークと呼ばれるクラス群に対しての便利なstaticメソッド集という感じである。
さて、先程「メソッドのIN / OUTにおいてどことどこが同じ型なのか」といったが、実例をみてみよう。
public static <T> Set<T> singleton(T o)
https://docs.oracle.com/javase/jp/9/docs/api/java/util/Collections.html#singleton-T-
引数がT型で、戻り値がSet<T>型となっている。引数と戻り値に型変数で対応付けがされているのが分かるだろう。
public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
この例では戻り値はboolean型で型変数は関係ないが、複数の引数があり、それらがList<T>型、T型、T型であるという関連性であることがわかる。
public static void shuffle(List<?> list)
https://docs.oracle.com/javase/jp/9/docs/api/java/util/Collections.html#shuffle-java.util.List-
対して、このshuffleというメソッドでは型変数は用いられておらずワイルドカードでList<?>とされている。ワイルドカードについては別稿で詳しく挙げるが、ざっくり言えば何型でもいいというわけだ。それは、shuffleの引数がただひとつであり、また戻り値もvoidであって、メソッドの引数や戻り値間の型の関連性をチェックする必要性がないからだ。
このように、引数間や戻り値の間で、こことここの型が同じ、だが、その型はべつになんでも良い、という場合にメソッドスコープのジェネリクスが用いられる。
Java Generics Hell - ジェネリクスの構文
Java Generics Hell アドベントカレンダー 6日目。
- 前回(5日目) パラメタライズドタイプ
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
ここまで話の簡単のためにいろいろ端折って書いてきているのだが、徐々に詳細を書かねばなるまい。同時に取り漏らしも回収して行かなければならない。
スコープ
- メソッドの範囲で有効な型変数
- クラスのインスタンスの範囲で有効な型変数
List<String>といった例でバインドされているのは後者のクラスのインスタンスを範囲としたもの。よく目にするのがこちらだろう。
前者のメソッドの範囲の型変数は入門書ではあまり取り上げられていないかもしれない。しかし、話としてはメソッドスコープの方が単純なのできっちりやるならメソッドスコープから導入したほうがわかりやすいのではないかと筆者は考える。
また、「インスタンスの範囲」と言っているのは、型変数への型のバインドがインスタンス単位で行われるので、staticメソッドではクラススコープの型変数が参照できないことを指している。例えばList<String>とList<Integer>といったものが宣言できるわけだが、Listのコード中で型変数EがこのStringやIntegerとして扱われるわけだ。ではstaticメソッドでは?インスタンス単位でEをStringとして扱います、Integerとして扱います、とやっているわけだから、staticメソッドでEを用いることは出来ない。
さて、そこまで話を振っておきつつ、メソッドスコープの話は先送りで今回はこれ以上つっこまない。
3種の山括弧
Javaのジェネリクスの構文で重要なのは<>の山括弧には主に3種類あるということだ。<>を見たら漠然と「ジェネリクス」と考えているかもしれない。しかし、ちゃんとマスターしたければ、まず構文上の違いを理解しなくてはならない。
型変数の宣言
まずは型変数の宣言。型変数を用いたジェネリックなクラスを自作することがなくとも、ソースコードから宣言をみる必要性が生じることもある。理解しておこう。
public class Hoge<T> { }
クラスを宣言する際にクラス名の後ろで山括弧に型変数名を宣言する。複数の型変数を宣言する場合はカンマで区切って並べる。
public class Piyo<T1, T2> { }
型変数名は慣習としてTypeの頭文字Tとすることが多い。構文上はclass名などと同じで事由に識別子を設定できるが、一般的なclass名の命名規約と同等のキャメルケースとかにすると紛らわしい。ジェネリクスで抽象化する時点で扱いが抽象的になるので、Typeの頭文字TとかElementの頭文字Eとかになることが多いが、より具体的に名付けたければRESULTなど英語大文字が一般的か。言語仕様上は日本語などでも構わない。
パラメタライズドタイプ
次はパラメタライズドタイプ。これは前回も出てきたが、変数宣言の際の型で用いる。
// 変数の宣言
List<String> stringList;
また、メソッド引数の型などでも用いられる。
public void hoge(List<String> list) { // ... }
バインド
そしてバインド。バインドにはいくつかあるが、本稿では基礎的なnewの際のバインドだけを挙げておこう。
List<String> stringList = new ArrayList<String>();
この場合、左辺のList<String>はパラメタライズドタイプで右辺のnew ArrayList<String>();の<String>がバインド部分である。
この際の山括弧はListの型変数に対してString型をあてはめる(バインドする)という意味合いである。ListのEはStringですよ、というわけだ。
複数の型変数を持つクラスの場合はカンマで区切って複数の型を指定する。
Map<String, Integer> map = new HashMap<String, Integer>();
Java Generics Hell - パラメタライズドタイプ
Java Generics Hell アドベントカレンダー 5日目。
- 1日目 Java Generics Hell 序章
- 2日目 オブジェクト指向
- 3日目 リスコフの置換原則
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
パラメタライズドタイプ
Javaのジェネリクス周りは一緒くたに単に「ジェネリクス」と呼ばれている気がするが、<>の山括弧は構文上、3種類ぐらいに分類される。詳しい話はまた別途行うが、今回はパラメタライズドタイプ(パラメータ化された型)について。
パラメタライズドタイプの例を挙げよう。
List<String> list;
要するに変数宣言時の型で< > の山括弧が付いているものを指すと思ってもらいたい。
<>の山括弧でパラメータが付いているので「パラメータ化された型」というわけだ。ただ、固有名詞としては「パラメータ化された型」というのは決まりが悪いので英語でパラメタライズドタイプ parameterized type と筆者は呼んでいる。
非変性
前回、変性についての話をしたが、Javaの参照型は宣言した型と、その子の型が代入できる共変性であると説明した。配列を含め一貫して共変だが、そのせいで配列の静的な型安全性にほつれがあることも取り上げた。
通常パラメタライズドタイプでは型安全性のために非変性となっており、例えばList<Object>型の変数にはList<String>型を代入することはできない。これまた別途取り上げるがワイルドカード List<? extends Object>といったものを使うと非変性ではなくなるのだが、別途ということで我慢していただきたい。
ジェネリクスの話題はどこから始めるかが難しいのだが、ある順序で学べばすっきり全部わかるというわけにはいかない。螺旋を描くように、少しずつ全体をぐるぐると何周か巡らないと理解しにくいと考えている。
まとめ
- List<String>型といった<>つきの型をパラメタライズドタイプという
- 通常、パラメタライズドタイプは非変性
- ワイルドカードで変性が異なるものもある
今日は短め。次回は一旦構文の話をしたい。
Java Generics Hell - 配列と変性
Java Generics Hell アドベントカレンダー 4日目。
- 1日目 Java Generics Hell 序章
- 2日目 オブジェクト指向
- 3日目 リスコフの置換原則
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
前回は継承のある型システムで重要な「リスコフの置換原則」について取り上げた。
今回はそれを踏まえて、Javaの一般的な参照型での暗黙のアップキャストと、ジェネリクス(正確にはパラメタライズドタイプ)がアップキャストできない理由について取り上げたい。
キャスト
継承関係のある親クラスと子クラスのインスタンスについて
- 子クラスを親クラスに変換する「アップキャスト」 (upcast あるいは Widening Reference Conversion)
- 親クラスを子クラスに変換する「ダウンキャスト」 (downcast あるいは Narrowing Reference Conversion)
がある。
そもそも、前提として「リスコフの置換原則」があるので、子クラスは親クラスを代替することができる。子のインスタンスが親型で宣言された変数にすっと代入することができる。ChildをParentにするアップキャストである。
Parent p = new Child();
この親の型で宣言された変数に代入されている、実際のインスタンスは子の型である。これを取り出して、子の型に戻す場合、明示的なキャストが必要となる。
Child c = (Child) p;
この操作は安全ではない。変数pに格納されているインスタンスは親型のParentかもしれないし、Childかもしれないし、Brotherかもしれないし、Sisterかもしれない。
このようにJavaの参照型の変数というのは、その型もしくは、その子の型を代入できるというルールになっている。こうした性質を「共変」(きょうへん)という。
変性
世の中の言語には「共変」とは違うルールのものもある。
- 子の型の代入を許す「共変」covariance
- 子の型の代入も許さない「非変」invariant
- さらに親の型の代入を許す「反変」contravariance
「非変」invariant は不変とも。ただし値が変更できない意味のimmutable(これも不変と訳される)との用語の使い分けの都合から本稿では「非変」という用語を採用することにしよう。
Java 1.4まではとにかく共変だけを理解していれば良かった。Javaのジェネリクスではこの3種を使い分ける必要がある。
配列の静的な型安全のほつれ
Java 1.4までは参照型は一貫して全てが共変で扱われていた。これは、言語組み込みの機能限定版ジェネリクスであるところの「配列」にひとつの問題を残している。
String[] stringArray = {"one", "two"}; Object[] objectArray = stringArray; // 代入できる objectArray[0] = Integer.valueOf(1); // コンパイル上は代入できる
Object配列型の変数objectArrayにString配列のインスタンスが格納されている。objectArray型はObjectの配列であるからInteger型を格納できるはずである。しかし、実体はString配列であるから格納できては矛盾が生じる。
Javaではこうした場合に実行時にjava.lang.ArrayStoreExceptionが投げられることが保証される。良く分からないまま動き続けたりはしないのだが、静的なコンパイルでは検出できないのは事実である。
なぜこのようなことが起こるのだろうか。リスコフの置換原則を思い出しつつ、配列の機能性に着目しよう。
- String[ ] は String型を格納できる
- String[ ] は String型を取り出す
- Object[ ] は Object型を格納できる
- Object[ ] は Object型を取り出す
String[ ]がObject[ ]の子となるためには、親の型の機能を満足させなければならないのであった。値の取り出しは問題ない。String型で取り出されるが、String型はObject型でもあるので問題とはならない。
問題となるのはObject[ ]のObject型を格納できる機能性だ。String[ ]はString型しか格納できない。Object[ ]の機能を全て提供できていないのである。
型を扱う型
このように配列、つまり、ある型を間接的に扱う機能では、共変では矛盾が生じるのである。String[ ]はString型を扱う。Object[ ]はObject型を扱う。StringはObjectの子だが、String[ ]はObject[ ]の子として責務を果たせない。Integerなどを格納できない。
配列が言語組み込みの機能限定版のジェネリクスである、と言う言い方をしていた。事実、Scalaのようなより新しい世代の言語では配列がジェネリクスで表現され、型システム上「配列」という特別な型は存在しない。
現代という未来からJava 1.0の時代(1995年)を振り返れば、このように語ることが出来るが、当時にそれを求めるのは酷である。だが、現代にこうした禍根が残ったことは事実であり、Javaを使う以上、この歴史の禍根も飲み込んで使わなければならない。より未来に言語仕様が変更になって問題が解決するまでは(後方互換性の観点からするとなかなか解決は難しそうだ)
JavaではJava5のジェネリクス導入時に新機能であるジェネリクスについては変性を見直し、静的に安全となるように設計された。配列についてはそのままである。負の遺産となってしまった。
「型を扱う型」を考える時、いろいろと複雑な話が生じる、という話に片足を突っ込んだ所で次回に続く。