贖罪のイレイジャ
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エントリなどでの解説を試みていただきたく
セミコロンレスJavaコンパイラの設計
すいません。完成まで漕ぎ着けれませんでした。
本稿はセミコロンレスJava Advent Calendar 2015の25日目です。
セミコロンレスJavaについて簡単に説明すると、Javaの構文上のセミコロンを用いないで標準APIの範囲でプログラミングをするにはどうしたら良いかを考える衒学的な娯楽です。
アプローチ
Oracle版のJDK (Java Development Kit)に標準で入っているcom.sun.tools.javac.main.JavaCompilerを使います。JDKをインストールするとtools.jarというのがJavaのインストールフォルダ以下に展開されていると思います。Windowsだと C:\Program Files\Java\jdk1.8.0\lib あたりですね。
このtools.jarに含まれているJavaCompilerを使うとJavaのソースコードをコンパイルしてAST(abstract syntax tree)抽象構文木を得ることができます。抽象構文木は言語の構文を木構造のノードで現したもので、この抽象構文木をGoFのVisitorパターンをつかって走査して、それを元にセミコロンレスJavaのコードを吐き出す、という目論見です。
厄介なのは、識別子の解決で、import文とクラス名から対象のクラスの完全名を当てたりするところですね。まだコンパイルしていないコードに存在しているクラスとかもあるので、まずは一回目のVisitorで識別子の一覧を作って、二回目のVisitorで実際の処理を行うような工夫が必要です。
古いC言語のヘッダファイルのように、宣言だけくくりだしてあるとコンパイルの処理が楽になります。一回中間コード(オブジェクトファイル)に置き換えてから識別子の接続をすればメモリも食わずに済む。このあたりが古いC言語の時代のコンパイラとリンカの存在理由なのかなと思います。
でもソースコードを書く側は面倒くさいですよね。Javaの時代にはCPUパワーがやや向上しましたしメモリも増えてきていましたから、ヘッダファイルなし、中間コードの生成なしにコンパイルしてくれるようになりました。便利です。
クラスの一般セミコロンレス化
セミコロンレスJavaの技法はいろいろと工夫されていますが、コンパイラを作ろうとする場合、一般化が大事です。よりよいコンパイラは最適化を行うでしょうが、まずは一般化してコンパイル出来るようにすることが重要です。
例として以下の様なクラスを検討しましょう。
public class HelloObject { /** フィールド */ public String name; /** コンストラクタ */ public HelloObject(String name) { this.name = name; } public void syso() { System.out.println(hello()); } public String hello() { return "hello "+name; } public String hello(String message) { return "hello "+name+", "+message; } }
フィールドがあって、コンストラクタがあって、インスタンスメソッドが3つある、普通のJavaのクラスです。しかし、セミコロンレスJavaをかじった人であれば、Javaのクラスそのものをクラスの機能を保ったままセミコロンレス化することが困難であることは容易に想像が付くことでしょう。
- フィールドの宣言が出来ない
- 戻り値のあるメソッドの宣言が出来ない (return文が書けないため)
というのはよく知られたところであります。
上記クラスをセミコロンレス化すると以下の様な形になります。以下は読みやすいようにimport文は残したままにしています。
import java.util.HashMap; import java.util.LinkedList; import java.util.stream.Stream; @SuppressWarnings("serial") public class HelloObjectSlj extends HashMap<String, Object>{ // public String name; /** コンストラクタ */ public HelloObjectSlj() {} public HelloObjectSlj(String name) { if (this.put("name", name) == null) {} } public void syso() { try (Stream<?> stream = Stream.of().onClose(()->System.out.println(new m0_hello().pop()))) {} } // public String hello() { public class m0_hello extends LinkedList<String>{ public m0_hello() { try (Stream<?> stream = Stream.of().onClose(()->m0_hello(this))) {} } } public void m0_hello(LinkedList<String> ret) { if (ret.add("hello "+ (String)get("name"))){} } // public String hello(String message) { public class m1_hello extends LinkedList<String>{ public m1_hello(String message) { try (Stream<?> stream = Stream.of().onClose(()->m1_hello(this, message))) {} } } public void m1_hello(LinkedList<String> ret, String message) { if (ret.add("hello "+ (String)get("name") +", "+message)){} } }
クラスの宣言
セミコロンレスJavaではフィールドの宣言が出来ませんし、戻り値の返るメソッドの宣言が出来ませんから、一般にクラスを用いたオブジェクト指向プログラミングは不可能であると考えられているかと思います。そのため、どうしてもセミコロンレスJavaで実装されるコードは小規模にとどまっていました。
プログラミングの歴史を辿れば、オブジェクト指向によるコードのモジュール化は、大規模なコードを整理整頓するために役立ち、巨大なシステム開発の土台となりました。
まずはフィールド宣言ですが、フィールドの宣言が出来ないためHashMap<String, Object>を継承することでフィールドに代えます。フィールド名をキーにして値をputすることでフィールドへの格納とし、フィールド名をキーとしたgetでフィールドの参照に代えます。
HashMapを継承するとjava.io.Serializableをimplementsすることになるため、EclipseなどIDEが警告を出すかと思います。そのため@SuppressWarnings("serial")をつけています。詳しくはEffectiveJavaあたりを参照して欲しいのですが、直列化のバージョンID (serialVersionUID) を宣言することで解消するのが筋なのですけれども、そのためにはフィールド宣言が必要になるためセミコロンレスJavaでは無視するより仕方がないのです。
戻り値のないメソッドの宣言と文の実行
戻り値のないメソッドの宣言はそのまま行うことが出来ます。
メソッド中の式(要するに値として評価されるもの)の記述は
if ((/* 式 */)==null) {}
というif文を用いる記法が一般的かと思います。
困るのは文で、いろいろな工夫がされて来たのですが、Java8ではラムダ式を用いて
Runnable r; if ((r = ()->System.out.println("hello!"))==null) {}
といったようにRunnableとなるラムダ式を用いれば文末のセミコロンを取り除くことが出来ます。
しかし、宣言されたRunnableのrun()メソッドを呼び出す場合、戻り値がvoidですから呼び出しに困ります。
Threadを作って実行すると同一スレッドで順次処理出来ないので甚だ不便です。
そこでExecutors#callable()を用いることで戻り値のあるCallableに変換した後、Callable#call()を呼び出すという手法を使っていましたが、この技法の欠点はCallable#call()がthrows Exceptionであるという点です。
これを克服するために開発された技法がStreamの(正確にはその親であるBaseStreamの)onClose()を用いる方法です。
try (Stream<?> stream = Stream.of().onClose(()-> /* 文 */ )) {}
StreamのonClose()は、引数として渡したRunnableを、まさにStreamのclose()メソッドが呼び出された時に実行するわけですが、このclose()はAutoCloseableから継承されたclose()ですので、try-with-resources構文で呼び出すことが出来ます。
余計なthrowsもなく、(他スレッドではなく)同期的に実行できる点でこの技法は優れています。
戻り値のあるメソッドの宣言
戻り値のあるメソッドはreturn文が書けないため宣言できないというのがセミコロンレスJavaでの常識でありました。克服するための技法としては第一引数に値を返せる参照をもらって副作用をもたらし呼び出し元に値を伝えるという技法が考えられます。
例えば配列を使って
public void zzz(String[] ret) { if ((ret[0] = "zzz") == null){} }
といった形で値を返す、というわけなのですが、このメソッド宣言では呼び出し側が甚だ使いにくいわけなのです。
for(String[] s : new String[][]{new String[1]}) { try (Stream<?> stream = Stream.of().onClose(()->zzz(s))) {} try (Stream<?> stream = Stream.of().onClose(()->System.out.println(s[0]))) {} }
zzz(new String[1]) という形でnewして渡しっぱなしにすると戻り値が取れないんですね。なのでいったん変数を宣言して、zzz(s)と呼び出して、そこから連鎖的に呼び出しができないので改めてSystem.out.println(s[0])とするような…。
こんなの、コンパイルの一般規則として使えないじゃないですか。
そこで古くよりjava.util.LinkedListを継承した内部クラスや匿名クラスのコンストラクタを用いてthisに処理をaddさせ、newして出来たオブジェクトからpop()して用いるということが行われてきたわけです。コンストラクタが愛しいですね。
ところが、ただ内部クラスにしてしまうと困ったことが起こるのです。
ポリモフィズム
単に内部クラスとして
public class HelloObjectExSlj extends HelloObjectSlj { // public String hello() { public class m0_hello extends LinkedList<String>{ public m0_hello() { if (add("こんにちは "+ (String)HelloObjectExSlj.this.get("name"))){} } } }
というようにすると、ポリモフィズムがされず、宣言型によって静的にHelloObjectSlj.m0_hello型とHelloObjectExSlj.m0_hello型が呼び分けられるようになってしまうんですね。
例えば以下のようにh型と、実体が違う組み合わせを実行すると
// 親の型 for (HelloObjectSlj h : new HelloObjectSlj[]{new HelloObjectSlj("なぎせ")}){ try (Stream<?> stream = Stream.of().onClose(()-> System.out.println(h.new m0_hello().pop()) )) {} } // ポリモフィズムのつもり for (HelloObjectSlj h : new HelloObjectExSlj[]{new HelloObjectExSlj("なぎせ")}){ try (Stream<?> stream = Stream.of().onClose(()-> System.out.println(h.new m0_hello().pop()) )) {} } // 子の型 for (HelloObjectExSlj h : new HelloObjectExSlj[]{new HelloObjectExSlj("なぎせ")}){ try (Stream<?> stream = Stream.of().onClose(()-> System.out.println(h.new m0_hello().pop()) )) {} }
hello なぎせ
hello なぎせ
こんにちは なぎせ
といった結果になり、2つめのケース、HelloObjectSlj hで変数が宣言され、new HelloObjectExSlj()としている、要するにポリモフィズムして欲しいパターンでポリモフィズムしないわけなんです。内部クラスのnew演算子は静的に決められてしまう。
そこで、呼び出しやすいように内部クラスを宣言しつつ、実装は普通のメソッドにしてそちらを呼び出すようにしておく
// public String hello() { public class m0_hello extends LinkedList<String>{ public m0_hello() { try (Stream<?> stream = Stream.of().onClose(()->m0_hello(this))) {} } } public void m0_hello(LinkedList<String> ret) { if (ret.add("hello "+ (String)get("name"))){} }
class m0_helloをnewすると、その内部から m0_hello(LinkedList<String>)が呼び出されるわけです。このような仕込みをした上で、次のように子クラス側でオーバーライドをしてやる
@SuppressWarnings("serial") public class HelloObjectExSlj extends HelloObjectSlj { /** コンストラクタ */ public HelloObjectExSlj() {} public HelloObjectExSlj(String name) { if (this.put("name", name) == null) {} } @Override public void m0_hello(LinkedList<String> ret) { if (ret.add("こんにちは "+ (String)get("name"))){} } @Override public void m1_hello(LinkedList<String> ret, String message) { if (ret.add("こんにちは "+ (String)get("name") +", "+message)){} } }
このようにすることで、
hello なぎせ
こんにちは なぎせ
こんにちは なぎせ
といったように、正しくポリモフィズムさせることが出来るようになるのです。
そうそう、コンストラクタのところでデフォルトコンストラクタを用意する諸般の事情というのは継承をした時に引数付きのsuper()を呼び出せないという事情です。継承しないと気が付かないところですね。
まとめ
本稿では一般的なJavaのオブジェクトをセミコロンレス化させる技法について検討しました。それにより
- クラスのフィールドを表現することが出来る
- 戻り値を返すメソッドを表現することが出来る
- 戻り値を返すメソッドをオーバーライドすることが出来る
ということを示しました。
おまけ
抽象構文木をVisitorで走査する場合、com.sun.source.tree.TreeVisitorインターフェースを直接implementsせずcom.sun.source.util.TreeScannerクラスを継承して実装するのが一般的かと思いますが、既存クラスの継承をして戻り値のあるメソッドをオーバーライドすることがセミコロンレスに行えないのでcom.sun.source.tree.TreeVisitorインターフェースをimplementsすることになります。
この時、java.lang.reflect.Proxyを用いて動的にインターフェースの実装を作り処理をInvocationHandlerに委譲させます。InvocationHandlerは関数型インターフェースなのでラムダ式で記述することができ、戻り値を返す実装をすることができます。
うまく実装を書いてやれば、セミコロンレスJavaコンパイラによってセミコロンレスJavaコンパイラ自身のソースコードをコンパイルすることが出来るようになるでしょう。
ブリ会の振り返り
富山で寒ブリを買い付けて東京に運び、料理ハンズオンというか、要するにホームパーティーをやろうという企画の振り返り。あくまでもエンジニアの個人的なホームパーティーなのでその点はひとつご理解を。
kamekoopa氏の振り返りも参考に。本稿のブリの写真は @usaturn の撮影したものを使わせてもらっています。
季節的制約
食材について、魚は冬が旬のものが多い。日本海の寒ブリもそのひとつである。寒ブリの季節は12月〜2月あたり。開催時期は冬に限られる。
冬であるもうひとつの必然性は気温。魚の輸送に際して暖かい時期は保冷が難しくなる。冬場の輸送であれば保冷がしやすいという利点がある。
距離的制約
今回は一般的な食品流通で食材を調達した。具体的には大阪屋ショップ。観光客向けの新湊きっときと市場やひみ番屋街などを使う手もあるが、総じて観光客向けの売り場は価格が高い。型の良い一級品が揃うためで、普通に食べる分には一般的なスーパーマーケットの流通分でも十分。普通にブリの一匹買いができる大阪屋の大きめの店舗を利用した。比較的小さめの店舗でも事前に注文しておけば一匹仕入れて用意してくれるだろう。
スーパーマーケットで調達する場合、開店待ちで乗り込むとしても開店が9:30とかそのぐらいなので、買い物を30分で済ませて即時出発したとしても最速で10:00発ぐらいになる。
実際には一旦自宅にもどり、嵩張るため食品トレイなどを乗り除きクーラーボックスに移し替える作業などを行った。10:30を目標にしていたが色々手間取り11:00の出発となった。
富山から東京までの道のりは高速道路で400kmぐらいである。高速道路が100km/hなので4時間で移動できるというのは算数のたかしくんの世界の話で、実際には休憩時間などもあるので5時間はかかる。またICまでの道のりや、ICからの道のりもあるので更に+1時間はみて6時間ぐらい。11:00出発だとうまくいって17:00着といったところなのだ。
今回は八王子のあたりから渋滞に巻き込まれた。そのため到着が18:30ごろになってしまった。そのため、調理〜食事までが慌ただしくなってしまってゆっくり談話できなかった。もっと到着が遅れていたら会自体が成立しなかったことだろう。なんとかなってホッとしている。
会場は17:30-21:45までを抑えてもらっていたが、魚の到着が1時間おくれて賞味3時間となった。3時間だと食事をゆっくりしている時間がなく、慌ただしく片付けに突入、会場を引き渡して路上で料金精算をしていた有様である。この手のホームパーティーを企画する場合、賞味4時間は時間をとれるようにした方がいい。欲を言えばもっと長く時間をとったほうが良いだろう。
天候という不確定要素
今回は天候に恵まれたのだが、場合によっては積雪もありうる。積雪の高速道路は60km/h程度の速度制限がかかる。積雪時の北陸自動車道はこんな感じ。
富山県は積雪が多いことで有名だ。世界的な豪雪地帯であって一晩で30cm程度積もることは珍しくない。
長距離の輸送時間はリスクが高い。仕入れから出発までのロスが少なくなるように工夫する必要がある。また、魚屋さんとネゴって仕入れをしてもらい早朝に受け取るなどの工夫が必要になるだろう。
物理的な距離の都合上、車での輸送を前提とすると富山-東京間というのはおそらくギリギリ成立する限界点に近い。開催地を東京として考えるならば、輸送の物理距離からして新潟あたりから輸送するほうが余裕が有るだろう。
駐車場と宿泊施設
都内の宿泊施設、近年埋まりが早いというのは今回痛感。早めに予約しないと宿が確保できない。会場に近くて駐車場もついていれば最高なのだけど、なかなかそこまでの好立地はない。
駐車場は一晩車を預けれる露天のコインパーキングが適している。上限金額が設定されているものが最適。機械式駐車場だと荷物の出し入れに苦労するので向いていない。
駐車場は遠隔地からネットで情報を漁るのが難しく、また事前にあたりをつけていた駐車場はそこまで路地が細くて進入をためらわれたので回避したということもあり、現地スタッフに下調べしてもらえると助かると思った。
特別な事情がない限り、都心部に車で行くものではない。
調理
参加してくれた皆さん、全般的に調理スキルが高くて助かりました。
仕入れ食材が当日にならないと確定しないというのもあったのですが、候補の魚の処置の仕方について事前に共有できていればよりスムーズだったかと思います。柵取りまでしてしまえば作業を振れるので最初の解体を手早くやることが重要ですね。
なお、今回のブリは9.2kgでした。内蔵や骨などで2kg以上はあったと思いますが、それでも参加人数で割ると一人あたり350gぐらいという計算に。満腹、満腹。
時間がなくて慌ただしかったのもありますが、欲を言えば刺し身やブリしゃぶなどが一周りして落ち着いたところで焼き物が出てくるぐらいの順序にしたかったですね。やはりブリカマ塩焼きは熱いうちが美味しいので。
寿司を握るのは非常に面倒くさいことを実感しました。あれは専任であたらないと無理ですね。人件費が高くつくのも頷けます。
鰹節を削ったり、イシダイを松皮造りにしたり、ヒラメを5枚卸ししたりなど普段やらないことをやったので楽しかったのではないでしょうか。僕もあの大きさのブリを卸したのは初めてだったので、なかなか楽しかったですね。
地方在住だと東京の友人とホームパーティーをすることはなかなか難しいのですが、今回はなんとか実施できてよかったです。慌ただしかったですが、とても楽しかったです。皆さん、良いお年を!
FutureTaskのコンストラクタと魔法のバインド
java.util.concurrent.FutureTaskのコンストラクタの定義が
public FutureTask(Callable
http://docs.oracle.com/javase/jp/8/docs/api/java/util/concurrent/FutureTask.html#FutureTask-java.util.concurrent.Callable-callable)
とCallable<V>となっていてCallable<? extends V>じゃないのが不便だという話題。
これにより Thread Safe な汎用オブジェクトCache - がくぞーのメモ のキャッシュ機構のコンストラクタをCallable<? extends V>に出来ないのだとがくぞさん(@gakuzzzz)が嘆いていたので、小手先のテクニックで対応したのが以下のコード。
import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.atomic.AtomicReference; public class Cache<T> { private Callable<? extends T> originalFactory; private AtomicReference<Future<? extends T>> cache = new AtomicReference<Future<? extends T>>(); public Cache(Callable<? extends T> originalFactory) { this.originalFactory = originalFactory; } public T get() { try { return getFuture().get(); } catch (ExecutionException e) { // ここの処理は現場のポリシーによって柔軟に throw new RuntimeException(e.getCause()); } catch (InterruptedException e) { throw new CancellationException(e.getMessage()); } } private Future<? extends T> getFuture() { Future<? extends T> result = cache.get(); if (result != null) return result; FutureTask<? extends T> task = getFutureTask(originalFactory); if (cache.compareAndSet(null, task)) { task.run(); } return cache.get(); } private <X> FutureTask<X> getFutureTask(Callable<X> callable) { return new FutureTask<X>(callable); } }
問題の解説
問題の中心になるのは
public FutureTask(Callable
http://docs.oracle.com/javase/jp/8/docs/api/java/util/concurrent/FutureTask.html#FutureTask-java.util.concurrent.Callable-callable)
というコンストラクタに対して、
Callable<HogeEx> call = ()->new HogeEx(); new FutureTask<Hoge>(call);
という呼び出しが出来ないという点。
Callable<HogeEx>をCallable<Hoge>にキャストすることは出来ない。
このキャストについてはJavaジェネリクス再入門 - プログラマーの脳みそあたりを参考にされたし。ジェネリクスの代入互換性は通常の型の代入互換性とは異なる。
そして
Callable<? extends Hoge> call = ()->new Hoge(); new FutureTask<? extends Hoge>(call);
というようにコンストラクタで? extends Hogeをバインドすることもできない。
もし、Javaの標準APIのFutureTaskのコンストラクタが
public FutureTask(Callable<? extends V> callable)
という形状であれば
Callable<HogeEx> call = ()->new HogeEx(); new FutureTask<Hoge>(call);
とすることが可能になる。が、Javaの標準APIのシグネチャ(ようするにメソッドの型)なんて互換性の関連もあってそうそう変わるものでもない。
対応策
new FutureTask<? extends Hoge>を実現できれば良いのに、ということで? extends Hogeをロンダリングしてメソッドスコープの型変数にすることで回避。
private <X> FutureTask<X> getFutureTask(Callable<X> callable) { return new FutureTask<X>(callable); }
このgetFutureTaskを呼び出す側だが、メソッドスコープの型変数へのバインドは型が推論できるので
FutureTask<? extends T> task = getFutureTask(originalFactory);
と書くことで呼び出すことが出来る。
逆に推論をさせずに明示的にcaptuer ? extends Tをバインドしようとするとコンパイルエラーになる。
FutureTask<? extends T> task = this.<? extends T>getFutureTask(originalFactory); // NG
ここまで書いてもうひとつ型推論が行われるダイヤモンド演算子を使えばいいことに気づく。
FutureTask<? extends T> task = new FutureTask<>(originalFactory); // OK
Java8流インスタンスを生成するメソッドの書き方
メソッド内部で任意の型のインスタンスを生成してオブジェクトを返す場合
public static <T> T hoge(Class<T> clazz) { try { return clazz.newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } }
といったようにjava.lang.Classを引数にとり、リフレクションでインスタンス生成するというのが常道だった。
対象となるClassにはデフォルトコンストラクタ(引数なしのコンストラクタ)が存在することが前提となる。
呼び出し側は以下のように 型名.class を渡す。
String string = hoge(String.class);
これがstataicメソッドではなく、thisオブジェクトが継承による型変数のバインドをしているような特殊なケースでは引数にClassを渡さずともインスタンス生成することも出来たが、限定的なケースである。(参考:new T()したいケースへの対処法 - プログラマーの脳みそ)
裏技的な手法としては
public static <T> T hoge(T ... dummy) { try { Class<?> clazz = dummy.getClass(); Class<?> componentType = clazz.getComponentType(); return (T) componentType.newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } }
といったように可変長配列を用い、呼び出し時には
String string = hoge();
といったように引数を渡さないことで空の配列を送り、そこからインスタンスを作成するという手法もあった。
しかし、この手法を用いる場合、空のダミー配列は静的に型が解決されておらねばならず、型変数などで間接的に扱うことはできない。
また、IDEによっては引数が補完されてしまって逆に使いにくいということもマイナス要因だった。
ラムダ or メソッド参照
Java8ではラムダ式やメソッド参照が使えるようになったので、インスタンス生成をこれらで外に引っ張り出すことができる。
public static <T> T hoge(Supplier<T> supplier) { return supplier.get(); }
ここでは標準APIのjava.util.function.Supplierを用いた。
この関数型インタフェースはT get()というメソッドが用意されており、引数なしで値を返す実装を汎用的に表すことができる。
これに対して呼び出し側は
String str1 = hoge(String::new); String str2 = hoge(()->"test");
といった記述になる。
これにより、java.lang.Classを渡してリフレクションでnewInstance()するよりも柔軟なインスタンス生成ができるようになる。デフォルトコンストラクタがある前提を敷く必要もない。
そしてメソッドの実装側もリフレクションに伴う例外処理をする必要がなくなる。
ぜひ、導入を検討してもらいたい。