2019/04/24
JavaのOptionalを使ってみたら手放せなくなった
目次
こんにちは、アーキテクトのQZ西垣です。
平成最後の開発者ブログになります。
今回はJava8で導入されたOptionalについての話です。
Optionalって?
Optionalは前述のとおりJava8で導入された新しいクラスです。
当然興味を持ったので調べてみたところ、JavaDocで以下のように説明されています。
null以外の値が含まれている場合も含まれていない場合もあるコンテナ・オブジェクトです。
なんだかよくわからない。
Stream APIと対になる機能とのことですが、正直これだけでは何が便利なのかさっぱり分かりませんでした。
そんなわけで、Java8導入後もOptionalを使用しないコーディングを続けていたのです。
わからないけれど
それからしばらくはモヤモヤした日々を過ごしていました。
Optionalを使用することはないものの、ずっと考えて続けていたのです。
わざわざ追加されたクラスなのですから、きっと何か使い道があるに違いない。
Optionalと同時に導入されたStream APIについては多用していました。
その過程で関数型言語に関する知識も少しながら深めていくことになります。
また、同僚のゆうみさんとSlackで関数型言語的にどういう記述が望ましいかについても話しました。
ひらめいた
そんなある日、ふと突然トイレの個室でひらめいたのです。
まずは以下のコードをご覧ください。
|
1 2 3 4 5 6 7 8 |
class Hoge0 { public static void main() { System.out.println(calc0(10)); } private static int calc0(int x) { return calc1(x + 2); } private static int calc1(int x) { return calc2(x * 2); } private static int calc2(int x) { return x ^ 2; } } |
このコードには以下の問題があります。
- メソッド間に強い依存関係が存在している
問題を解決してみます。
|
1 2 3 4 5 6 7 8 |
class Hoge1 { public static void main() { System.out.println(calc2(calc1(calc0(10)))); } private static int calc0(int x) { return x + 2; } private static int calc1(int x) { return x * 2; } private static int calc2(int x) { return x ^ 2; } } |
- メソッド間に依存関係はない
しかし別の問題が発生しています。
- カッコが多重にカスケードされていて読みづらい(コードの読みやすさは大切です!)
さらに問題を解決します。
|
1 2 3 4 5 6 7 8 9 10 11 |
class Hoge2 { public static void main() { int x0 = calc0(10); int x1 = calc1(x2); int x2 = calc2(x1); System.out.println(x0); } private static int calc0(int x) { return x + 2; } private static int calc1(int x) { return x * 2; } private static int calc2(int x) { return x ^ 2; } } |
- メソッド間に依存関係はない
- カッコはシンプルなので読みやすい
しかしまた別の問題が発生してしまいました。
- スコープのわりに実際に使用される期間が短い変数がいくつもある
ここでOptionalの出番です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Hoge3 { public static void main() { int result = Optional.of(10) .map(x -> calc0(x)) .map(x -> calc1(x)) .map(x -> calc2(x)) .get(); System.out.println(result); } private static int calc0(int x) { return x + 2; } private static int calc1(int x) { return x * 2; } private static int calc2(int x) { return x ^ 2; } } |
- メソッド間に依存関係はない
- カッコは2重までなので許容範囲内
- 使用される期間が短い変数はラムダ式内に閉じ込められるためスコープは狭い
なんと、問題を全て解決できてしまいました。
しかも、このコードはさらに最適化可能なのです。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
class Hoge4 { public static void main() { Optional.of(10) .map(Hoge4::calc0) .map(Hoge4::calc1) .map(Hoge4::calc2) .ifPresent(System.out::println); } private static int calc0(int x) { return x + 2; } private static int calc1(int x) { return x * 2; } private static int calc2(int x) { return x ^ 2; } } |
ラムダ式をメソッド参照に置き換え、Optional::getの使用をやめOptional::ifPresentを使用することにより、mainメソッドからすべての変数を除去できてしまいました。
いくつかの変数を元に新しい変数を作り出し、さらにそれを元に次の新しい変数を作り出す。
プログラムで散見するこの流れにおいて、Optionalは極めて強力な記法であることを、遅まきながら理解しました。
理解した以上は使わなければ勿体無い。
さまざまなケースでOptionalを使ってみることにより、さらに理解を深めました。
その結果、Optionalのことがよく分からなくても、使ってみると便利なケースも見えてきます。
カスケードしたMapとOptionalを組み合わせる
一番手っ取り早くOptionalの効果が分かるのは、カスケードされたMapから値を取得する場合ではないでしょうか。
まずはOptionalを使わない場合のサンプルを見てください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class Hoge5 { public static void main() { Map<Integer, Map<Integer, Map<Integer, String>>> map = generateMap(); System.out.println(pickValue(map, 0, 1, 2, “”)); } private static Map<Integer, Map<Integer, Map<Integer, String>>> generateMap() { // マップを生成し返却する } private static String pickValue(Map<Integer, Map<Integer, Map<Integer, String>>> map, Integer key0, Integer key1, Integer key2, String defaultValue) { if (map == null) { return defaultValue; } Map<String, Map<String, String>> map0 = map.get(key0); if (map0 == null) { return defaultValue; } Map<String, String> map1 = map.get(key1); if (map1 == null) { return defaultValue; } String str = map1.get(key2); if (str == null) { return defaultValue; } return str; } } |
Mapからgetするごとにnullチェックを行わないとNullPointerExceptionが発生する恐れがあるため、非常に冗長なコードに見えます。
一方、Optionalを使う場合のサンプルです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Hoge6 { public static void main() { Map<Integer, Map<Integer, Map<Integer, String>>> map = generateMap(); System.out.println(pickValue(map0, 0, 1, 2, “”)); } private static Map<Integer, Map<Integer, Map<Integer, String>>> generateMap() { // マップを生成し返却する } private static String pickValue(Map<Integer, Map<Integer, Map<Integer, String>>> map, Integer key0, Integer key1, Integer key2, String defaultValue) { return Optional.ofNullable(map) .map(m -> m.get(key0)) // mapがnullならこの行は実行されずorElseに飛ぶ .map(m -> m.get(key1)) // 直前の処理の結果がnullならこの行は実行されずorElseに飛ぶ .map(m -> m.get(key2)) // 同上 .orElse(defaultValue); // 上述の処理の結果のいずれかがnullならこの結果が返却される } } |
Optionalは、nullの場合処理がorElse系までジャンプするのでこのような記述ができます。
さらにこれをFunctionと組み合わせることにより、Mapに近い使い勝手でNullPointerExceptionの発生を回避することができます。
まずはFunctionを使わないサンプルです。
|
1 2 3 4 5 6 7 8 9 10 |
class Hoge7 { public static void main() { Map<Integer, Map<Integer, Map<Integer, String>>> map = generateMap(); System.out.println(map.get(0).get(1).get(2)); // mapの内容次第でNullPointerException発生 } private static Map<Integer, Map<Integer, Map<Integer, String>>> generateMap() { // マップを生成し返却する } } |
コメントのとおり、NullPointerExceptionが発生する可能性があります。
次にFunctionを使うサンプルです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Hoge8 { public static void main() { Function<Integer, Function<Integer, Function<Integer, String>>> toValue = toValue(); System.out.println(toValue.apply(0).apply(1).apply(2)); // NullPointerExceptionを考慮する必要はない } private static Function<Integer, Function<Integer, Function<integer, String>>> toValue() { return key0 -> key1 -> key2 -> Optional.ofNullable(generateMap()) .map(m -> m.get(key0)) .map(m -> m.get(key1)) .map(m -> m.get(key2)) .orElse(“”); } private static Map<Integer, Map<Integer, Map<Integer, String>>> generateMap() { // マップを生成し返却する } } |
NullPointerExceptionの発生を気にする必要がなくなったのがお分かりいただけるでしょうか。
おわりに
Optionalは、工夫次第で可読性向上のためのさまざまなメリットを享受できる、非常に強力なクラスになります。
ストリームに比べると少し分かりにくいですが、ストリームに負けず劣らず便利な機能であることは間違いありません。
Java9からはifPresentOrElseが追加され、さらに便利になっています。
説明されてもなんだかよく分からないという方もいらっしゃるとは思います。
自分も当初はどう使えばいいのかまるで見当がつきませんでした。
しかし、使ってみればOptionalの便利さが身にしみて理解できるはずです。
まずは使ってみましょう!








