Java Generics Hell メソッドスコープのジェネリクス

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

読者の推奨スキルとしては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)

https://docs.oracle.com/javase/jp/9/docs/api/java/util/Collections.html#replaceAll-java.util.List-T-T-

この例では戻り値は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日目。

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

ここまで話の簡単のためにいろいろ端折って書いてきているのだが、徐々に詳細を書かねばなるまい。同時に取り漏らしも回収して行かなければならない。

スコープ

Javaジェネリクスの型変数には2種類のスコープがある。

  • メソッドの範囲で有効な型変数
  • クラスのインスタンスの範囲で有効な型変数

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ジェネリクスの山括弧は主に3種類あり、役割が違う。また、ここではもっとも単純な例のみ挙げたが、実際には変性の指定やら、境界の指定やらあってもっといろんな書き方がある。また、メソッドスコープの場合も書き方が異なる。
しかしまずは構文を意識してソースを眺めることが大事だ。漠然と眺めていては理解がすすまない。まずは意識して構文をを見るようにしてほしい。

次回はメソッドスコープのジェネリクスについて触れたい。

Java Generics Hell - パラメタライズドタイプ

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

読者の推奨スキルとしては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日目。

読者の推奨スキルとしては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日目。

読者の推奨スキルとしては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の型システムはリスコフの置換原則に基づいている
  • 提供されるメソッドだけではなく、クラス全体の責務を踏まえて継承関係を作る必要がある

今回はまずJavaの型システムで重要な前提となる「リスコフの置換原則」について取り上げた。Javaのような継承をもつプログラミング言語ジェネリクスについて考える際にはこのリスコフの置換原則は重要で、ジェネリクスでの変性について考える際にまた振り返ることになる。

次回も型システムについてである。Javaジェネリクスは通常の参照型と代入互換性に違いがある。その理由について迫ろう。

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つ挙げるためにこじつけた感じがする。いわゆる動的言語などであれば「継承」を挙げるのが不適切となるので代わりに「クラス」とした、といったところなのではないか。

まとめ

さて、次回は型システムについて取り上げたい。まずは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が(ハイブリッドではない)完全オブジェクト指向言語ではないということは当時特別問題視されてはいなかったようにも思う。

JavaC++と同様に配列は宣言後に自由にサイズを変更することが出来ない。そうした「可変長配列」への欲求への答えとして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 Generics Hell アドベントカレンダーだが、次回以降は個別の具体的な話に進んでいきたい