Java Generics Hell - 配列と変性

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

読者の推奨スキルとしては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型でもあるので問題とはならない。

問題となるのはObjectObject型を格納できる機能性だ。StringはString型しか格納できない。Object[]の機能を全て提供できていないのである。

型を扱う型

このように配列、つまり、ある型を間接的に扱う機能では、共変では矛盾が生じるのである。StringはString型を扱う。ObjectはObject型を扱う。StringはObjectの子だが、StringはObjectの子として責務を果たせない。Integerなどを格納できない。

配列が言語組み込みの機能限定版のジェネリクスである、と言う言い方をしていた。事実、Scalaのようなより新しい世代の言語では配列がジェネリクスで表現され、型システム上「配列」という特別な型は存在しない。

現代という未来からJava 1.0の時代(1995年)を振り返れば、このように語ることが出来るが、当時にそれを求めるのは酷である。だが、現代にこうした禍根が残ったことは事実であり、Javaを使う以上、この歴史の禍根も飲み込んで使わなければならない。より未来に言語仕様が変更になって問題が解決するまでは(後方互換性の観点からするとなかなか解決は難しそうだ)

JavaではJava5のジェネリクス導入時に新機能であるジェネリクスについては変性を見直し、静的に安全となるように設計された。配列についてはそのままである。負の遺産となってしまった。

「型を扱う型」を考える時、いろいろと複雑な話が生じる、という話に片足を突っ込んだ所で次回に続く。