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のジェネリクス導入時に新機能であるジェネリクスについては変性を見直し、静的に安全となるように設計された。配列についてはそのままである。負の遺産となってしまった。
「型を扱う型」を考える時、いろいろと複雑な話が生じる、という話に片足を突っ込んだ所で次回に続く。
Java Generics Hell - リスコフの置換原則
Java Generics Hell アドベントカレンダー 3日目。
- 1日目 Java Generics Hell 序章
- 2日目 オブジェクト指向
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
前回はオブジェクト指向の中核が(継承に限定せず幅広い意味での)ポリモーフィズムではないか、という話であった。
今回はそれらをコンパイル、つまり静的型チェックによって安全を目指す「型システム」について。その中でも「リスコフの置換原則」について取り上げる。
ヒューマンエラー
ボイラープレートと呼ばれるプログラミングの定型句ぐらいであれば機械による自動生成もありうるが、プログラミングというのは現代では基本的に人力の作業である。
人間は簡単な問題であってもときおり誤りをおかす。一桁の足し算のようなドリルでも、延々とやらせると時折ミスが混ざる。「簡単だからミスするはずがない」「ミスをするのはたるんでいるからだ」といった考え方ではこうしたミスを防ぐことはできない。単純作業であっても人間はミスをおかすものだ、という前提に立ち、どういうときにミスが起こりやすいのか、それをどうすれば防げるのか?ということを考える必要がある。
こうした、偶発的に人間が生じさせるミスをヒューマンエラーと呼ぶ。故意ではないし、能力不足でなくとも生じることがある。
さて、プログラミングが主に人力で行われる以上、ヒューマンエラーに基づくバグというのは一定確率で必ず生じる。ヒューマンエラー対策としては機械による補佐が効果的であり、型システムもまたそうした対策の一種足りうる。筆者は型システムによる静的チェックを重要している。余談となるがそのほかのヒューマンエラー対策としてはコンパイルによる型チェック以外の静的解析(バグパターンを検出したりできる)、自動テストとCI(継続的インテグレーション)によるバグの早期発見のための体制づくりなどが挙げられる。
型システム
さて、型システムだが、データがどういう種類のものかを宣言することで、データの種類の取り違えを防ぐものである。
Javaの場合はプリミティブ型(boolean, char, int等の数値型)と参照型があるが、ここでは主に参照型の話をしよう。Javaでは継承とポリモーフィズムを採用しており、継承の親子関係がある場合、子は親の代わりができることとされる。
これはリスコフの置換原則と呼ばれるもので、Javaのオブジェクト指向では大原則となる。これに反すると設計がいろいろとおかしくなり、予期せぬバグのもととなる。
「子は親の代わりができること」つまり、親のクラスが提供する機能は子のクラスでもすべて提供されなくてはならない。親クラスで提供されるpublic メソッドを子のクラスで潰したいと思うことが生じたら、それは継承するべきだったのかを考え直す必要がある。このリスコフの置換原則に沿っているかを考える際の標語としてis-a関係という言われ方もする。子 is 親という言い方をしたときに概念として明らかに違うという場合は継承を避ける、という話だ。
ジェネリクスの変性、代入できるかどうかみたいな話をするときにこのリスコフの置換原則が前提となる。
ケーススタディ 不変クラス
リスコフの置換原則は単純だが重要な概念である。リスコフの置換原則に則っていなくともある程度はボロが出なかったりすることもあるのでうっかりすることもあるのだが、シチュエーションによってはボロが出る。きちんと対応しておかないとシステムの土台が崩れかねない。
ここではアンチパターン(つまり真似をしてはいけない失敗パターン)として、不変クラスを例に挙げよう。
不変クラスとは、最初にnewする際に値を決めたら、その後で値が変わらないものを言う。Javaで言えばjava.lang.Stringは不変クラスで、可変なのはjava.lang.StringBuilderクラスだ。StringとStringを結合したような場合、結合した別のインスタンスが返される。
String s1 = "hello "; String s2 = "world!"; String s3 = s1 + s2; System.out.println(s3);
このような場合、s1とs2を+で結合するが、この時返されるs3はs1やs2とは別のインスタンスである。なので
System.out.println(s1 == s3); System.out.println(s2 == s3);
とすると、結果は共にfalseとなる。
一方、StringBuilderの場合は同じインスタンスでありながら値が変化する。
StringBuilder sb = new StringBuilder("hello "); sb.append("world!"); System.out.println(sb);
Stringのような不変クラスは、同じインスタンスが複数のスレッドから参照されるようなケースでもインスタンスの値が変更されるタイミングを考慮する必要がない。またシングルスレッドでも不変であることが保証されると値が途中で変わっているのではないか?という可能性を考えなくて済むので単純になる。不変のインスタンスというのは計算するたびに新しいインスタンスを生成するのでなんだか面倒なように思うかもしれないが、バグ対策という点ではなかなか優秀である。不変で済むものは不変で済ませる、というのはプログラミングの良い習慣とされる。
StringとStringBuilderは同じjava.lang.CharSequenceというinterfaceを実装する兄弟のような関係にあるが、継承関係にはない。
ここまでが前置きである。
ケーススタディ アンチパターン
例としてx,y座標をもつjava.awt.Pointのようなクラスを考えてみよう。
変更可能なクラスと、不変のクラスという2種類のクラスを継承関係で作るとしよう。メソッドの有無だけに注目すると、変更可能なクラスは値の設定と値の取得のメソッドがある。一方不変のクラスは値の取得のメソッドだけしかない。
public class Point { int x; int y; public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public void getX() { return x; } public void getY() { return y; } } /** 不変のクラス */ public class ImmutablePoint { int x; int y; /** 不変なのでコンストラクタで値を設定する */ public ImmutablePoint(int x, int y) { this.x = x; this.y = y; } public void getX() { return x; } public void getY() { return y; } }
不変クラス ImmutablePoint はコンストラクタで値を設定し、以後は値を更新することができない。getX(), getY()だけが提供される。継承に際して、子は親のメソッド全てを提供しなければならないのであった。ということは、Point が親となって子を ImmutablePoint とするとsetX(int) setY(int)が提供できないので駄目である。ImmutablePoint を親として Pointを子としてみよう。
public class Point extends ImmutablePoint { // (省略) }
さてこの場合、ImmutablePoint 型変数にPoint型のインスタンスを格納することが出来る。
Point p = new Point(); ImmutablePoint ip = p; // pのxを変更すると変数ipのxも変わる p.setX(100);
そしてPoint型インスタンスを触ることでImmutablePoint 型変数の内容も変更することができてしまう。これが駄目な例だ。
概念としてのis-a関係
リスコフの置換原則はただ提供されるメソッドが親と同じものを子が提供してさえすればいいというものではない。
ここでは親は「不変なx,yの値を持つクラス」であった。子のPoint型はis-a関係であろうか? "Point is a ImmutablePoint"だろうか?「可変なx,yの値を持つクラス」は「不変なx,yの値を持つクラス」だろうか?
クラスの責務というのはただメソッドだけで決まるわけではない。「不変である」というクラスの責務として不変なのであれば、不変であることを想定して様々なコードが書かれる。そうしたコードで動くように子を作ろうとするならば、子もまた不変でなくてはならない。
Java Generics Hell - オブジェクト指向
Java Generics Hell アドベントカレンダー 2日目。
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
前回は与太話だった。ジェネリクスの話をする前にOOP(オブジェクト指向プログラミング)について整理しておかねばならない。
オブジェクト指向とは
オブジェクト指向が何かという点についてはなかなか難しい。
Smalltalkを作ったアラン・ケイが「オブジェクト指向」という言葉を初めて用いたといわれるが、この「オブジェクト指向」はJava言語の教育の過程で一般に教えられる「オブジェクト指向」とはかなりイメージが違う。
アラン・ケイが「オブジェクト指向」という言葉を創った当初は、Smalltalk システムが体現した「パーソナルコンピューティングに関わる全てを『オブジェクト』とそれらの間で交わされる『メッセージ送信』によって表現すること」を意味していた。しかしのちに、C++ の設計者として知られるビャーネ・ストロヴストルップが(自身、Smalltalk の影響は受けていないと主張する)C++ の設計を通じて整理し発表した「『継承』機構と『多態性』を付加した『抽象データ型』のスーパーセット」という考え方として広く認知されるようになった(カプセル化、継承、多態性)。現在は、両者の渾然一体化した曖昧な概念として語られることが多い。
Smalltalk とオブジェクト指向
要するに、「オブジェクト指向」と呼ばれる概念はアラン・ケイ系統とビャーネ系統とがあるわけだ。アランのオブジェクト指向は「メッセージング」というアイデアが中核になっている。よって、本稿ではそれらは「メッセージング」と呼ぶこととし、概念を呼び分けよう。Java言語でのオブジェクト指向はビャーネのC++の影響を強く受けている。本稿では特に断りがなければ「オブジェクト指向」はこのC++系統のオブジェクト指向としよう。
オブジェクト指向の三大要素?
しばしば「オブジェクト指向の三大要素」と銘打って「継承、カプセル化、ポリモーフィズム」が語られることがある。ものによっては「クラス、カプセル化、ポリモーフィズム」となっている記述も見かける。なお、原典はなんだかよくわからない。
この論に基づくと継承がないものはオブジェクト指向にあらず、といった思想にも陥りそうだが、クラスの継承を持たないプログラミング言語でも「オブジェクト指向」とされる。いわゆる動的言語など呼ばれるものなどでも「オブジェクト指向」だ。
「オブジェクト指向」の中心概念はなんなのか?という問いはまた難しく、多くの派閥があることだろう。以下は筆者の私見であるとまず断っておく。
「継承」という概念は「ポリモーフィズム」はおそらく不可分な概念であろうと思う。クラスを継承したとして、ポリモーフィズムがなかったとしたら、それは「継承」なのであろうか。クラスの継承がないプログラミング言語もある。「継承」はおそらくオブジェクト指向という概念に必須ではない。Javaのような静的言語(つまりコンパイルによって静的に型の整合性をチェックできる言語)では「継承」といった部分については「型システム」という概念で分離したほうが話がすっきりすると思うのである。
もちろん、「ジェネリクス」はその「型システム」の一要素ということになる。
カプセル化については設計指針として外部から内部を隠蔽し、窓口を限定し明確化することで柔軟性、保守性を高めることができる、が、これは「オブジェクト指向」そのものの構成要因ではない。つまり、オブジェクト指向を活かすにはうまくカプセル化をしなさいよ、という指針であって、オブジェクト指向プログラミング言語を採用すれば直ちにカプセル化されるものでもない。そういう意味では、この三大要素とやらに並べられる要素はどうもそろっていない。こじ付け的に3つレベル感のあわない要素を挙げたようなものに思える。
対してポリモーフィズムは「オブジェクト指向」そのものの構成要素である、と私は考える。つまり、データ(Javaでいえばフィールド)と操作(Javaでいえばメソッド)をひとまとめにし、オブジェクトごとに操作は挙動が変わりうる、ということだ。これはJavaももちろんのこと、JavaScriptのようないわゆる動的言語であっても同じことだ。ただ、Javaではオブジェクトの操作を異なるものにするには継承を用いるが、JavaScriptであればオブジェクトのfunctionを異なるもので上書きすればよい。
つまり、オブジェクトにより挙動が異なるわけで、これこそが「オブジェクト指向」の中核であるように思えるし、このシンプルなアイデアがもたらす恩恵が大きいからこそ、現代のプログラミング言語ではこれほどまでにオブジェクト指向が重視されるのであろう。
ところで三大要素が「継承、カプセル化、ポリモーフィズム」、ものによっては「クラス、カプセル化、ポリモーフィズム」であると言った。ここで後者の「クラス」はどうか。先ほど「データと操作をひとまとめに」と言ったが、これこそ「クラス」と呼ばれるものである。そして、これは挙動としてはポリモーフィズムすることが前提である。となると、「クラス」と「ポリモーフィズム」というのはわりと重複するもので「クラス」において「ポリモーフィズム」はやはり不可分である。これもまた3つ挙げるためにこじつけた感じがする。いわゆる動的言語などであれば「継承」を挙げるのが不適切となるので代わりに「クラス」とした、といったところなのではないか。
まとめ
- 「オブジェクト指向」という言葉にはアラン・ケイ系統(Smalltalk)とビャーネ系統(C++)があり用語の混乱がみられる
- オブジェクト指向の三大要素といわれる概念は胡散臭い
- 筆者の私見となるが「ポリモーフィズム」こそが中核ではないか
- 継承によってポリモーフィズムさせる言語(Javaはここ)もあれば、よりダイナミックにメソッドを上書きできる言語もある。それもまたポリモーフィズムである
- カプセル化はオブジェクト指向をより活かすための設計指針のようなもの
- 継承は型システムの段でとりあげる
さて、次回は型システムについて取り上げたい。まずはJavaの型システムの基礎となるリスコフの置換原則である。
Java Generics Hell 序章
気合が続くか分からない 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" といった言説もみなくなりつつあるが、一部では根強い人気がある思想である。
イレイジャの厳密な用語定義はまた別途とりあげたいが、オーバーロードに制約が生じるというのが直接的に感じられる問題であろうか。いささかメソッドの特定の仕様が中途半端な感じになってしまった。しかしながら、イレイジャについていえばいくらかの制約はあるものの、これは禍根というよりは、将来の拡張性を思えばむしろ良かったのかもしれない。
Java本格入門のジェネリクスの用語訂正案
聞くところJava本格入門(通称アクロ本)の評判がすこぶる良いようだ。1995年にJavaが発表されてから随分と経つ。Javaのメジャーバージョンも8を数え、9がリリースされようとしている。1990年代にはこの新しい言語についてたくさんの技術書が発行されたが、これほどバージョンに差異があると現代では役に立たない。現代のバージョンに即した質の良い入門書が発行されることは喜ばしいことだと思う。
しかしながら、このJava本格入門も3-4-2 ジェネリクス(総称型)にていささか不適切な用語の使用方法がされているようだ。本項では改善提案を行い、また関連する用語の解説を行いたい。
発端
Java本格入門の101p、何となく違和感…
— なめりかん(仮) (@heignamerican) 2017年5月11日
『GenericStack<E>のように、パラメータ化された型として定義』
ここパラメータ化された型ではなく総称型ではなかろうか?
これに端を発しThe Java® Language Specification Java SE 8 Edition(Javaの言語仕様)を引用しての用語検討が行われた。
改善案
Java本格入門の原文は次のような文である。(3-4-2 ジェネリクス(総称型) より。強調は私によるもの)
それではジェネリクスを利用して、任意の型を追加可能なスタックであるGenericStackクラスを作成してみましょう。先ほどのStringStackクラスでは、taskListフィールドの要素の型やpushメソッドの引数、popメソッドの戻り値の型がStringでしたが、任意の型にするために、これらを仮の型であるEという文字で表現することにします(文字はEでなくてもかまいません)。この仮の型であるEを、仮型パラメータと呼びます。
ジェネリクスを定義するには、仮型パラメータEを用いて、GenericStack<E>のように、パラメータ化された型として定義します。
私による修正案は以下の通り。
それではジェネリクスを利用して、任意の型を追加可能なスタックであるGenericStackクラスを作成してみましょう。先ほどのStringStackクラスでは、taskListフィールドの要素の型やpushメソッドの引数、popメソッドの戻り値の型がStringでしたが、任意の型にするために、これらを仮の型であるEという文字で表現することにします(文字はEでなくてもかまいません)。この仮の型であるEを、型変数と呼びます。
型変数を定義するには、型パラメータEを用いて、GenericStack<E>のように、総称型として定義します。
引数、このややこしいもの
ジェネリクスでは型を取り扱うために「型変数」(Type Variables)を取り扱う。型を扱うための変数で型変数、というわけだ。
ここで、メソッドの「引数」の話をしよう。この「引数」という用語、非常に混乱がみられる。
public void foo(int value) { ... }
といったメソッドがあったとして、
foo(123);
といった形で呼び出されたとする。
この時、宣言側のvalueという変数を指して、parameter, formal parameter, formal argument, パラメータ, 仮引数, 仮パラメータ といった呼びかたをする。
また、123という実際に引き渡す値のことを指して、argument, actual parameter, actual argument, アーギュメント, 実引数, 実パラメータ といった呼び方をする。
単に「引数」と呼んだ場合、前者のことを指すこともあれば、後者のことを指すこともある。非常に曖昧で混乱がみられる用語である。
value側 | 123側 |
---|---|
parameter | argument |
formal parameter | actual parameter |
formal argument | actual argument |
パラメータ | アーギュメント |
仮引数 | 実引数 |
仮パラメータ | 実パラメータ |
このあたりはパラメータと引数 - Life like a clownを大いに参考にさせてもらった。日本語圏だけでなく、英語圏でもparameter, argumentの使い分けについては混乱がみられるようだ。
用語の混乱はあるが、本稿では宣言側をパラメータ、渡す値をアーギュメントと呼ぶことにしよう。
変数の受け渡しに注目する文脈で、変数を「パラメータ」「アーギュメント」と呼び分けるように、型変数についても受け渡しする文脈で「型パラメータ」「型アーギュメント」と呼び分ける必要が生じる。
3種の山括弧
Javaのジェネリクスについて、単に「ジェネリクス」という用語でジェネリクスに関するもの全体をもやっと指すという使われ方をしているように思う。区別ができないからもやっと「ジェネリクス」と呼んでいる状態。とくに山括弧<>があればなんとなく雰囲気で「ジェネリクス」と呼んでいる人が多いだろう。
まずはJavaのジェネリクスの山括弧には構文的に3種類あることを理解しなければならない。
- 型変数の宣言での型パラメータ
- 型変数へのバインディングでの型アーギュメント
- パラメータ化された型(パラメタライズドタイプ)での型アーギュメント
の3種類だ。(本稿の性質上用語の厳密さを気にしていたら妙に回りくどい表現になってしまった)
public class Hoge<T> {}
public class Piyo { public static void main(String[] args) { Hoge<String> hoge = new Hoge<String>(); }
というコードがあったあったとき、
- 型変数の宣言時の型パラメータ → Hoge<T> の<T>
- 型変数へのバインディングでの型アーギュメント → new Hoge<String>(); の<String>の部分。
- パラメータ化された型 → Hoge<String> hoge = ... の Hoge<String>
- パラメータ化された型での型アーギュメント → Hoge<String> hoge = ... の<String>
となる。詳細はやや古いが拙稿のJavaジェネリクス再入門 - プログラマーの脳みそあたりも参考にして欲しい。
Hoge<T>では型変数Tが宣言される。メソッドの引数のときの例で言えばfoo(int value)のvalueに相当するのがTと言えよう。これに対し、new Hoge<String>();ではこのTにString型をあてますよ、という意味合いになる。foo(123);の123に相当するものがStringというわけだ。TにStringをあてる部分を私は「バインディング」と表現している(情報工学用語では「束縛」英語では"Binding")。こうした、型変数を宣言する/バインディングするという文脈では「型パラメータ」「型アーギュメント」を厳密に使い分ける必要が生じる。しかし、そうした文脈でなければ型変数は単に「型変数」と呼べば良い。
ここで、Java言語仕様の英文での表現をみると
- 型変数の宣言時の型パラメータ Type Parameters 8.1.2. Generic Classes and Type Parameters
- 型変数へのバインディングでの型アーギュメント Type Arguments 15.9. Class Instance Creation Expressions
- パラメータ化された型 Parameterized Types Type Arguments of Parameterized Types
- パラメータ化された型での型アーギュメント Type Arguments Type Arguments of Parameterized Types
となっている。また単に「型変数」という場合はType Variables 4.4. Type Variablesという語が使われる。こうした型変数をもつクラスを指してはGeneric Classes 8.1.2. Generic Classes and Type Parameters という語が使われる。これはそのままカタカナにしてジェネリック・クラスと表現するか漢字で「総称型」と表現される。Javaの公式の日本語訳書で用いられた漢字表現だ。
Java本格入門の用語のなにが悪いのか
「Java本格入門」は入門書であるから、余計な概念を持ち込んで読者を混乱させることは好ましくないと思う。3-4-2 ジェネリクス(総称型) の章では込み入った構文までは踏み込まないのであるから、型変数を宣言する/バインディングするという概念を持ち込む必要はないのではないか。
「仮型パラメータ」という語は、(型変数ではない通常の)変数でいうところの「仮パラメータ」- 「実パラメータ」という対比で用語選定した上で「型」をつけたものだと思われる。この用語の用例はOracleの公式のドキュメントにも存在することは存在する(ジェネリック・インスタンス作成のための型推論)。しかし、そもそもこのあたりの訳語についてはOracle(や前身のSun)の公式の書籍などでも統一されていない。
先に挙げたドキュメントの英語版では"the formal type parameter", "the actual type parameters"という語を使っており、日本語ドキュメントでは「仮型パラメータ」「実型パラメータ」という訳語をあてている。Java言語仕様での用語とすでに違う。
型変数を宣言する/バインディングするという文脈を厳密に語ろうとすれば、これら混乱した用語の中から用語を、そしてその訳語を探さなくてはならない。これは入門書の領域を脱していると思う。なので単に「型変数」で済ませるほうが良いのではないか。だいたい「pushメソッドの引数」といった風に普通の変数については「引数」とざっくりした宣言/バインディングを意識しない表現をして、型変数についてだけ「仮型パラメータ」と妙に意識した表現をするというのは不自然ではないか。
また、ふわっと「ジェネリクスを定義するには」と表現している部分は明確に「型変数を宣言するには」あるいは、「総称型のクラスを宣言するには」といった表現を用いたほうがよいだろう。
「パラメータ化された型として定義します」の下りについては、Javaのジェネリクス関連用語として「パラメータ化された型」という専門用語が存在しており(私はこの1単語に見えない用語を嫌って「パラメタライズドタイプ」と表現する方がよいと考えている)これは本稿の例ではHoge<String> hoge = ... の Hoge<String>と解説した。型変数を加えることを「パラメータ化」という言い方をする用例もあるのだが、「パラメータ化された型」という専門用語が存在してしまっている以上、紛らわしい表現は避けるべきだろう。