Hatena::ブログ(Diary)

プログラマーの脳みそ このページをアンテナに追加 RSSフィード

2017-05-12

Java本格入門のジェネリクスの用語訂正案

| 19:54 |  Java本格入門のジェネリクスの用語訂正案を含むブックマーク

聞くところJava本格入門(通称アクロ本)の評判がすこぶる良いようだ。1995年にJavaが発表されてから随分と経つ。Javaのメジャーバージョンも8を数え、9がリリースされようとしている。1990年代にはこの新しい言語についてたくさんの技術書が発行されたが、これほどバージョンに差異があると現代では役に立たない。現代のバージョンに即した質の良い入門書が発行されることは喜ばしいことだと思う。

しかしながら、このJava本格入門も3-4-2 ジェネリクス(総称型)にていささか不適切な用語の使用方法がされているようだ。本項では改善提案を行い、また関連する用語の解説を行いたい。

発端

これに端を発し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側
parameterargument
formal parameteractual parameter
formal argumentactual 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>();
	}

というコードがあったあったとき、

となる。詳細はやや古いが拙稿の Javaジェネリクス再入門 - プログラマーの脳みそあたりも参考にして欲しい。

Hoge<T>では型変数Tが宣言される。メソッド引数のときの例で言えばfoo(int value)のvalueに相当するのがTと言えよう。これに対し、new Hoge<String>();ではこのTにString型をあてますよ、という意味合いになる。foo(123);の123に相当するものがStringというわけだ。TにStringをあてる部分を私は「バインディング」と表現している(情報工学用語では「束縛」英語では"Binding")。こうした、型変数宣言する/バインディングするという文脈では「型パラメータ」「型アーギュメント」を厳密に使い分ける必要が生じる。しかし、そうした文脈でなければ型変数は単に「型変数」と呼べば良い。

ここで、Java言語仕様の英文での表現をみると

となっている。また単に「型変数」という場合は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>と解説した。型変数を加えることを「パラメータ化」という言い方をする用例もあるのだが、「パラメータ化された型」という専門用語が存在してしまっている以上、紛らわしい表現は避けるべきだろう。

トラックバック - http://d.hatena.ne.jp/Nagise/20170512

2016-05-20

贖罪のイレイジャ

| 20:48 | 贖罪のイレイジャを含むブックマーク

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<T1, …,Tn>のイレイジャは|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:

[TypeParameters] SuperclassSignature {SuperinterfaceSignature}

TypeParameters:

< TypeParameter {TypeParameter} >

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.9.1

なぜ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はやれているのだから。

実行時の型変数

実行時に動的に型変数の値を得られるようにするにはいくつかの方法論が考えられる。ジェネリクスの実装方式も含めていくつか挙げると

これらはどれが一概に優れているとは言えない。いずれの方式でも相応の実用性を得ることができるだろうし、それぞれデメリットもある。

Javaジェネリクスを導入するにあたって互換性を考慮してイレイジャ方式を採用したため、実行時に動的に型変数の値を得られるようにするには、必然的にScalaのような方式を取らざるをえないのであるが。

ただ、そもそも、ジェネリクスを静的な型チェックに重きをおくならば、実行時に動的に得る機能性というコードを書くことは下策なのである。だが、時にこの抜け道が役に立つ時がある。しかしこれはあくまでも抜け道であって、抜け道があることをジェネリクスの本質なのだ、などという主張は説得力に欠ける。

イレイジャ方式のメリット

イレイジャ方式のメリットとして挙げられるものは

  • ジェネリクス対応前のコードとの互換性
  • VM上でより拡張された型システムの実装を設計する際の容易さ

があるだろう。

既存の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のような機能性が求められることだろう。

トラックバック - http://d.hatena.ne.jp/Nagise/20160520

2016-04-25

入門書が教えてくれないJava スコープ編

| 22:08 |  入門書が教えてくれない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の場合、スコープについて考える場合、影響する要素は以下の様なものがある。

今回、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種+α - プログラマーの脳みそ参照)に至っては宣言されたメソッド内だけが有効なスコープとなる。

いずれも、基本的には「関係ないものは見えなくする、関係あるものだけが見える」という状態を保つことで、プログラムを整理する技なのである。大量のハズレの選択肢から当たりを探すような作業プログラミングの妨げでしかない。

ごく小規模なプログラムならいざしらず、ちょっと大きくなってくると途端に整理整頓が必要になってくる。変数のスコープは小さくという先人の知恵をぜひ活かして欲しい。納得が行かない人は心ゆくまで試行錯誤してみるのも良いかもしれない :-)

トラックバック - http://d.hatena.ne.jp/Nagise/20160425

2016-04-22

入門書が教えてくれないJava 変数編

| 23:33 |  入門書が教えてくれない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フィールドをハイディングしてもアクセスしたいときは明示的に親クラス名.フィールド名を書けば済む。

そのオブジェクトでのインスタンスフィールドをアクセスしているのだ、ということを明示的に表現したい場合はthis.フィールド名といったようにthisキーワードを用いて明示することができる。

親クラスのインスタンスフィールドと現在のクラスのインスタンスフィールドが同名となった場合、super.フィールド名で親クラスのフィールドを参照することができる。現クラスのほうはthis.フィールド名で明示するとよいだろう。superによるアクセスは当クラスからのみで、外部からオブジェクトの親のフィールドをsuperで参照するようなことはできない。

また、3段以上に継承している場合、superが参照するのはひとつ上の階層となる。多重ハイディングしている場合に階層ごとに呼び分けるようなことはできない。

内部クラスから外部クラスの同名フィールドを参照したい場合、外部クラス名.this.フィールド名とすることで呼び出すことができる。まれにこの記法を知らないがために迂回路となるアクセサ・メソッドを用意したりしているソースコードを見ることがある。ダサいといえばダサいが、そんなに実害のあるものでもない。

  • staticフィールドはクラス名.フィールド名でアクセス
  • インスタンスフィールドはsuper.フィールド名で親のフィールドを参照することができる
  • 外部クラスのインスタンスフィールドは外部クラス名.this.フィールド名

おわりに

入門書ではスルーされがちな部分をいくつかピックアップしてみたが、楽しんでいただけたであろうか。訂正はもちろん、リクエストなどもあればコメントいただきたい。

*1:このあたりクラスロードとClassLoaderについてはざっくり割愛しているので上級者の皆様におかれましてはblogエントリなどでの解説を試みていただきたく

kmaebashikmaebashi 2016/04/24 16:28 「C言語の影響で変数名の後にを記述して」のように、一部[]が消えているところがあります。
(はてな記法の影響?)

NagiseNagise 2016/04/25 10:59 ありがとうございます。はてな記法で化けていたようです。エスケープしておきました。

2015-12-25

セミコロンレスJavaコンパイラの設計

00:01 |  セミコロンレス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のクラスそのものをクラスの機能を保ったままセミコロンレス化することが困難であることは容易に想像が付くことでしょう。

というのはよく知られたところであります。

上記クラスをセミコロンレス化すると以下の様な形になります。以下は読みやすいように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オブジェクトをセミコロンレス化させる技法について検討しました。それにより

ということを示しました。

こうした一般化規則によってセミコロンレス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コンパイラ自身のソースコードコンパイルすることが出来るようになるでしょう。

匿名匿名 2016/01/10 22:14 Minecraftもコレで動いてるんですね

トラックバック - http://d.hatena.ne.jp/Nagise/20151225