Javaについて徹底解説!

【Optional入門】Javaでnullを扱うベストプラクティスのご紹介

大石 英人

大石 英人

開発エンジニア/Java20年/Java GOLD/リーダー/ボールド歴2年

Javaのjava.util.Optionalは「nullかもしれない値」を上手に取り扱うためのクラスです。Java 8で追加された、まだ比較的新しいクラスです。

「え、このメソッドって戻り値がnullになることがあるの?」ということは、Javaプログラマなら一度は経験があると思います。そういう時に限ってnullチェックを忘れてしまって、バグを作り込んで怒られてしまうのです

Optionalを使うと、そういうミスを事前に防ぐことができます。値がnullかもしれないと意識できますし、nullだった場合にありがちな処理も、大変簡単に書けるのです。

この記事では「Optionalってなにそれ? おいしいの?」な人向けに、Optionalの使い方についてお伝えします。

※この記事のサンプルは、Java 10の環境で動作確認しています。


1.Optionalはnullかもしれないと伝えるもの

Optionalは何かの値を包み込むクラスであり、同時に「値はnullかもしれないよ、気を付けて!」と伝えるためのクラスです。

Optionalはメソッドの戻り値で使われるのが普通ですが、nullかもしれない値に何か処理をしたい時にも便利に使えます。


2.Optionalの使い方

2-1.nullチェックをしないと痛い目を見る例

以下のプログラムがあったとします。getSampleOptionalSampleを取得して、helloメソッドを呼ぶものです。

class OptionalSample {
	static OptionalSample getSample() {
		OptionalSample sample = null;

		if (new java.util.Random().nextBoolean()) {
			sample = new OptionalSample();
		}

		return sample;
	}

	String hello() {
		return "Hello, World.";
	}

	public static void main(String[] args) {
		OptionalSample sample = getSample();
		System.out.println(sample.hello()); // → NullPointerExceptionが発生するかどうかは五分五分!!
	}
}

メソッドgetSampleOptionalSampleのインスタンスが戻ってくるかはランダムです。helloできるかは、まさに運次第。気を付けないとNullPointerExceptionが簡単に発生してしまう、とても危険なプログラムです。

これをどうにかしようとすれば、普通は以下のようにメソッドの戻り値がnullかどうか、if文でチェックします。

OptionalSample sample = getSample();

if (sample != null) {
	System.out.println(sample.hello());
}

でも、こうして戻り値をnullチェックしてくれるかは、このメソッドを「呼び出す」プログラマ次第です。プログラムを一回動かして、上手く動けばそのまま、という人も多いでしょう。でも、この例のように実はnullが戻ることがあるとすれば…?

「メソッドを作る」プログラマは、どうやれば「メソッドを呼び出す」プログラマに「このメソッドはnullが戻るかもしれない」と気付いてもらえるのでしょうか。設計書やJavadocをきちんと書いても、読まない人は全然読みませんよね。正直言って、私もそうです。

これをOptionalでどうにかしてみましょう。

2-2.Optionalを使ってみる

2-2-1.メソッドの戻り値をOptionalにする

では、さっそくOptionalを使ってみます。

まず最初にやることは、値がないかもしれないメソッドの戻り値の型を、戻り値にしたいクラスそのものではなく、Optionalにすることです。

Optionalの型引数(<>でくくってある部分)は、Optionalで包み込みたいクラスを書きましょう。型引数を使えば、何が本当の値なのか呼び出すプログラマに簡単に伝わります。

// まずはメソッドのシグネチャ部分だけ変えてみる
static Optional<OptionalSample> getSample() {

こうすると、メソッドを呼び出す人は「Optionalが戻るなら、値がないかもしれないんだな」と気付いてくれます。Javaプログラマの間には、Optionalとはそういうものだ、という共通認識があるからです。

2-2-2.Optionalのインスタンスを作る

次に、メソッドからOptionalreturnするために、Optionalのインスタンスの作り方を知りましょう。

Optionalのインスタンスを作るには、Optional.ofOptinal.ofNullableを、包み込みたい値を引数に使います。ofnullでない値専用で、ofNullableは値がnullかもしれない場合に使います。値がnullの場合は、Optional.emptyでもよいですね。

public static <T> Optional<T> of(T value)
型パラメータ:
	T - 値の型
パラメータ:
	value - 説明する値。非nullである必要があります。
戻り値:
	存在する値でのOptional
例外:
	NullPointerException - valueがnullの場合
public static <T> Optional<T> ofNullable(T value)
型パラメータ:
	T - 値の型
パラメータ:
	value - 説明するnullの値
戻り値:
	指定された値が非nullの場合は現在の値を持つOptional、それ以外の場合は空のOptional
public static <T> Optional<T> empty()
型パラメータ:
	T - 存在しない値の型
戻り値:
	空のOptional。

では、サンプルのメソッドを書き換えてみましょう。サンプルでは値は「nullかもしれない」ので、ofNullableを使います。

static Optional<OptionalSample> getSample() {
	OptionalSample sample = null;

	if (new java.util.Random().nextBoolean()) {
		sample = new OptionalSample();
	}

	return Optional.ofNullable(sample); // Optionalを戻すようにした
}

これでメソッドがOptionalreturnするようになりました。次は、メソッドを呼び出した側の処理を変更します。

2-2-3.Optionalから値を取り出す、値があるかチェックする

Optionalから値を取り出すには、Optional.getをします。getするとOptionalが持っている値を取り出せるのですが、値がないならNoSuchElementExceptionthrowされます。

なお、Optionalで気を付けたいのは、Optionalnullで作った場合でも、nullという値を持っているわけではないことです。Optionalは、あくまで値のあり・なしを管理するという考え方なので、nullという値は直接取り出せないのです。

public T get()
戻り値:
	このOptionalによって記述される非null値
例外:
	NoSuchElementException - 値が存在しない場合
Optional<OptionalSample> opt = getSample();
OptionalSample sample = opt.get();
System.out.println(sample.hello());

Optionalからgetした際に例外が発生するのは嫌なので、事前に値があるかをチェックしましょう。値のありなしの確認にはOptional.isPresentを使います。isPresentは、値があるならtrueを戻します。

public boolean isPresent()
戻り値:
	値が存在する場合はtrue、そうでない場合はfalse
Optional<OptionalSample> opt = getSample();

if (opt.isPresent()) {
	OptionalSample sample = opt.get();
	System.out.println(sample.hello());
}

これでOptional化は一応できましたがうーん、これだけだと、正直言ってOptionalにはいいところがあまりなさそうに見えます。

メソッドから値を直接取得できないので、追加の処理は確かに書くようにはなりました。でも、値がないならそのままgetすると例外が発生しますし、結局if文でのチェックがあるのですから、if文でnullかどうかをチェックするのと変わらないですよね?

そう思ってしまうのは、まだここではOptionalの本当の機能を使っていないからです。Optionalの他のメソッドと組み合わせてこそ、Optionalの便利なところが見えてきます。


3.Optionalを上手に使う

Optionalを上手に使うには、Java 8で追加された関数型インターフェイスやラムダ式の知識が少しだけ必要です。でも、怖がる必要はありません!! 簡単なことをするクラスを作るだけです。

Optionalを使うメリットの一つは、nullチェックのif文や続くelseを、プログラマが直接書かないで済むことです。処理の書き忘れを防止する上では、「書かなくてもいい」ということそのものが、大変意味のあることなのです。

3-1.値がある時だけ処理をするifPresent/ifPresentOrElse

値がある時だけ処理をすることは良くあります。普通はif文を使いますが、OptionalにはそのためのメソッドifPresentがあります。

public void ifPresent(Consumer<? super T> action)
パラメータ:
	action - 値が存在する場合、実行されるアクション
Optional<OptionalSample> opt = getSample();
opt.ifPresent(v -> System.out.println(v.hello()));

if文とgetがなくなったので、スッキリしました。値がある場合の処理だけをConsumerで書けばいいのですね。値がない場合は何もされません。

ifPresentOrElseは、少しパワーアップしたifPresentです。値がない場合のelseの処理を、二つ目の引数のRunnableで書けるのです。

public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
パラメータ:
	action - 値が存在する場合、実行されるアクション
	emptyAction - 値が存在しない場合、実行される空のベースのアクション
Optional<OptionalSample> opt = getSample();
opt.ifPresentOrElse(v -> System.out.println(v.hello()), () -> System.out.println("nullでーす"));

3-2.値がない時にデフォルト値を指定できるorElseorElseGet

値がない時のデフォルト値を指定したいなら、Optional.orElseを使いましょう。orElseifelseelse部分を補ってくれます。

デフォルト値をnullにしたいなら、orElseの引数にnullを指定すればOKです。

public T orElse(T other)
パラメータ:
	other - 値が存在しない場合、返される値。 nullも可。
戻り値:
	値(存在する場合)、それ以外の場合はother
Optional<OptionalSample> opt = getSample();
OptionalSample sample = opt.orElse(new OptionalSample()); // nullの場合のデフォルト値を指定して値を取得する
System.out.println(sample.hello());

orElseのいいところは、ここでもif文を書かずに済んでいることです。でも、もっと大事なのはOptionalのメソッド一つを呼ぶだけで、後続の処理はnullかどうかを全く意識しなくて済むことです。

デフォルト値を作るロジックが少々複雑な場合は、Optional.orElseGetを使いましょう。こちらでは複数行からなる処理を、Supplierのラムダ式で渡せます。

public T orElseGet(Supplier<? extends T> supplier)
パラメータ:
	supplier - 戻される値を生成する供給関数
戻り値:
	存在する場合は値、そうでない場合は供給関数によって生成された結果
Optional<OptionalSample> opt = getSample();
// nullの場合のデフォルト値を指定して値を取得する
OptionalSample sample = opt.orElseGet(() -> {
	// 複数行の何かの処理を書ける!!
	return new OptionalSample();
});
System.out.println(sample.hello());

なお、このサンプルでは自分で作ったクラスを使っていますが、StringIntegerでも同じことができますよ。

Optional<String> opt = Optional.empty(); // Stringを持つOptionalを取得、Stringはnullかもしれない
String s = opt.orElse("こんにちは、世界"); // nullの場合のデフォルト値を指定してget、if文は不要
System.out.println(s); // 後続処理ではnullかどうかは意識しなくてもいい

3-3.値がある時だけ値を変換するmap

値がある時だけ、値を何かに変換したいことがあります。その場合はOptional.mapを使います。

public <U> Optional<U> map(Function<? super T,? extends U> mapper)
型パラメータ:
	U - マッピング関数から返される値の型
パラメータ:
	mapper - 存在する場合、値に適用するマッピング関数
戻り値:
	値が存在する場合はマッピング関数をこのOptionalの値に適用した結果を記述するOptional、それ以外の場合は空のOptional
// 値がある時だけ、値のメソッドを実行した結果に文字列を追加する
Optional<OptionalSample> opt = getSample();
Optional<String> opt2 = opt.map(v -> v.hello() + "でーす"); // getの戻り値はOptional<String>になる

mapした結果は、またもやOptionalになります。なぜかと言うと、変換後の値があるかわからないからです。値がないなら、値を持たないOptionalが戻ります。

mapしたOptionalを使って何かをしたいなら、前述したifPresentorElseを合わせて使いましょう。メソッドチェインを使うと、プログラムがよりスッキリします。こうすると、やりたいことがif文を解読するよりも読み取りやすいと思いませんか?

// 値がある時だけ、末尾に文字列を追加してprintする
getSample().map(v -> v.hello() + "でーす").ifPresent(v -> System.out.println(v));
// 値がある時は末尾に文字列を追加、値がないならデフォルト値をprintする
String s = getSample().map(v -> v.hello() + "でーす").orElse("こんにちは、世界");
System.out.println(s);

3-4.条件にマッチした値だけにするfilter

Optionalが値を持っており、また値が特定の条件を満たす時のみ取り出したいなら、Optional.filterを使いましょう。

filterした結果は、mapと同様にOptionalです。filterの条件にマッチするなら値があるOptionalで、マッチしないなら値を持たないOptionalです。

public Optional<T> filter(Predicate<? super T> predicate)
パラメータ:
	predicate - 存在する場合、値に適用する述語
戻り値:
	このOptionalの値を記述するOptional(値が存在し、その値が与えられた述語と一致する場合)、そうでない場合は空のOptional
// StringのOptionalで、値のStringにABCを含む場合だけprintする
Optional<String> opt = Optional.of("ABCDEFG");
opt.filter(v -> v.contains("ABC")).ifPresent(v -> System.out.println(v));

3-5.値変換の結果がOptionalの場合に便利なflatMap

mapでOptionalを戻したい場合に使えるのがOptional.flatMapです。mapOptionalに変換すると、mapの戻り値がOptional<Optional<?>>というようにOptionalの入れ子になってしまい、大変使いづらいです。

flatMapを使うと、Optionalが入れ子にならないようにmapできます。つまり、Optionalの値を取り出して、新しいOptionalを作ってくれるのです。しかも、値の有無はそのままにしてくれます。

public <U> Optional<U> flatMap(Function<? super T,? extends Optional<? extends U>> mapper)
型パラメータ:
	U - マッピング関数によって返されたOptionalの値の型
パラメータ:
	mapper - 存在する場合、値に適用するマッピング関数
戻り値:
	値が存在する場合はOptional生成マッピング関数をこのOptionalの値に適用した結果、それ以外の場合は空のOptional
// StringのOptionalで、値のStringがABCの場合だけ新しいOptionalを作り、そうでなければ空のOptionalにする
Optional<String> opt = Optional.of("ABC");
Optional<String> opt2 = opt.flatMap(v -> "ABC".equals(v) ? Optional.of(v + "CDF") : Optional.empty());

3-6.値でStreamを作るstream

Optionalが持っている値から直接Streamを作れます。Optionalが持っているのがnullの場合は、空のStreamです。

streamは、Optionalの値がStreamを作れるようなクラス(Listなど)だと、Stream.flatMapと組み合わせて便利に使えたりします。

public Stream<T> stream()
戻り値:
	値のStreamです。
Optional<List<String>> opt = Optional.of(Arrays.asList("AA", "BB"));
opt.stream().flatMap(v -> v.stream()).forEach(v -> System.out.println(v)); // "AA"、"BB"が出力される

Optional<List<String>> opt2 = Optional.empty();
opt2.stream().flatMap(v -> v.stream()).forEach(v -> System.out.println(v)); // 何も出力されないし、エラーにもならない

このように、値をOptionalから取り出す処理をいちいち書かなくても、安全に処理できるStreamが作れるのは便利ですね。


4.まとめ

この記事では、Java 8から追加されたOptionalをお伝えしました。Optionalは、nullかもしれないモノを扱うためのクラスで、メソッドの戻り値として使うことが主に想定されています。

メソッドがOptionalを戻すと、メソッドを呼ぶ人が「このメソッドはnullを戻すかもしれないのか」と気付きやすくなります。これにより、予期せぬNullPointerExceptionの発生を少なくできるのです。

そしてOptionalを使えば、普通の処理ではif文を書かなければできない良くある処理を、Optionalのメソッド呼び出しで書けるようになります。プログラムの行数は減りますし、なによりやろうとしていることが明確に表現できるのです。

私たちは、全てのエンジニアに市場価値を高め自身の望む理想のキャリアを歩んでいただきたいと考えています。もし、今あなたが転職を検討しているのであればこちらの記事をご一読ください。理想のキャリアを実現するためのヒントが見つかるはずです。

『技術力』と『人間力』を高め市場価値の高いエンジニアを目指しませんか?

私たちは「技術力」だけでなく「人間力」の向上をもって遙かに高い水準の成果を出し、関わる全ての人々に感動を与え続ける集団でありたいと考えています。

高い水準で仕事を進めていただくためにも、弊社では次のような環境を用意しています。

  • 定年までIT業界で働くためのスキル(技術力、人間力)が身につく支援
  • 「給与が上がらない」を解消する6ヶ月に1度の明確な人事評価制度
  • 平均残業時間17時間!毎週の稼動確認を徹底しているから実現できる働きやすい環境

現在、株式会社ボールドでは「キャリア採用」のエントリーを受付中です。

まずは以下のボタンより弊社の紹介をご覧いただき、あなたの望むキャリアビジョンをエントリーフォームより詳しくお聞かせください。

コメント

公式アカウントLINE限定!ボールドの内定確率が分かる無料診断実施中
公式アカウントLINE限定!
ボールドの内定確率が分かる無料診断実施中