NullPointerException
嫌いですよね!そんなときに頼りになるのが Java 8 から導入された Optional
です。
でも、 null
の代わりに Optional
を積極的に使うとコードが Optional
だらけになり、特に次のような場合に面倒くさいです。
-
Optional<Integer> a
があるとき、a
の値を二乗したい。ただし、a
がempty
の場合はempty
を得たい。 -
Optional<Double> a
があるとき、a
の平方根を計算したい。ただし、a
がempty
または負の数の場合はempty
を得たい。なお、Math.sqrt
を安全にした(負の数を渡すとempty
を返す)関数Optional<Double> safeSqrt(double)
があるものとして考えて良い。 -
Optional<Integer> a
とOptional<Integer> b
があるとき、a + b
を計算したい。ただし、a
かb
がempty
の場合にはempty
を得たい。
本投稿では、このような場合の簡潔な書き方について説明します(本投稿は "SwiftのOptional型を極める" と "モナドについてSwiftで説明してみた" を Java 向けにアレンジしたものです)。
map
1.
Optional<Integer> a
があるとき、a
の値を二乗したい。ただし、a
がempty
の場合はempty
を得たい。
これを単純にやろうとすると次のようなコードになります。
// ダメな例
Optional<Integer> a = Optional.of(3);
Optional<Integer> result;
if (a.isPresent()) { // empty でなければ
Integer x = a.get(); // 中の値を取り出し
result = Optional.of(x * x); // 二乗してから Optional で包み直す
} else {
result = Optional.empty();
}
たかが整数を二乗するためだけに面倒すぎます。
Optional
の map
メソッドを使えば次のように書けます。
a.map(x -> x * x)
この式は次のように働きます。
- もし
a
がempty
でなければOptional
に包まれた中の値を取り出す。- 取り出された値を引数として
x
に渡す。 - 計算された
x * x
をOptional
で包み直して返す。
- 取り出された値を引数として
- もし
a
がempty
ならempty
を返す。
このように、 map
メソッドを使えばわざわざ Optional
に包まれた値を取り出してから計算して包みなおさなくても、「二乗する」という操作を直接記述するだけで事足ります。
実際に a
が値を持つ場合と empty
の場合に実行すると次のようになります。
Optional<Integer> a = Optional.of(3);
Optional<Integer> result = a.map(x -> x * x); // Optional[9]
Optional<Integer> a = Optional.empty();
Optional<Integer> result = a.map(x -> x * x); // Optional.empty
map
という名前が Stream
の map
と同じなので気になる人もいるかもしれませんが、 Stream
の map
との関連については後ほど述べます。
flatMap
2.
Optional<Double> a
があるとき、a
の平方根を計算したい。ただし、a
がempty
または負の数の場合はempty
を得たい。なお、Math.sqrt
を安全にした(負の数を渡すとempty
を返す)関数Optional<Double> safeSqrt(double)
があるものとして考えて良い。
この safeSqrt
は次のように実装できます1。
static Optional<Double> safeSqrt(double x) {
if (x < 0.0) {
return Optional.empty();
}
return Optional.of(Math.sqrt(x));
}
見ての通り、 safeSqrt
は引数 x
が負のときに empty
を返す他は、ただ Math.sqrt
の結果を Optional
に包んで返しているだけです。
Optionalとエラー
このように、 Optional
はエラーを表すためにも使われます。これまで、 null
を return
することでエラーを表すメソッドがあったように、 empty
を return
することでエラーを表すわけです。 null
でエラーを表すとチェックを忘れて NullPointerException
を引き起こしがちですが、 Optional
であれば empty
である可能性が型で表されており、値を利用するためには明示的に取り出さないといけないのでより安全です。
例えば、 Stream<T>
の最初の要素を返す findFirst
メソッド2の戻り値の型は T
ではなく Optional<T>
です。 Stream
に要素が存在すれば最初の要素を( Optional
に包んで)返しますが、一つも要素がなければ empty
を返します。これは、 Java 8 以前から存在する Deque<E>
( LinkedList
などのスーパータイプ)の peekFirst
メソッド3の戻り値が E
なのとは対照的です。 peekFirst
メソッドは要素がなければ null
を返すので忘れずにチェックしなければなりません。
検査例外でも安全にエラー処理ができますが、 try
, catch
は書くのが面倒なのと、式ではなく文になってしまうため計算の途中に組み込むことができません。また、 Optional
であればエラーという状態を値のまま扱うことができるため、それをフィールドに格納しておいて後から取り出すなど、 エラーを値として取り回して任意のタイミングで(しかし忘れずに)エラー処理を行うことができます4。
フラットにする
さて、本題に戻りましょう。
一見、この場合も map
を使えば書けそうに思えます。
a.map(x -> safeSqrt(x))
しかし、これではダメです。上記の式の結果の型は何になるでしょうか?
答えは、 Optional<Double>
ではなく Optional<Optional<Double>>
です。なぜなら、 map
は safeSqrt(x)
の戻り値をさらに Optional
で包んで返してしまうので、二重に Optional
に包まれてしまうわけです。
この場合、 a
が null
のときには外側の Optional
が empty
に、 a
が負の数の場合には内側の Optional
が empty
になります。今やりたいことは、それらをフラットに潰して Optional<Double>
にし、外と内のどちらか一方でも empty
であれば empty
にしてしまうことです。
これをやってくれるのが flatMap
です。 flatMap
は map
とフラットに潰す処理をまとめてやってくれるメソッドです。 Optional
に包まれた値に適用したい処理がエラーになる可能性があるなど( x * x
は失敗しないが safeSqrt(x)
は失敗し得る)、 Optional
に渡す処理の戻り値も Optional
になってしまう場合には flatMap
が使えます。
a.flatMap(x -> safeSqrt(x))
概念的には、これは map
をしてから二重になった Optional
を平らにする( flatten
)という処理です。
// ※ このコードは概念を表したものであり、正しい Java のコードではありません。
a.map(x -> safeSqrt(x)).flatten() // map すると二重になったのでフラットにする
なお、 Java には flatten
はないですが(というか Optional
のインスタンスメソッドとしては Java の構文の限界で適切に宣言できない)、 flatten
相当の処理は次のようにして実現できます。
Optional<Optional<Integer>> nested = Optional.of(Optional.of(42));
Optional<Integer> flattened = nested.flatMap(x -> x); // Optional[42]
a
が正の場合、負の場合、 empty
の場合に a.flatMap(x -> safeSqrt(x))
を実行すると、次のように望んだ結果が得られます。
Optional<Double> a = Optional.of(2.0);
Optional<Double> result = a.flatMap(x -> safeSqrt(x)); // Optional[1.4142135623730951]
Optional<Double> a = Optional.of(-2.0);
Optional<Double> result = a.flatMap(x -> safeSqrt(x)); // Optional.empty
Optional<Double> a = Optional.empty();
Optional<Double> result = a.flatMap(x -> safeSqrt(x)); // Optional.empty
複数のOptional
3.
Optional<Integer> a
とOptional<Integer> b
があるとき、a + b
を計算したい。ただし、a
かb
がempty
の場合にはempty
を得たい。
これを愚直にやると次のようになります。
Optional<Integer> a = Optional.of(2);
Optional<Integer> b = Optional.of(3);
Optional<Integer> result;
if (a.isPresent()) {
if (b.isPresent()) {
result = Optional.of(a.get() + b.get());
} else {
result = Optional.empty();
}
} else {
result = Optional.empty();
}
足し算をするためだけにこれは耐えられません。
このようなケースにも、 flatMap
と map
を組み合わせて使うことで対応できます。
a.flatMap(x -> b.map(y -> x + y))
イメージとしては、 flatMap
と map
をネストすることで Optional
に包まれていない世界をつくり( Optional<Integer> a
と Optional<Integer> b
を Integer x
と Integer y
として扱えるようにし)、 x + y
を計算するという感じです。計算結果は Optional
に包まれますが、外側が flatMap
なので二重に包まれてしまうことはありません。
実行結果は次の通りです。 a
と b
の片方でも empty
なら結果は empty
となります。
Optional<Integer> a = Optional.of(2);
Optional<Integer> b = Optional.of(3);
Optional<Integer> result = a.flatMap(x -> b.map(y -> x + y)); // Optional[5]
Optional<Integer> a = Optional.empty();
Optional<Integer> b = Optional.of(3);
Optional<Integer> result = a.flatMap(x -> b.map(y -> x + y)); // Optional.empty
処理が失敗しうる場合
x + y
という失敗しない処理なので、上記のコードでは内側が map
になっていますが、失敗しうる処理であれば flatMap
を入れ子にする必要があります。
例えば、 safeSqrt
と同じように安全な Optional<Double> safePow(double x, double y)
を考えます5。その場合、 Optional<Double> a
と Optional<Double> b
に対して safePow
を計算するには次のようにします。
a.flatMap(x -> b.flatMap(y -> safePow(x, y)))
三つ以上のOptional
Optional
の値が三つ以上になってもその分ネストするだけで対処できます。
例えば、 Optional<Integer> a
, Optional<Integer> b
, Optional<Integer> c
の和を求める計算は次のように書けます。
a.flatMap(x -> b.flatMap(y -> c.map(z -> x + y + z)))
もちろん、処理の結果が Optional
になる場合には一番内側の map
を flatMap
にしないといけません。
コレクションとしてのOptional
Optional
は要素が 0 個か 1 個の場合しかないコレクションと考えることもできます。
このことは次の二つのコードを見比べればよくわかります。前者は Optional
で書いたもの、後者は同じことを Stream
で書いたものです。
Optional<Integer> a = Optional.of(3);
Optional<Integer> b = a.map(x -> x * x); // Optional[9]
Optional<Integer> c = Optional.empty();
Optional<Integer> d = c.map(x -> x * x); // Optional.empty
Optional<Integer> e = Optional.of(2);
Optional<Integer> f = Optional.of(3);
Optional<Integer> sum = e.flatMap(x -> f.map(y -> x + y)); // Optional[5]
Stream<Integer> a = Stream.of(3);
Stream<Integer> b = a.map(x -> x * x); // [9]
Stream<Integer> c = Stream.of();
Stream<Integer> d = c.map(x -> x * x); // []
Stream<Integer> e = Stream.of(2);
Stream<Integer> f = Stream.of(3);
Stream<Integer> sum = e.flatMap(x -> f.map(y -> x + y)); // [5]
そっくりですね!
厳密には Stream
はストリームとして一度だけ値を取り出すことしかできずコレクションではないですが、イメージとしては↑で伝わると思います。
filter
Stream
には filter
というメソッドがあります。 filter
を使えば、条件にマッチする要素だけを取り出すことが簡単にできます。
例えば、 Stream<Integer>
から偶数の要素だけを取り出すには、 filter
に x -> x % 2 == 0
( 2 で割った余りが 0 )を渡します。
Stream<Integer> a = Arrays.asList(1, 2, 3, 4, 5).stream(); // [1, 2, 3, 4, 5]
Stream<Integer> result = a.filter(x -> x % 2 == 0); // [2, 4]
Optional
も filter
メソッドを持っています。これは、 Optional
を要素が 1 個 か 0 個のコレクションと考えて自然な挙動をします。
例えば、 [2]
(一つの要素 2
だけを持つコレクションを意図)に対して先程の filter
を実行すると 2
は偶数なので [2]
が返されますが、 [3]
だと []
(空のコレクション)になってしまいます。また、 []
に対して偶数かどうかで filter
しても、最初から要素がないので当然結果は []
です。
Optional
の filter
は、このように考えたときと同じように動きます。
Optional<Integer> a = Optional.of(2); // [2] に相当
Optional<Integer> result = a.filter(x -> x % 2 == 0); // Optional[2]
Optional<Integer> a = Optional.of(3); // [3] に相当
Optional<Integer> result = a.filter(x -> x % 2 == 0); // Optional.empty
Optional<Integer> a = Optional.empty(); // [] に相当
Optional<Integer> result = a.filter(x -> x % 2 == 0); // Optional.empty
ループ
コレクションと言えばループです。 Java 8 からは 拡張 for 文 だけでなく forEach
メソッドが使えます。
例えば、次のようにして List
の要素を表示することができます。
List<Integer> a = Arrays.asList(2, 3, 5, 7, 11);
a.forEach(x -> System.out.println(x));
実行結果は次の通りです。
2
3
5
7
11
なお、 println
するだけであれば次のように forEach
メソッドに直接 println
を渡すこともできます。
a.forEach(System.out::println);
Optional
に forEach
メソッドはないですが、同じことをしてくれるのが ifPresent
メソッドです。
ifPresent
メソッドは、 Optional
が値を持っている場合のみ、引数に渡した処理を実行するメソッドですが、 Optional
をコレクションだと考えるとこれは forEach
に当たります。
次のコードを実行すると 2
が表示されます。
Optional<Integer> a = Optional.of(2); // [2] に相当
a.ifPresent(System.out::println);
しかし、次のコードでは要素がないコレクションの forEach
と等価なため、何も表示されません。
Optional<Integer> a = Optional.empty(); // [] に相当
a.ifPresent(System.out::println);
残念ながら Optional
は Iterable
を実装していないので、 拡張 for 文 で使うことはできません。
ifPresentとisPresent+get
ifPresent
を使うのと、 isPresent
で分岐してから get
で値を取り出すのでは似たようなことができます。
しかし、 isPresent
+ get
は、本当に isPresent
でチェックした上で get
を呼んでいるかコンパイル時にチェックすることができません。もし empty
に対して get
を呼び出すと実行時エラー( NoSuchElementException
)になってしまいます。 ifPresent
であればそのような心配は必要ないので、 isPresent
+ get
よりも ifPresent
を使うことをオススメします。
モナドとしてのOptional
本投稿のタイトルは "JavaのOptionalのモナド的な使い方" ですが、 モナド とは何でしょうか?
実は map
と flatMap
を使うというのが「モナド的な使い方」の意味するところです。前節で Optional
と Stream
の類似性を見てもらいました。 Optional
と Stream
の map
と flatMap
が同じ意味を持つことがわかったと思います。その共通性がモナドの持つ性質です。 モナドとは、 map
や flatMap
を持つ存在を抽象化したものです。
しかし、 map
や flatMap
を持っていたら何でもモナドと呼べるわけではありません。いきなり return null
するとかいう実装だと、とても「共通」の性質を持つわけがないですよね。モナドであるためには、 モナド則 と呼ばれるルールを満たさなければなりません。
Optional
を使う上では知らなくても特に問題はないですが、せっかくなので モナドとは何か についてももう少し正確な説明をします。モナドは関数型言語でよく用いられる概念です。モナドについて調べると大抵 モナド則 が出てきますが、肝心の記述が関数型言語で書かれていてわからん!となることが多いんじゃないかと思います。本節では、モナド則についても Java の構文で記述します。ただし、ややこしいだけなので ? extends
のようなワイルドカードは省略します。
モナドとは
Foo<T>
が次の条件を満たすとき、 Foo<T>
はモナドです。
-
Optional
のof
のように、単一のT
型の値を包んでFoo<T>
を返す静的メソッドまたはコンストラクタを持つ。 -
<U> Foo<U> flatMap(Function<T, Foo<U>>)
というメソッドを持つ。 -
flatMap
の実装が モナド則(後述)を満たす。 -
Foo<T>
が ファンクター(後述)である。
ざっくりと言えば、適切に実装された(モナド則を満たした) flatMap
を持った型はモナドと言えるでしょう(モナドはファンクターでもあるので map
も持っている必要がありますが)。
モナド則
-
Foo.of(x).flatMap(x -> f(x)) == f(x)
が成り立つ。 -
foo.flatMap(x -> Foo.of(x)) == foo
が成り立つ。 -
foo.flatMap(x -> f(x).flatMap(y -> g(y))) == foo.flatMap(x -> f(x)).flatMap(y -> g(y))
が成り立つ。
モナド則は、中身をじっくり見てみればわかりますが、ごく当たり前に満たすべき性質を言ってるだけです。言ってみれば、積の結合法則 (a * b) * c == a * (b * c)
のモナド版のようなものです。
例えば、最初の法則は x
を Foo
で包んでからやっぱり中身の x
を取り出してメソッド f
を適用した結果は、直接 f
を x
に適用した結果と等しいという意味です。要するに、 of
で値を包むときや flatMap
で取り出すときに余計なことをするなというだけです。
積の結合法則を満たさなければ数とは言えないというのと同じように、モナド則を満たさなければモナドとは言えないという当然の性質を述べているに過ぎません。自作の Number
クラスに結合法則が成り立たないような( *
の代わりの) multiply
メソッドを実装することはできますがそれが意味を持たないのと同じように、モナド則を満たさないモナドっぽい型を実装することはできますがそれはナンセンスなのです。
他の二つについても、中身をじっくり見てみればそれが何を意味するのかわかると思いますし、当然満たすべきものであることがわかると思います。
ファンクターとは
前述の通り、モナドはファンクターでないといけません。ファンクターとは何でしょうか。
Foo<T>
が次の条件を満たすとき、 Foo<T>
はファンクターです。
-
<U> Foo<U> map(Function<T, U>)
というメソッドを持つ。 - 上記の実装が ファンクター則(後述)を満たす。
同じくざっくりと言えば、適切に実装された map
を持った型はファンクターと言えるでしょう。
ファンクター則
次のような、引数で受けた値を返すだけのメソッド id
があるとします。
static <T> id(T x) {
return x;
}
そのとき、ファンクター則は次のように書けます。
- 恒等メソッド
id
(後述)に対して、foo.map(x -> id(x)) == id(foo)
が成り立つ。 -
foo.map(x -> g(f(x))) == foo.map(x -> f(x)).map(y -> g(y))
が成り立つ。
ファンクター則もモナド則同様、じっくり見れば当然満たすべきものだとわかると思います。
まとめ
Optional
は憎き NullPointerException
と戦うための強力な武器です。でも、 Optional
に包まれた値を扱おうとすると少し面倒なことがあります。
そんなときには、 map
や flatMap
のようなモナド的なメソッドを使えばコードを簡潔に書けることがあります。 map
や flatMap
を習得して快適な Optional
ライフを送りましょう!
おまけ:Optionalと変性
去年の Advent Calendar では Java のジェネリクスと変性について書きました。
Optional
は大抵の場合、共変として問題ないので、(特にメソッドの引数や戻り値にする場合には) Optional<? extends Foo>
の形で使うのがいいと思います。
// 非変の場合( Cat は Animal のサブクラスとする)
Optional<Cat> cat = Optional.of(new Cat());
Optional<Animal> animal = cat; // コンパイルエラー
// 共変の場合( Cat は Animal のサブクラスとする)
Optional<? extends Cat> cat = Optional.of(new Cat());
Optional<? extends Animal> animal = cat; // OK