Java Generics Hell - インスタンススコープのジェネリクス

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

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

読者の推奨スキルとしては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の型システムの基礎となるリスコフの置換原則である。