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
の便利さが身にしみて理解できるはずです。
まずは使ってみましょう!