【Java】コンストラクタで異常を検知した時

はじめに

オブジェクトが生成されるとき、コンストラクタが実行されて、オブジェクトの初期化が行われる。
コンストラクタ実行時に異常が検知されたときにどうすればよいかを備忘録がてらエントリする。
今回の例では、初期化時、特にコンストラクタ実行時の例を挙げる。

サンプルコード

public class Sample {
	String str;

	 public Sample(String str) {
		 if(str!=null) {
			 this.str = str;
		 } else {
			 throw new NullPointerException("str is null.");
		 }
	 }

 	public String getStr() {
		 return str;
	 }

	public static void main(String[] args) {
		 try {
			Sample s1 = new Sample("Hello");
			System.out.println(s1.getStr());

			Sample s2 = new Sample(null);
			System.out.println(s2.getStr());
		} catch (NullPointerException e) {
			e.printStackTrace();
		}
	}
}

このコードのコンストラクタでは、String型の引数を受け取り、その値をフィールドにセットする。
String型の引数が得られることを想定している(String型を受け取る、フィールドにnullが設定されたオブジェクトを生成しない、というのがクラス設計の契約になっている)が、例えば、nullが渡ってきたとき、想定外の事象にも対応できるようにしておかないといけない。
今回は、想定外の例としてnullが来てしまったときのソースコードを示している。引数がnullだったときは、NullPointerExceptionを呼び出し元にthrowしている。
また、確認用で、そのオブジェクトのフィールド値を表示するgetStrメソッドを作成した。

実行結果は以下の通り。

Hello
java.lang.NullPointerException: str is null.
	at prog.Sample.<init>(Sample.java:10)
	at prog.Sample.main(Sample.java:23)

Sampleオブジェクトs1: オブジェクト生成に成功し、getStrメソッドで、フィールドにセットした値(Hello)が表示されている。
Sampleオブジェクトs2: コンストラクタ実行時に異常(引数がString型じゃなくてnullであること)を検知し、呼び出し元にNullPointerExceptionをthrowしている。呼び出し元であるmain内で投げられた例外をcatchし、例外の内容をコンソール上に表示している。

このように、想定外のケースを考えておくと、オブジェクトが正しく生成されないことがわかり、バグ発生の芽を摘むことにつながる。
では、想定外のケースが起こった時、「例外を投げる」のではなく「returnで処理を終える」と、どうなるのだろうか。
以下にサンプルプログラムを記載する。

public class Sample {
	String str;

	public Sample(String str) {
		if(str!=null) {
			this.str = str;
		} else {
			return;
		}
	}

	public String getStr() {
		return str;
	}

	public static void main(String[] args) {
		Sample s1 = new Sample("Hello");
		System.out.println(s1.getStr());

		Sample s2 = new Sample(null);
		System.out.println(s2.getStr());
	}
}

実行結果は以下の通り。

Hello
null

本来であれば、「String型の引数を受け取ってフィールドを初期化したい(フィールド値がnullのオブジェクトは作られないようにしたい)」のに、コンストラクタ処理が成功して処理が進んで行ってしまう。これはクラス設計の契約に反することであり、バグのもとにもなりかねない。

まとめ

今回の例では、「フィールド値がnullのオブジェクトは生成しない」という契約にもとづいて、コンストラクタの記述を行った。
この例の場合、コンストラクタは異常を検知した時点で呼び出し元に例外を投げるべきであり、returnでそのまま終わってはいけない。
例外を投げるようにしておくと、契約に反したオブジェクトは生成されない。returnで終わってしまうと、コンストラクタ処理が成功してしまい、契約に反したオブジェクトが生成されてしまう。これは、後々バグの原因にもなりかねない。
(別にオブジェクト生成時に、フィールドにnullが入ってもいいよ、という契約なのであれば、例外を投げる云々はしなくてもよいと思う)

そのクラスのフィールド値はどうあるべきか、どのように振る舞うのかといった取り決め事(契約)に基づいて設計・コーディングしていくことが大切になると思う。
・・・とはいえ、言うのは簡単、実行するのは難しいんだよねぇ。