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>と解説した。型変数を加えることを「パラメータ化」という言い方をする用例もあるのだが、「パラメータ化された型」という専門用語が存在してしまっている以上、紛らわしい表現は避けるべきだろう。
贖罪のイレイジャ
Javaのジェネリクスでしばしば話題に上がる「イレイジャ」について整理しておきたい。
イレイジャについては僕もいろいろと誤解しており、過去に誤った発言をしている。本エントリはその贖罪として書かれたものである。
「イレイジャ」という方式についてはネガティブな誤解が広まっていると思う。「イレイジャ方式」が問題の根ではない事象について、それを「イレイジャのせい」であると誤って理解することはエンジニアとしてはマイナスである。
しばしばイレイジャのせいとされる事象にnew T()できないという論点があるが、これはJavaのジェネリクスがC#でいうnew制約(型変数の制約としてデフォルトコンストラクタを持つことを要求する機能)を持たないことに起因する問題である。
そのため、この点についてJavaの言語仕様に改善を求めるのであれば、new制約を導入せよという現実的な要求とするべきである。
イレイジャ方式を採用したのは歴史上、Javaが初めてというわけでもなく、Java VM と .NET の IL がよく対比されるのは Java / .NET がそれだけ多く利用されているからなのだろう。
イレイジャの定義
まず「イレイジャ」(type erasuer:型消去)という用語の定義を確認したい。おそらく、多くの方のイメージとは食い違うことだろう。とにかく名前から「消える」のニュアンスが強すぎるきらいがある。
Java言語仕様より抜粋する。
(日本語訳を引用するため古いJava5時代の言語仕様から引用している。以後の言語仕様は日本語化されていない。本稿執筆時点最新のJava8のものはThe Java® Language Specification)
4.6 型のイレイジャ
型のイレイジャ(type erasuer:型消去)とは,型(パラメータ化型や型変数を含む)から型(パラメータ化型や型変数を含まない)への対応付けである。型Tのイレイジャは|T|と表記される。イレイジャの対応付けは以下のように定義される。・パラメータ化型(§4.5) G
のイレイジャは|G|である。
・ネストされた型T.Cのイレイジャは|T|.Cである。
・配列型Tのイレイジャは|T|である。
・型変数(§4.4)のイレイジャは,その最も左端の境界におけるイレイジャである。
・その他の型すべてのイレイジャは,その型自身である。メソッド・シグネチャsのイレイジャは,sと同じ名前,およびs中で指定されたすべての形式的パラメータ型のイレイジャからなるシグネチャとなる。
主にシグネチャについての話なのである。
イレイジャの理解
イレイジャを理解するために、類似性のあるものとしてメソッドのオーバーライド / オーバーロードを例に挙げよう。
Javaのメソッドは「メソッド名」と「引数の型」によって特定される。
プログラミング言語の種類によってはメソッドを「メソッド名」のみで特定して用いる言語もあり、その場合は同名で引数の型違いのメソッドを扱うことができない。
Javaの型(class / interface)は「型名」によって特定される(より正確にはパッケージ名を含む完全名のことである)。同名で型変数違いの型を扱うことはできない。
その「型名」をより厳密に定義したものが先に挙げた「イレイジャ」であり、
型Tのイレイジャは|T|と表記される
に従えば、例えばjava.util.ArrayList<String>のイレイジャは|java.util.ArrayList|であり、java.util.ArrayList<Integer>のイレイジャもまた|java.util.ArrayList|である。
Java5以前であれば「型」を特定するのに型の完全名さえあれば十分であった。Java5によってジェネリクスが導入されるとそれまでの「型」に「型変数」という要素が増えた。
この時に「型」+「型変数」で特定するような変更を行わなかった。「型」+「型変数」に対応づく「型(パラメータ化型や型変数を含まない)」つまり「イレイジャ」を使って特定することとし、既存の機能との互換性を保つ方針としたわけである。|java.util.ArrayList|で解決するわけだ。
これはメソッドのシグネチャについても波及して、hoge(List<String> list)とhoge(List<Integer> list)というようなオーバーロード定義ができないことに繋がる。いずれのイレイジャも|java.util.List|であるから。
classファイルに残されるジェネリクス
パラメタライズド・タイプを削ったシグネチャを「イレイジャ」と呼び、「イレイジャ」で型やメソッドを特定する(コンパイラの挙動からすると「解決する」という用語が適切か)わけだが、かといってclassファイルには「イレイジャ」だけが記録されていると考えるのは誤りだ。
classファイルのフィールドやメソッドシグネチャにはパラメタライズド・タイプなどの情報が残されている。以下はThe Java® Virtual Machine Specification Java SE 8 Editionからの引用である。(Java1.2のものまで日本語化されているのだが Amazon CAPTCHA 以降は日本語訳書は出ていない)
ClassSignature:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.9.1
[TypeParameters] SuperclassSignature {SuperinterfaceSignature}
TypeParameters:< TypeParameter {TypeParameter} >
なぜclassファイルにパラメタライズド・タイプのようなジェネリクスの情報が残されているのか。それはclassファイルだけが提供されているライブラリを利用するJavaコードを書いた際もまたジェネリクスによってコンパイラが型安全を担保するために必要だからである。
classファイルにする段階でメソッドhoge(List<String> list)をイレイジャにしてhoge(List list)にしてしまっていたら、このclassファイルを参照するjavaファイルをコンパイルするにあたって
List<Integer> list = new ArrayList<>(); hoge(list); // List<String>じゃないとダメ
をコンパイルエラーにできないではないか!
JavaのリフレクションでもまたClassやMethodからパラメタライズド・タイプを含む情報を取得することもできる。
イレイジャ方式というのは、例えるなら実行時にメソッド名+引数の型でメソッドを解決するのではなくメソッド名だけで解決するようなものだ。
そして、classファイルにはパラメタライズド・タイプなどの情報がしっかりと記録されている。そういうものなのである。メソッド名の例でいえば、メソッド名だけで解決する言語があったとしても引数の型の情報を残せないわけではない、ということだ。
イレイジャ方式というのはシグネチャの解決の話であるのだが、javaファイルがコンパイラによってclassファイルになる時点でシグネチャの型が消されることはない。
実行時のイレイジャ
Javaは実行時に用いるjava.lang.Classの情報をパラメタライズド・タイプ毎に持たない。ClassLoaderがClassを読み込む際には代表してイレイジャが用いられる。
そのため、パラメタライズド・タイプを多用してもClassが大量に読み込まれてMetaspace(Java7までだとPermanent領域)が圧迫される心配はない。
しかし同時に、実行時にObject#getClass()で得られるjava.lang.Classもイレイジャなのである。VM内でClassのロードやメソッドシグネチャの解決をするにはイレイジャがあれば十分なので、getClass()の機能性もJava1.4まで同等としたのだろう。
この点が語感もあって「イレイジャ」の本質であると誤解している人が多いのではないか。
実行時に消すから「イレイジャ」なのではなく、シグネチャ解決のためにイレイジャを用いるから動的に持ちまわっている情報もイレイジャまでで済ませている、ということである。
まぁ腐して言う人にしてみれば理由はなんであれ実行時に型が取れなければ一緒だろう :-P
イレイジャ方式のScala
Java VM 上で動くJavaより後の世代の言語に Scala が挙げられる。Scalaもまたイレイジャ方式のメソッド解決をする言語である。
Javaと同様にイレイジャが同一となるメソッドのオーバーロードができないという制約を持つ。
しかし、TypeTag / ClassTagを用いて実行時に型変数の型を得ることができる。つまりイレイジャ方式であっても、実行時に型変数の型を動的に得られるようにすることは可能である。
Javaが実行時に型変数の型を動的に得られないのは、実行時のイレイジャに対して、別途、型変数の型を渡す機構を作らなかったからと言えよう。現に、別途作ったScalaはやれているのだから。
実行時の型変数
実行時に動的に型変数の値を得られるようにするにはいくつかの方法論が考えられる。ジェネリクスの実装方式も含めていくつか挙げると
- シグネチャの解決に型変数も利用するようにする (非イレイジャ)
- コンパイル時に型のぶんだけコードの複製を作る (テンプレート方式)
- メソッド呼び出しに際してコンパイラが暗黙の変数を渡す (Scala方式)
これらはどれが一概に優れているとは言えない。いずれの方式でも相応の実用性を得ることができるだろうし、それぞれデメリットもある。
Javaはジェネリクスを導入するにあたって互換性を考慮してイレイジャ方式を採用したため、実行時に動的に型変数の値を得られるようにするには、必然的にScalaのような方式を取らざるをえないのであるが。
ただ、そもそも、ジェネリクスを静的な型チェックに重きをおくならば、実行時に動的に得る機能性というコードを書くことは下策なのである。だが、時にこの抜け道が役に立つ時がある。しかしこれはあくまでも抜け道であって、抜け道があることをジェネリクスの本質なのだ、などという主張は説得力に欠ける。
イレイジャ方式のメリット
イレイジャ方式のメリットとして挙げられるものは
があるだろう。
既存のVMに乗っかる形での言語拡張は、どうしてもその土台の言語のランタイムに引きずられる部分がある。型システムをより先進的なものに拡張しようとした場合、土台の言語のランタイムの型システム上にのせる必要がある。
この時、ランタイムが型+型変数(ただし土台の言語の型システムの型)にて解決する仕様だとすれば、より先進的な型を土台の言語のより古い型+型変数に対応付けなくてはならない。これは正により先進的な型システムから型情報を消し去る対応付けなわけで、イレイジャである。そして、完全なイレイジャではなく、土台の言語の型システムの型へと対応付ける部分欠損するイレイジャということになる。この部分欠損するイレイジャは、完全欠損するイレイジャに比べ扱いが面倒くさいものになることが想像できることだろう。
つまり、土台を変えない前提ならイレイジャとせざるを得ないのである。そして不完全なイレイジャが面倒ごとを持ってくることは容易に想像できるだろう。そのときにランタイムを一新する痛みを我慢できるかどうか、である。
高階
イレイジャと似て非なるものとしては、型の高階が挙げられるだろう。
この時、java.util.ArrayList<String>とjava.util.ArrayList<Integer>のイレイジャの|java.util.ArrayList|のようなものが必要になってくる。そしてこれはraw型とはまた違うものである。
結局のところ、Javaの世代の型システムは型の表現力が乏しいのである。
ジェネリクスのない時代は型の概念がシンプルだったが、型変数という「関節」のようなものが入ると、求められる型の表現が多様化し複雑化し、随分と難しくなってしまった。型クラスのような高階をやりだすとなおのことである。
まとめ
イレイジャ方式では、同名の型変数違いのclassを定義することはできないし、同名で引数のパラメタライズド・タイプの型が違うだけのメソッドオーバーロードをすることもできない。
これは正にイレイジャ方式を採用したが故の直接的な弊害である。
classファイルはパラメタライズド・タイプなど、ジェネリクスの情報はちゃんと保持している。保持しているからclassファイルだけが提供されているライブラリであってもコンパイラがジェネリックなメソッド呼び出しが安全かチェックしてくれる。
型変数にバインドされた型を動的に得ることのできるScalaのTypeTag / ClassTagのような機能性がJavaには提供されていないが、イレイジャ方式であるJava VM上で実装できないという意味ではない。
また、実行時にnew T()したいという要求の多くはC#のnew制約(該当の型がデフォルトコンストラクタを持つことを要求する制約)がないことに起因する。メソッド内でのインスタンス生成がしたいケースでは、Java8の現代では引数にSupplier<T>をとり、ラムダ式ないしメソッド参照を用いるのが柔軟性が高くシンプルで使い勝手が良いだろう。
この方式については過去に拙稿Java8流インスタンスを生成するメソッドの書き方 - プログラマーの脳みそで触れた。
しかしO/Rマッパーのようなフレームワークを作るケースではSupplier<T>方式が不便なときもあるだろう。こうしたケースではTypeTag / ClassTagのような機能性が求められることだろう。
入門書が教えてくれないJava スコープ編
入門書ではあまりとりあげられない部分を解説するコンセプトの「入門書が教えてくれないJava」シリーズの第二弾。前回は変数についてだった。今回はそのスコープについて取り上げたい。
スコープとは
スコープとは大雑把に言えば変数やメソッドなどが見える範囲のことを指す。
Javaの変数のスコープで一番簡単なのはローカル変数で、これは{から}まで(これをブロックという)の中で、宣言した位置より後ろで参照することができる。
public void main(String[] args) { int i1 = 0; // i1はここからmainの}までの間で参照できる if (true) { int i2 = 0; // i2はこのifの}までの間で参照できる } // i2はifの}を過ぎると参照できなくなる i2 = 1; // ← コンパイルエラー }
なお、Javaの言語仕様では{}のブロックは結構いろんなところに書けるが、ブロックとは違うものがちょっとだけある。
- クラス宣言等の{}
- 配列の初期化に用いる{}
逆説的だがローカル変数宣言ができるやつがブロックと覚えてもよい。ブロック内では文(このあたりは踏み込むとボリュームがあるので割愛。そのうち別稿を用意するかもしれない)を書けるところならifなどを伴わずに置くこともできる。
public void main(String[] args) { int x1 = 0; int x1 = 0; // ← 変数名が被るのでコンパイルエラー { int x2 = 0; } { int x2 = 0; // ← 先のx2はスコープ範囲外なのでOK } }
上記例ではx2をブロック内においてみた。x2が有効なスコープはブロックの内側だけなので、下で同じようにx2を宣言しているが名前が被らないのでOKとなる。
一般に長いメソッドを記述するのは良くないとされるが、どうしても長くなる場合は一時的に用いる変数をブロックを用いて局所的にしておくことで整理することもできる。ただし、そうした整理ができる人は、おそらくそのブロックを独立したprivateメソッドに切り出したくなるだろう。ブロックを用いた変数スコープの制御はそういう意味であまり脚光は浴びないものである。
スコープの全体像
さて、簡単な事例でスコープの概要がつかめただろうと思うので、スコープについての全体のイメージをここで抑えておこう。
Javaの場合、スコープについて考える場合、影響する要素は以下の様なものがある。
- ローカル変数、インスタンスフィールド、static フィールド
- 参照箇所がインスタンスメソッドか、staticメソッドか
- 継承階層
- アクセス修飾子 (public, protected, private, パッケージプライベート)
- 内部クラス(インナークラス)
- 匿名クラス・ラムダ式
- モジュール (Java9の新機能。参考: 「JavaOne 2015」レポート、注目浴びたProject Jigsaw | 日経 xTECH(クロステック))
今回、Java9のモジュールについては述べない。当エントリの時点ではまだJava9はリリースされていないが、新機能については試作版が提供されているので興味がある人は試してみて欲しい。
なぜスコープを用いるのか
あの変数を参照したいのに、コンパイルエラーになって参照できずに困ったことがあることだろう。Javaのオブジェクト指向に触れた時、必ず通る道だと思う。なんでわざわざ参照できなくするんだ。全部参照できたら便利なのに!
教室を表現したclassを例にしてみよう。クラスと書くと紛らわしいので「教室」としている。JavaのクラスはClassと書くので混乱しないようにして欲しい。朗読して読み聞かせをしている人は諦めて文字で文章を読んでもらおう :-P
なお、オブジェクト指向の比喩というのは往々にして正しくオブジェクト指向を表現できないものである。あくまで比喩であることを予め断っておく。
教室があったとして、ここでは適当に3年B組としておこう、出席番号3番の人を探そう。
出席番号3番の人は3年B組には一人かも知れないが、3年A組にもいることだろう。2年生にも1年生にもいるだろうし、他の学校にもいるだろう。
いやいや、今話しをしているのは3年B組のことだった。よその出席番号3番は関係無い!関係無いんだから引っ込んでいろ!
これがスコープで「見えなくする」動機付けなのである。
要するに、世の中のありとあらゆる出席番号3番が同じスコープにいると、ごちゃごちゃになるのだ。「今話題にしているところ」だけをスコープにして、関係無いところは無視したいのである。ごく小さいプログラムを組んでいる時には変数は全部参照できたら便利なのにと思うかもしれないが、ちょっと規模が大きくなってくると、余計なところは無視したい!という要求が強くなってくる。
大きなプログラムを組むにはプログラムの整理術が必要になる。その整理術のひとつがスコープの管理なのである。
ローカル変数、インスタンスフィールド、static フィールド
ローカル変数については先に述べた。ローカル変数はローカルであることが大事である。ローカル変数は中の仕組みみたいなもんだから外からそれを意識ないで済むのがよい。家電の類はだいたいあんまり中の仕組みを知らなくても使えるように設計されている。トースターでパンを焼くのにいちいち中の仕組みなんて考えないで済むのが理想なのである。
インスタンスフィールドは、classのインスタンスが持つ情報である。先ほどの教室の例でいえば、3年B組というのがインスタンスで、教室に在籍している生徒はインスタンスフィールド(どういう形で格納するかは別途考えて欲しい)に持つ情報ということになる。
対してstaticフィールドというのは比喩が難しい。「教室」というclassそのものに保持する情報ということになる。一般にはこのclassを扱う場合に必要となる定数などの定義をすることはあるが、static「変数」を定義することは稀。
参照箇所がインスタンスメソッドか、staticメソッドか
さてそんなフィールドたちだが、どこからアクセスできるかを考える場合、参照しようとするコードがインスタンスメソッドか、staticメソッドかというのが影響してくる。
が、まずは一般論としてのルール。インスタンスフィールドにアクセスするには、オブジェクト.フィールド名でアクセスする。staticフィールドにアクセスするにはクラス名.フィールド名でアクセスする。
- オブジェクト.フィールド名
- クラス名.フィールド名
この時、インスタンスメソッドでは自分自身のインスタンスを指し示すthisというキーワードが使える。
- this.フィールド名
そして、this.は省略可能だ。
- フィールド名
また、staticフィールドも、自分自身のclassのstaticフィールドだとクラス名を省略することができる。
- フィールド名
このように、省略した結果、インスタンスフィールドへのアクセスと、staticフィールドへのアクセスが同じ形になってしまう。なので、コードを読み解く場合は逆のルールでこのフィールド名はインスタンスのものか、staticなものかを探すことになる。
IDEを使っている場合はだいたい色分けしてインスタンスフィールドかstaticフィールドかわかるようにしてくれるので混同することはないと思う。
インスタンス内からは同一インスタンスのフィールドをthisを省略してアクセスできるが、慣れるまではthisを補ったほうがスコープについてはわかりやすいと思う。3年B組がthisである場合に、3年A組のフィールドにアクセスする、といった事例でも省略形のフィールド名だけのアクセスで考えるのではなく 3年B組.担任 と 3年A組.担任 であると考えるほうが混乱がないように思う。
インスタンス間でデータが混ざらないということは整理術として非常に重要なポイントとなる。
継承階層
前段ではごく簡単にインスタンスフィールドとstaticフィールドのアクセスについて書いたが、実際には継承階層がある場合、親クラスおよびインターフェースにあるものも参照できるので話がもうちょっとややこしい。
基本を言えば
- 親のクラスで宣言されたインスタンスフィールドは、子供のクラスで参照できる
- 親のクラスで宣言されたstaticフィールドは、子供のクラス名.フィールド名でアクセスできる
このうち、特にstaticフィールドについては実際にstaticフィールドが宣言されているクラス.フィールド名でアクセスすることがよしとされている。なお、子供のクラス内からだとクラス名.が省略できるが、この省略をしたいがために継承をするというのは一般にNGとされる。
なおJava5からはstaticインポートという機能があるので、継承階層にないstaticフィールドをフィールド名だけでアクセスしていることがある。
アクセス修飾子
アクセス修飾子については入門書で必ず記載があることだろう。public, protected, private, パッケージプライベートというやつである。
ごく小規模なプログラムだとパッケージプライベートを使わないことも多いかもしれないが、いくつかのクラスの集まりをパッケージにまとめる設計をし始めると、むしろパッケージプライベートを多用することになる。
とは言え、パッケージをひとつのモジュールとして中の仕組みを隠蔽しよう、中の仕組みを知らなくても使えるようにしようとするには、この4つのアクセスレベルでは力不足なのであって、Java9で導入されるモジュール機能というのはそれを改善するためのものなのだ。
内部クラス
スコープの話で特殊かつ入門書ではスルーされるものとして本稿で解説しなければならないものに内部クラスが挙げられる。
内部クラスについては拙稿Javaのクラス宣言5種+α - プログラマーの脳みそでも取り上げているが、ここの内部クラス(Inner class)の部分がこの話題となる。
内部クラスは教室の例でいえば、班みたいなものとしてイメージしてもらいたい。3年B組という教室のインスタンスがあって、そこに属する班が複数ある。3年B組の1班、2班といった形だ。班というインスタンスが教室というインスタンスにぶら下がっている形なのが特徴だ。
班型からみた教室型のことをエンクロージング型(Enclosing type)と呼ぶ。3年B組の1班から見た3年B組はエンクロージングインスタンスというわけだ。このとき、1班からは3年B組の情報が直接参照できる。
ただし、1班から2班を直接参照することはできない。変数名.フィールド名といった形で2班.メンバーといった参照であれば参照可能だが、このときはまず1班はなんらかの形で2班インスタンスを持ってこなくてはならない。やりたければ直接参照可能な3年B組インスタンスから2班インスタンスを得て参照するようなオブジェクトの引き回しが必要になる。
内部クラスのスコープはちょっと独特だ。
内部クラスの内部クラスといったものも宣言することができる。この場合は内部クラスの内部クラスから外側の内部クラスのインスタンスを直接参照することができる。また一番外側のクラスのインスタンスも参照できる。
匿名クラス・ラムダ式
匿名クラス(Anonymous class)についても拙稿Javaのクラス宣言5種+α - プログラマーの脳みそで取り上げているがここでも簡単に触れておこう。
匿名クラスの宣言時には、外側のブロックで宣言されているfinalな変数を参照できる。finalでなくてはならない。
public static void main(String[] args) { final int i = 42; Runnable r = new Runnable() { @Override public void run() { System.out.println(i); } }; // 上記のrun()はここで呼び出される r.run(); }
なぜfinalじゃないといけないのかという話もいろいろ議論があるのだが、Javaのスタンスの現れと捉えておくのが無難だろう。finalな変数は再代入はできないが、代入されたオブジェクトの内容を更新することはできる。なので以下の様なコードで匿名クラス内部から外側のスコープの変数を更新することも可能ではある。
public static void main(String[] args) { final int i[] = {42}; Runnable r = new Runnable() { @Override public void run() { i[0] += 10; } }; // 上記のrun()はここで呼び出される r.run(); System.out.println(i[0]); // 結果は52 }
しかし、このようなコードは「整理術」としてはNGであるというのが主流の考え方だ。コード的に実装可能であっても避けるべきである。
さて、ラムダ式についても触れておこう。ラムダ式でも匿名クラスと似たように外側のブロックで宣言されている変数を参照できる。
しかし、ラムダ式の場合はfinalではない変数でも参照できる。
int i = 42; Runnable r = () -> System.out.println(i);
しかし、これは外側のブロックの変数を変更できるという意味ではない。該当の変数にfinal修飾子をつけたとしても大丈夫な場合にのみ、ラムダ式内で外部のブロックの変数を参照できるようになっている。これは実質的finalと呼ばれる。
int i = 42; Runnable r = () -> System.out.println(i++); // コンパイルエラー
そのため、ラムダ式で外側のブロックの変数を変更した場合、上記の例ではi++となっているが、その場合はコンパイルエラーとなるし、
int i = 42; i = i+10; Runnable r = () -> System.out.println(i); // コンパイルエラー
変数が宣言されたあとに再代入されている場合(上記の例ではi+10で再代入されている)もコンパイルエラーとなる。
なので、Java言語の立ち位置は変わらず、ただシンタックスシュガーとして構文を簡素で便利にしているだけ、と捉えるのが妥当ではないかと思う。
メソッド、型変数、etc.
ここまで変数・フィールドのスコープを中心に見てきたが、スコープというと何も変数に限って使われる言葉ではない。
メソッドを参照できる、できないといった場合にもスコープと表現されるし、ジェネリクスの型変数についても参照範囲についてはスコープと表現する。またマイナーではあるが、ローカル内部クラス(Javaのクラス宣言5種+α - プログラマーの脳みそ参照)に至っては宣言されたメソッド内だけが有効なスコープとなる。
いずれも、基本的には「関係ないものは見えなくする、関係あるものだけが見える」という状態を保つことで、プログラムを整理する技なのである。大量のハズレの選択肢から当たりを探すような作業はプログラミングの妨げでしかない。
ごく小規模なプログラムならいざしらず、ちょっと大きくなってくると途端に整理整頓が必要になってくる。変数のスコープは小さくという先人の知恵をぜひ活かして欲しい。納得が行かない人は心ゆくまで試行錯誤してみるのも良いかもしれない :-)
入門書が教えてくれないJava 変数編
春なのでJava入門的なことを書こうと思い立ったので、入門書ではあまりとりあげられない部分を解説するコンセプトの入門記事を書いてみようと思う。(←ひねくれ者)
対象読者としては、Java言語の基礎を学んだがもう一歩踏み込んだ話が知りたいぐらいの初学者〜中級者を想定してる。上級者の方は記述に誤りがないかチェックしていただければ幸いだが、説明を簡単にするためにいろいろ端折っている点はご理解いただきたい。
今回は変数・フィールド編とした。筆者のやる気次第だがこのシリーズでいくつか書こうと考えている。
- 入門書が教えてくれないJava 変数編 (本稿)
- 入門書が教えてくれないJava スコープ編 - プログラマーの脳みそ
初期値を指定しない変数宣言
変数宣言に際して初期値を設定しないことができる。
int i;
この場合、このint i;がフィールドであるか、ローカル変数であるかで扱いが変わってくる。
iがフィールドであれば、intなどのプリミティブ型であれば0 (boolean型ならfalse) で参照型ならnullの扱いとなる。(インスタンスフィールド、staticフィールドともに同じ)
メソッド内などのローカル変数の場合、使用する前に値を代入して初期化しなければならない。
if文などの分岐で初期化がされない可能性がある場合はコンパイルエラーとなる。
int i; if (flag) { i = 1; } System.out.println(i);
上記の例だとflagがfalseの場合、初期化されずにSystem.out.println(i);へと進むことになるのでエラーとなる。
細かいことを言えば、初期化がされてなくても使用されない変数であればコンパイルエラーにはならない。(が無駄なので書かないほうがよい)
ローカルfinal変数の初期化
通常の変数と基本的に変わらないが、2回目の代入をしようとするとコンパイルエラーになる。
static final フィールドの初期化
static final なフィールドの初期化は、クラスの初期化が終わるまでにされなければならない。*1
final変数の初期化ではフィールド宣言と同時に
public static final String NULL_PO = "GA!";
といったように値を設定する使い方が多いと思うが、時には複数行にまたがる演算によって初期値を設定したくなることもある。
public static final String START_DATE; public static final String END_DATE; static { LocalDate d1 = LocalDate.of(2016, 4, 1); START_DATE = d1.format(DateTimeFormatter.ISO_DATE); LocalDate d2 = d1.plus(75, ChronoUnit.DAYS); END_DATE = d2.format(DateTimeFormatter.ISO_DATE); }
このような時は、クラスの初期化が終わるまでにfinal変数に値を代入すればよい。具体的にはstatic初期化ブロックを用いる。
final変数への代入は一度きりである必要がある。2回代入しようとすればコンパイルエラーになるし、初期化忘れもコンパイルエラーとなる。
インスタンスの final フィールドの初期化
インスタンスのfinalフィールドの初期化は、インスタンスの初期化が終わるまでにされなければならない。
これは3つのやり方がある。
インスタンス初期化ブロックは先にあげたstatic初期化ブロックと似ている。static {} からstaticを取り除くとインスタンス初期化ブロックになる。
public class Hoge { final int i; { // ここがインスタンス初期化ブロック i=1; } }
配列の[]
Javaは言語の構文はC言語に似せてある。その関係で配列の変数の宣言は少々特殊な書き方ができる。
int[] i1 = null; int i2[] = null; int[] i3[] = null;
Javaの構文は一般に最初に宣言型を書き、次に変数名を書き、それから = 初期値となるが、C言語の影響で変数名の後に[]を記述して配列を宣言することができる。そのためint i2[]のような記述でint配列の宣言を行うこともできるのだが、Cから受け継いだ負の遺産だと思って使用しないようにしよう。
i3の場合はint[][]型となるのだが、紛らわしいので当然書くべきではない。
変数の初期化時だけで使える構文
配列の初期化について、変数の初期化時だけで使える便利構文がある。
String[] s0 = new String[2]; String[] s1 = new String[]{"one", "two"}; String[] s2 = {"one", "two"};
通常、配列を作成する場合はnew String[2]のように添え字に配列のサイズを指定する。
s1の例のように添え字なしでnew String[] とし、直後に{}で初期値を記述する書き方もできる。
さらに、変数の宣言への代入に限りs2のようにnewと型の記述を省略していきなり{"one", "two"}といった記述をすることができる。
変数のtransient
比較的マイナーなJavaの予約語にtransientというものがある。変数宣言に付記することができるのだが
transient String text;
これは、オブジェクトを直列化する際にこのフィールドは無視するという設定である。
直列化とは何か?という話は別途調べていただきたいが、大雑把に言えばオブジェクトをbyte配列に変換して保存したり別のJavaVMに送信したりするこのできる機能である。(参考:Java直列化メモ(Hishidama's Java Serializable Memo))
冗長だが扱いやすいように加工したデータをフィールドに持っているような場合、それらをtransientキーワード指定することがある。もっとも直列化機能を使わない限りはあまり気にすることはないのだが。
変数のvolatile
比較的マイナーなJavaの予約語にvolatileというものがある。これはマルチスレッドに関連しており、端的に言えばこのフィールドを操作するにあたってメモリの同期化がされるようになる。
Javaはマルチスレッドで動作している場合にメインメモリからコピーをもってきて操作するような動きをする。そのため、複数のスレッドの間で違う値を見ているということが起きる。じゃあvolatileをつければとにかく大丈夫なのかというと、そんなに単純な話ではないので別途マルチスレッドの解説を読んでいただきたい。
変数のハイディング
変数・フィールドは同名のものを宣言できるケースがある。
同名の宣言で覆い隠すことをハイディングという。
- 親クラスのstaticフィールドと同名のstaticフィールド
- 親クラスのインスタンスフィールドと同名のインスタンスフィールド
- 親クラスのstaticフィールドと同名のインスタンスフィールド
- 親クラスのインスタンスフィールドと同名のstaticフィールド
- 同クラス/親クラスのstaticフィールドと同名のローカル変数
- 同クラス/親クラスのインスタンスフィールドと同名のローカル変数
- 外部クラスのインスタンスフィールドと同名の内部クラスのインスタンスフィールド
基本的にstaticフィールドはクラス名.フィールド名でアクセスできるので、親クラスのstaticフィールドをハイディングしてもアクセスしたいときは明示的に親クラス名.フィールド名を書けば済む。
そのオブジェクトでのインスタンスフィールドをアクセスしているのだ、ということを明示的に表現したい場合はthis.フィールド名といったようにthisキーワードを用いて明示することができる。
親クラスのインスタンスフィールドと現在のクラスのインスタンスフィールドが同名となった場合、super.フィールド名で親クラスのフィールドを参照することができる。現クラスのほうはthis.フィールド名で明示するとよいだろう。superによるアクセスは当クラスからのみで、外部からオブジェクトの親のフィールドをsuperで参照するようなことはできない。
また、3段以上に継承している場合、superが参照するのはひとつ上の階層となる。多重ハイディングしている場合に階層ごとに呼び分けるようなことはできない。
内部クラスから外部クラスの同名フィールドを参照したい場合、外部クラス名.this.フィールド名とすることで呼び出すことができる。まれにこの記法を知らないがために迂回路となるアクセサ・メソッドを用意したりしているソースコードを見ることがある。ダサいといえばダサいが、そんなに実害のあるものでもない。
おわりに
入門書ではスルーされがちな部分をいくつかピックアップしてみたが、楽しんでいただけたであろうか。訂正はもちろん、リクエストなどもあればコメントいただきたい。
*1:このあたりクラスロードとClassLoaderについてはざっくり割愛しているので上級者の皆様におかれましてはblogエントリなどでの解説を試みていただきたく