読者です 読者をやめる 読者になる 読者になる

かとじゅんの技術日誌

技術の話をするところ

Javaでnullを回避するために似非Option型を作ってみる

ScalaにはOption型というプログラムの世界観を変えるような魅力的な型があり、それでnullを回避することができる。*1
詳しくはこちらを参照。
ScalaのOptionステキさについてアツく語ってみる - ゆろよろ日記

Javaでも、ScalaのOption型と似て非なるOption型を作れないかなーと思い、思いつきと勢いでコード書いてみました。まぁ、同じものは作れっこないので、遊びです。本気にしないでくださいw

以下のような感じ、Optionのインターフェイスを作って、実装クラスにSomeとNoneを定義。それぞれにofというファクトリメソッドがあります。

public interface Option<T> {
	// オプションから値を取得する。
	public T get();
	// オプションから値を取得するが、値がない場合はdefaultValueを返す。
	public T getOrElse(T defaultValue);

}

Someの場合は内部でT型のフィールド valueを持っているだけです。不変条件としては値がない場合がないので、getもgetOrElseもvalueを返すだけです。equalsやhashCodeはvalueに依存します。

public final class Some<T> implements Option<T> {

	private final T value;

	@Override
	public int hashCode() {
		return value.hashCode();
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		@SuppressWarnings("unchecked")
		Some<T> other = (Some<T>) obj;
		return value.equals(other.value);
	}

	public static <T> Some<T> of(T value) {
		return new Some<T>(value);
	}

	private Some(T value) {
		Validate.notNull(value);
		this.value = value;
	}

	@Override
	public T get() {
		return value;
	}

	@Override
	public T getOrElse(T defaultValue) {
		return value;
	}

}

Noneは内部にTのクラスクラス clazzを保持します。これも不変条件で値がない場合がありません。valueは持ちませんがclazzを持っているわけです。getとgetOrElseの実装は、ステートパターンのようになっていて、値がないという状態の時の挙動を実装します。
Noneは複数のインスタンスを作る必要がないので、シングルトンパターンでもよいかもしれません。今回は、clazzを取ることで、ファクトリメソッドの型の決定がしやすいようにしたのと、equalsでclazzに依存することでシングルトンパターンを採用するのをやめました。この場合だと、new None(String.class).equals(new None(String.class))は等価と判定されるので実用上問題ないかなと思います。

public final class None<T> implements Option<T> {

	private final Class<T> clazz;

	@Override
	public int hashCode() {
		return clazz.hashCode();
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		@SuppressWarnings("unchecked")
		None<T> other = (None<T>) obj;
		return clazz.equals(other.clazz);
	}

	private None(Class<T> clazz) {
		Validate.notNull(clazz);
		this.clazz = clazz;
	}

	public static <T> None<T> of(Class<T> clazz) {
		return new None<T>(clazz);
	}

	@Override
	public T get() {
		throw new NoSuchElementException();
	}

	@Override
	public T getOrElse(T defaultValue) {
		return defaultValue;
	}

}

実際の使い方は以下。値がある場合はSome.ofメソッド、ない場合はNone.ofメソッドで生成できます。このファクトリメソッドを使うと型指定が楽になって生成しやすくなります。
この例では、Noneを2個目の要素に登録しているので、getOrElseでは引数に与えたUser.of("NULL USER")が、値がない場合に利用されます。以下の例にはないですが、getを使うと”値がない”=Noneの時はNoSuchElementExceptionがスローされます。

List<Option<User>> userOptions = new ArrayList<Option<User>>();

userOptions.add(Some.of(User.of("Junichi Kato"))); // 値がある時はSome
userOptions.add(None.of(User.class)); // 値がない時はNone(nullは使わない)

for (Option<User> option : userOptions) {
	User user = option.getOrElse(User.of("NULL USER"));
	System.out.println(user);
}

出力結果は以下。

test.User@5a9e29fb[name=Junichi Kato]
test.User@7f09fd93[name=NULL USER]

追記:
Google Collectionsを使うとNoneだけを除去したコレクションをさくっと取得できる。それとNoneが除去されているはずなので、option.get()を実行しても例外は発生しません。他のコレクション系のAPIも、equalsとhashCodeをオーバーライドしているので意図通り動作すると思います。Someの場合はequalsもhashCodeも内部の値のものを返すので、透過的にコレクション操作ができるはず。便利ですね。

Collection<Option<User>> withoutNone = Collections2.filter(userOptions,
				new Predicate<Option<User>>() {

					@Override
					public boolean apply(Option<User> input) {
						return input.equals(None.of(User.class)) == false;
					}
				});
		for (Option<User> option : withoutNone) {
			User user = option.get();
			System.out.println(user);
		}

Scalaだとコレクションに対してflatten, flatMapを使うと、Noneを除去して、Optionの値だけを簡単に取得できる。

scala> List(None, Some(5), None, if (true) Some(10) else None)
res0: List[Option[Int]] = List(None, Some(5), None, Some(10))

scala> res0.flatten
res1: List[Int] = List(5, 10)

scala> res0.flatMap(x => x)
res2: List[Int] = List(5, 10)

あと、パターンマッチはよく使うと思います。getとか、getOrElse使っていないのが肝です。

scala> case class User(name:String)
defined class User

scala>  def view(userOption:Option[User]) {
     | userOption match {
     | case Some(user) => println(user) // 抽出子を使ってSomeが持っている値を取得して、画面に表示。
     | case None => // 値がない状態を表すNoneでは、何もしない。
     | }
     | }
view: (userOption: Option[User])Unit

scala> view(Some(User("Kato")))
User(Kato)

scala> view(None) // User型を意識しないで使える。
// 何も表示されない

今回つくったのは全くの似非だし、Javaだといろいろと涙ぐましい努力が必要です。本当のOption型を知りたければ、Scalaを触ってみるしかない。ということで、ScalaのOptionは一度使ってみて欲しい。そして、Javaでnullを使っている人は、そのコードでnullが本当に必要か考えよう。
nullという値は本当に必要か考えよう - じゅんいち☆かとうの技術日誌

*1:Null Objectの一種といえると思います。