
JavaのOptional|Optionalはnullかもしれないと伝えるもの
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チェックをしないと痛い目を見る例
以下のプログラムがあったとします。getSampleでOptionalSampleを取得して、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が発生するかどうかは五分五分!! } }
メソッドgetSampleでOptionalSampleのインスタンスが戻ってくるかはランダムです。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のインスタンスを作る
次に、メソッドからOptionalをreturnするために、Optionalのインスタンスの作り方を知りましょう。
Optionalのインスタンスを作るには、Optional.ofかOptinal.ofNullableを、包み込みたい値を引数に使います。ofはnullでない値専用で、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を戻すようにした }
これでメソッドがOptionalをreturnするようになりました。次は、メソッドを呼び出した側の処理を変更します。
2-2-3.Optionalから値を取り出す、値があるかチェックする
Optionalから値を取り出すには、Optional.getをします。getするとOptionalが持っている値を取り出せるのですが、値がないならNoSuchElementExceptionがthrowされます。
なお、Optionalで気を付けたいのは、Optionalをnullで作った場合でも、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.値がない時にデフォルト値を指定できるorElseとorElseGet
値がない時のデフォルト値を指定したいなら、Optional.orElseを使いましょう。orElseはif~elseのelse部分を補ってくれます。
デフォルト値を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());
なお、このサンプルでは自分で作ったクラスを使っていますが、StringやIntegerでも同じことができますよ。
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を使って何かをしたいなら、前述したifPresentやorElseを合わせて使いましょう。メソッドチェインを使うと、プログラムがよりスッキリします。こうすると、やりたいことが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です。mapでOptionalに変換すると、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のメソッド呼び出しで書けるようになります。プログラムの行数は減りますし、なによりやろうとしていることが明確に表現できるのです。
コメント