Hatena::ブログ(Diary)

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

2014-08-13

Javaの型システムとコンストラクタ

| 15:00 |  Javaの型システムとコンストラクタを含むブックマーク

今回はJavaの型システムのコンストラクタについて考えてみたい。

Javaの型システム、あるいはJavaオブジェクト指向において、コンストラクタという存在は特殊な存在だ。

コンストラクタ内からはそのクラスのインスタンスフィールドにアクセスできる。これは通常のインスタンスメソッドと同等のスコープであってstaticメソッドのそれとは異なる。しかし、コンストラクタを呼び出すにあたってはインスタンスメソッドという体ではなく、staticメソッドのように(インスタンスではなく)クラスに属するものとして呼び出すことになる。(もっともnewという専用のキーワードを用いるのでそうは見えないかもしれないが)

クラスやinterface、つまりJavaの「型」によるポリモフィズムの世界を考えるとき、コンストラクタはのけものである。継承関係を持つクラスであってもコンストラクタ継承されないし、オーバーライドすることもできない。

コンストラクタのおさらい

コンストラクタは次のように宣言する。隅カッコは省略可能を意味する

【アクセス修飾子】 【型変数宣言】 クラス名(【引数, ...】) {

  【this(【引数, ...】);】

  【super(【引数, ...】);】

}

ここでアクセス修飾子はpublic protected privateを宣言でき、省略するとパッケージプライベートとなる。public修飾子を用いることが多いが、abstractクラスにおいてはprotectedとするし、singletonパターンやstaticメソッドだけからなるユーティリティクラスなどではコンストラクタをprivateにして外部からの想定外のインスタンス化を防ぐことだろう。

また通常のインスタンスメソッドでは使用可能なsynchronized, strictfp, finalといったキーワードを指定することはできない。synchronizedキーワードの指定はsynchronized(this){}とsynchronized句を書くのと同等の簡易記法であって、ロックオブジェクトがthisなのであるからコンストラクタに指定してもナンセンスであることが分かるだろう(詳しく知りたい人は オワコンであるVectorとListの同期の話 - プログラマーの脳みそあたりを参照)。またコンストラクタはオーバーライドできないのであるからfinalキーワードがナンセンスであることも分かるだろう。

コンストラクタ内でだけ厳密浮動小数演算を行いたい場合にstrictfpキーワードを用いれないのは不便な気もするが、コンストラクタなのだから演算結果をフィールドなどに保持することになると想定され、だったら最初からコンストラクタ限定ではなくクラス全体をstrictfp指定しておけよということなのではないだろうか。

話は変わって、ジェネリクスの型変数宣言だが、コンストラクタでもメソッドスコープのジェネリクス変数の宣言を行うことが出来る。しかし、未だにコンストラクタメソッドスコープの型変数が役に立つシチュエーションに遭遇したことがない。コンストラクタの型変数宣言については忘れてくれていい。なお、型変数にバインドする場合は次のように記述する。

new <String>Hoge();

繰り返しになるがコンストラクタの型変数については忘れてくれていい

メソッドスコープのジェネリクスとはなんぞという人は拙稿の Javaジェネリクス再入門 - プログラマーの脳みそあたりを参考にしてほしい。

コンストラクタは同クラス内に宣言された他のコンストラクタシグネチャの違うオーバーロード。なおシグネチャとは大雑把に言えば引数の形状のことである)に処理を委譲することができる。この際にはthis(引数);という呼び出し方をする。なお、このthis()はコンストラクタのブロックの先頭に書かなくてはならないという制約がある。

一般に引数の少ないコンストラクタから引数の多いコンストラクタを呼び出す形になる。

public Hoge() {
	this("no name");
}
public Hoge(String name) {
	this(name, -1);
}
public Hoge(String name, int age) {
	this.name = name;
	this.age = age;
}

つまるところ引数デフォルト値を定義するようなものだ。なお、this()の手前には文が書けないのでMapを作って値をput()して…といったものをthisの引数に渡したい場合、適当なstaticメソッドを用意してデータを加工するのがよい。

継承階層とコンストラクタ

同様にスーパークラスコンストラクタに処理を委譲する場合はsuper();を用いるスーパークラスデフォルトコンストラクタ引数のないコンストラクタのことを指す)が存在しない場合、明示的にsuper(引数);として宣言しなくてはならない。

継承関係にあるクラスのコンストラクタが呼ばれた場合、super()の記述があればまず親クラスの該当コンストラクタが処理される。super()の記述がない場合、親クラスのデフォルトコンストラクタが呼び出され処理される。

ここでコンストラクタの動きをおさらいしておこう。Javaのクラスは、コンストラクタが全く宣言されていない場合、暗黙にデフォルトコンストラクタが存在するものとして扱われる

public class Hoge {
	// コンストラクタが宣言されていない
}

コンストラクタは宣言されていないが、もちろんnewを行うことができる。

Hoge hoge = new Hoge();

これは、暗黙に次のようなコンストラクタが存在しているとみなされるためだ。

public class Hoge {
	// 空の実装のpublicコンストラクタ
	public Hoge() {}
}

次に、明示的に引数をもつコンストラクタが宣言された場合を見てみよう。

public class Piyo {
	public Piyo(int i) {}
}

この場合、次のような引数なしのコンストラクタ呼び出しはコンパイルエラーとなる。

Piyo piyo = new Piyo(); // コンパイルエラー!

コンストラクタが宣言されている場合、宣言されたシグネチャコンストラクタのみが存在することとなる。デフォルトコンストラクタはあくまでひとつもコンストラクタが宣言されていない場合のデフォルトだということだ。

次に、継承を見てみよう。親クラスAがあり、子クラスBがある場合

public class A {
	public A() {
		System.out.println("A");
	}
}

public class B extends A {
	public B() {
		System.out.println("B");
	}

	public static void main(String[] args) {
		B b = new B();
	}
}

これを実行するとコンソールには以下のように出力されるだろう

A

B

クラスBのコンストラクタが実行される前に、親であるクラスAのコンストラクタが暗黙に呼び出される。クラスAを継承しているということは、クラスAのコンストラクタを呼び出した上で、拡張的な機能を持つということだ。なので親であるクラスAのコンストラクタを呼びださなくてはならない。明示的に書くと次のようになる。

public class B extends A {
	public B() {
		super(); // 親クラスのコンストラクタ呼び出し
		System.out.println("B");
	}
}

この時、親クラスAに複数のコンストラクタがある場合、そのうちのどれを使うかを選択することになる。

public class A {
	public A(int i) {
		System.out.println("A int "+i);
	}
	public A(String s) {
		System.out.println("A String "+s);
	}
}

public class B extends A {
	public B() {
		super("B");
		System.out.println("B");
	}

	public static void main(String[] args) {
		B b = new B();
	}
}

これを実行すると

A String B

B

と表示されることだろう。

ここで、クラスAが引数をもつコンストラクタを定義しているわけだが、クラスBではおなじシグネチャコンストラクタを定義すること無く、Stringの値を"B"と固定してしまっているわけだ。すると、親クラスで引数がStringのコンストラクタを定義しているにも関わらずクラスBは引数Stringのコンストラクタを持たない。

あるいは次のようにStringの他にパラメータを受け取るようなコンストラクタを定義することもあるだろう


public B(String s, int i) {
	super(s);
	this.i = i;
	System.out.println("B");
}

このように、クラスが継承関係にある場合、親クラスは必ず必要となるパラメータコンストラクタを通じて必ず受け取るということを子クラスに課すことができる。しかし、それは子クラスのコンストラクタシグネチャを固定化するという制約ではない。最低限必要な情報は渡せ、ということだが、別にnewの呼び出し方を決めようというわけではない

あるクラスで、必ずある状態を持っていなければならないのだとしよう。デフォルトコンストラクタインスタンスを生成し、setXXXで状態をセットするということをした場合、オブジェクトが生成されてから正しく使えるようになるまでの間に不完全な状態が存在することになる。コンストラクタ内で処理することでこうした不完全な状態を持つことなくインスタンス生成を保証できるようになる。

継承関係を持った場合でも、前述のsuper()の呼び出しによってコンストラクタ原子性は保たれる。

ポリモフィズムコンストラクタ

あるメソッド引数

public someMethod(List<Hoge> list) {}

といった形でデータが渡されてきた場合、Listの中にはHoge型か、あるいはその継承クラスのオブジェクトが格納されていることになる。もしくはHogeinterfaceである場合、Hogeインターフェイスの実装型が格納されていることになる。

このとき、listからgetしたオブジェクトをinstanceofで何型か調べて条件分岐することも可能ではあるが

そのようなコードが肯定される状況というのは極めて少ない。一般に、Hoge型であると宣言されている以上、実体が何型であるかなぞ気にせず一律にHoge型であろうとして取り扱う。それを可能とするためにHoge型の継承クラスはHoge型で提供されるメソッドをすべて機能する形で提供しなくてはならない。リスコフの置換原則というやつである。

Hoge型を継承したPiyo型があったとして、一旦Hoge型の変数に代入されてしまえば、以後はHoge型に定義された

メソッドしか呼び出してはならなくなる。Piyo型がPiyo型独自の情報を内部に持っていたとしてそれらの設定を行うためにはPiyo型がHoge変数になってしまう前、具象型のPiyoを知っている箇所で行う必要がある。(ダウンキャストは禁じ手だ)

さらに言えば、インスタンスはどこかでnewされなければならず、そのコードは具体的にクラス名を記載した上でnewを行う。その箇所にその具象型の生成に必要な情報を流し込み、その箇所に具象性を閉じ込めてしまうのである。一度生成されてしまえばListにaddしてsomeMethodに渡すことができる。someMethod内ではその型が何かは問われずHoge型として扱われる。ポリモフィズムによって同じ形状のメソッドで同じように扱われるにあたって、コンストラクタはその具象性を隠す場所となる

リフレクション

本来はnewを行う場所というのはその具象型を知った上で書かれたコードのはずだった。ところが、具象型を知らずしてインスタンス生成を行うことが可能なのである。そう、リフレクションによってね。

Class<?> clazz = Class.forName("Hoge");
Object object = clazz.newInstance();

デフォルトコンストラクタのある場合は上記のように簡素にjava.lang.ClassからnewInstance()することでインスタンス生成を行える。引数を伴うコンストラクタを呼び出す場合はjava.lang.reflect.Constructorを取得してnewInstance()する。

Constructor<?> constructor = clazz.getConstructor(String.class);
Object object = constructor.newInstance("hoge");

これにより具象型を知らずともクラス名を文字列で与えればインスタンス生成が可能であることが分かるだろう。このとき、コンストラクタシグネチャがどのようなものであるか、Javaの型システムは何らの情報を与えてくれない。

リフレクションを多用したプログラムを書いているとコンストラクタシグネチャに対して制限がかけたいと考えることもあるだろう。しかし、それはJavaオブジェクト指向、あるいはJavaの型システムの外の話なのである。コンストラクタに具象性を閉じ込めたからこそ僕らはポリモフィズムを扱えるのであり、コンストラクタが具象性を一手に引き受けるからこそシグネチャは定まらないのである。

もしどうしても具象型のインスタンスを生成する機能を型システムの範疇で扱いたければ、いわゆるBuilderクラスを作ることになる。

何を優先するか

しかしながら単にデータを格納しておくだけのクラスに対してまでいちいちBuilderクラスを作るのは馬鹿らしいとかんがえる向きもある。例えばO/Rマッパーであるとか、DIコンテナであるとか。とかくフレームワークと呼ばれるようなプログラムではユーザが定義したフレームワークが知らないクラスをフレームワーク内部でインスタンス生成したくなることがある。

このとき、super()の呼び出しによってコンストラクタ原子性が〜という議論は多くの場合当てはまらない。Javaの型システムにおけるコンストラクタの機能性を捨ててしまっておよそ問題がないというシチュエーションが多いのである。しかるに、フレームワークインスタンス生成の対象となるクラスにはデフォルトコンストラクタを持つこと」というルールが課せられることになる。もちろん、Javaの型システムはこの制約に対して何らの機能性を提供しない。C#ジェネリックだとnew制約でこうした制約を課すことができるが、無難な妥協点であると言えよう。

Javaの場合はデフォルトコンストラクタの有無をコンパイルタイムに判断してエラーとすることはできないが、

デフォルトコンストラクタによるオブジェクト生成が行いたいのであれば引数java.lang.Classを渡すのが一般的である。java.lang.reflect.Constructorを渡せばよいのではないかと考えるかもしれないが、newInstance()メソッド引数に何を渡すのかという部分で大抵はどうにもならなくなるのでやめておいた方がいい。

より複雑なオブジェクト初期化ルーチンが必要であればBuilderクラスを作って渡すことになるだろう。Java8であれば簡素なBuilderならラムダ式で幾らかは記述の面倒臭さは緩和されるかもしれない(といっても許容できないと考える人も多いだろう)。Builderパターンの場合はつまるところBuilderコンストラクタオブジェクト生成に必要な特殊条件を詰め込むことになる。結局はどこかに具象型の具象性を隠さねばならない。

まとめ

僕達がオブジェクトの生成で取れるスタンスはみっつある。

一般に型システムを考えるとき、コンストラクタは「型」の部分に含まれない。しかしながら、型システムの具象性の始末をつけてくれるのは他ならないコンストラクタだ。型システムとポリモフィズムが光なのだとすれば、コンストラクタは影の部分なのかもしれない。

トラックバック - http://d.hatena.ne.jp/Nagise/20140813/1407909646
リンク元