気合が続くか分からない Java Generics Hell アドベントカレンダー 1日目。
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
導入
JavaのジェネリクスはJavaが設計された当初(Javaは1995年に発表されている)から検討はされてはいたものの、実装が追いつかず、導入はJava5(2004年)を待つこととなった。後方互換の都合からいろいろと不便な部分もある。Javaに限らず、ある程度、歴史の長いプログラミング言語は、改定の都合からいろいろと不都合な部分が生じるものである。後方互換性を犠牲にして一新すれば言語仕様もさっぱりするが、非互換の壁でバージョンアップできないという闇を抱えたりとそれはそれで大変だ。
後方互換、つまり、より新しいバージョンは、昔作ったモノも動かせる、ということを前提にした場合、プログラミング言語の仕様というのはどんどん肥大していくことになる。古い時代のある種の言語機能が脚枷となって、より新しい概念を導入する時に不整合を生じたりすることもある。なので、言語仕様としては"Keep it simple" (Javaの産みの親であるジェームズ・ゴスリンの言葉)という指針は良いものだと思う。とは言え、Javaも古い時代の言語仕様に引きずられた複雑さというものがどうしても残っているわけで、シンプルにしたくても後方互換性との兼ね合いでそうもいかなかったりと苦労が伺える。
そういった意味でJava1.4 -> Java5 の間で後方互換性を持たせてジェネリクスを後付けで導入したというのは偉業であると思う。同じことをやれと言われても自分には到底できないであろう。それはそれとして、Javaの型システムは後に大きな禍根を残すことになる。
プリミティブ型
Javaの入門書でかなり初期にプリミティブ型(int型やboolean型など)と参照型というものを習うことだろう。参照型はjava.lang.Object型を頂点として継承階層で表現される。プリミティブ型はその外にある。このハイブリッドシステムは、Javaが産まれた当時のコンピュータの貧弱さゆえの妥協点であったと言えるだろう。CPUクロックは150MHz程度、メインメモリは8 'M' といった時代だった。現代では8Gのメモリも一般的だが、それと比べると1000分の1といったところである。現代のスマートフォンの方がよほどハイスペックである。
JavaがターゲットとしたのはC++からの乗り換えであった。ジェームズ・ゴスリングによればJavaに最も影響を与えた言語はSimula-67とMesaだそうだが、当時の言語シェアからC++プログラマに親しみやすいようにC言語系の構文を採用した。当時でも数値型も含め全てがオブジェクトである、(ハイブリッドではない)完全なオブジェクト指向の言語は存在したが、C++はそうではないわけで、Javaが(ハイブリッドではない)完全オブジェクト指向言語ではないということは当時特別問題視されてはいなかったようにも思う。
JavaはC++と同様に配列は宣言後に自由にサイズを変更することが出来ない。そうした「可変長配列」への欲求への答えとしてjava.util.Vectorクラスが当初のJava1.0から提供されていた。Vectorクラスは値を参照型のトップであるjava.lang.Object型として扱う。ジェネリクス機能はないため、値を取り出すにはキャストが必要であった。
しかし、プリミティブ型と参照型のハイブリッドであるJavaではプリミティブ型はjava.lang.Object型の継承型ではない。つまりVectorに格納することができない。そのため、Java1.0の当初からVectorにint型の値を格納したい時にはjava.lang.Integer型でラップして格納する、という方策が取られた。これが回り回って悪名高いオートボクシングへと繋がる。
プリミティブ型もジェネリクスで扱えるようにしたいという欲求は当然あり、JEP 218ではこの改善を検討している。JEP 218はいつリリースされるのだろうか。
配列
配列もまた、ジェネリクスに禍根を残す古い時代の言語仕様と言える。Javaが発表された1995年の当時、C言語やC++言語の配列の機能性になんら疑問は持たれていなかったのではないだろうか。
現代から振り返れば、配列こそは機能が限定された言語組み込みの特別なジェネリクスのようなものなのである。
この言語組み込みの機能限定版ジェネリクスを、ジェネリクス導入時に統合できなかったことは禍根のひとつであるが、これもまた後方互換性を思えば後付けで統合するというのは過酷な要求かもしれない。
配列は、要するに「ある型」の配列を作るわけだが、「ある型」こそがジェネリクスで扱う型変数のようなもので、コンパイルでの型安全のためには代入互換性を非変(継承型を代入できてはならない)に保たなくてはならない。(詳細は別途)
しかし、1995年当時のJava1.0で参照型の代入互換性は共変、つまり、子の型は代入できるという方針であった。配列型もまた参照型扱いであり、java.lang.Objectに代入可能である中で、配列型変数だけを非変とする方針はとり難かったであろう。そもそもJavaのジェネリクスで変性を取り扱うワイルドカードについては2002年の論文 On Variance-Based Subtyping for Parametric Typesを待つことになるわけだから、1995年当時にこれを踏まえた配列型の変性設計をせよというのはオーパーツである。
配列の代入について型の安全性はコンパイル時にはチェックしきれない。仕方なく、そこは実行時の例外という形で対応している。なので、「コンパイルでの型安全」は保証できないという表現となっている。型安全にもレベルがあってキャストにより想定外の型であった場合、Javaは安全に停止するという意味合いでは型安全ではある。「型安全」というのもやすやすと使えない用語なのである。安全ではない時代はそれこそプログラムが暴走してマシンがフリーズするような世界であった。
とかく、Javaの型システムは配列について妥協があり、これが未来の禍根へと繋がる。Scalaのようなより後の時代の言語では配列もジェネリックな一種のコンテナの型として統合されている。「配列」という特殊な言語組み込みの型を特別扱いする必要がない。
イレイジャ
イレイジャについては誤解される部分が多い。イレイジャ方式を吊るし上げるような言説もみかけるが、多くはオブジェクトのインスタンス生成をジェネリックなメソッドで行いたいというシチュエーションでの筋の悪い逃げ道として、型変数からのリフレクションを用いており、そうした逃げ道が使えない、という恨みをイレイジャにぶつけたようなものである。
C#で言えばnew制約のようなものが欲しいというシーンはあるだろう。そもそもオブジェクトの生成、newをする際の引数についてはJavaでは継承階層では縛れないのであった。どんな引数のコンストラクタか分からないものを一律newで生成しようというのがそもそも設計上の誤りを多分に含んでいる。とはいえO/Rマッパーのような、デフォルトコンストラクタを期待しても妥当であろうというケースもあり、そうした場合にnew制約というのはキモチワルイしかし便利な救済策ではある。
より発展的には型クラスによって解決されるべき問題だろう。
こうした、実行時の動的な型情報の引廻しができることこそが「真のジェネリクス」である、という主張もある時期には流布したが、コンパイル時の型の整合性チェックこそがジェネリクスの主だった価値であるし、実行時のリフレクションにジェネリクスの本質を求めるというのは疑問のある主張と言えよう。最近はあまり「真のジェネリクス」 "true generics" といった言説もみなくなりつつあるが、一部では根強い人気がある思想である。
イレイジャの厳密な用語定義はまた別途とりあげたいが、オーバーロードに制約が生じるというのが直接的に感じられる問題であろうか。いささかメソッドの特定の仕様が中途半端な感じになってしまった。しかしながら、イレイジャについていえばいくらかの制約はあるものの、これは禍根というよりは、将来の拡張性を思えばむしろ良かったのかもしれない。