Java8の基本 - Optional型
前回まではStream
クラスを解説してきたが、今回はJava8が提供するもう1つの関数型プログラミングの機能であるOptional
クラスを紹介する。
Optional
型
Optional
は存在するかもしれないT型の値を1つ保持するクラスである。
(OptinalInt
, OptionalDouble
など、プリミティブ型用のOptional
もある)。
Stream
における終端操作の findFirst
, findAny
の戻り値はOptional
になっている。
これらの操作はStream
内の複数の要素から1つを選択して戻り値とする処理だが、Stream
の要素が何も無い場合は要素の選択ができないので、結果が無いということを知らせる必要がある。
従来であれば、値そのものかnull
を返すような設計となるが、Stream
ではそのような時に、Optional
を戻り値としている。
Optional
は結果となるT型の値をラップするものであり、このようにすることで、従来のnull
チェックに代わる柔軟な処理が行える。
では、Optional
のメソッドを見ていこう。
isPresent
, get
メソッド
Optional#isPresent
は 値が存在する場合にtrue
を、存在しない場合はfalse
を返す。
Optional#get
はラップされている計算結果のT型の値を取得するメソッドである。
Optional#get
では、結果が存在しない場合(isPresent
がfalse
になる場合)にNullPointerException
が発生する。
このため、安全にget
を実行するには事前にisPresent
で判定する必要がある。
このように、null
を返すかもしれない値に対してnull
チェックが強制されるので、思わぬNullPointerException
の発生を抑制できる。
その他の取得用メソッド
Optional
では、isPresent
によるチェックが煩雑な場合を考慮して、他にも取得用メソッドを用意している。
これらは Optional#orElse
,Optional#orElseGet
,Optional#orElseGet
,Optional#orElseThrow
などで、値が存在すれば値を取得し、存在しない場合は各メソッドの引数に指定した代替値が返される。
値が存在する場合だけラムダ式を実行するOptional#ifPresent
メソッドもある。
// ofは値ありのOptional, emptyは値なしのOptionalを生成する。 Optional.of("4").ifPresent(System.out::println); // 4が表示される Optional.empty().ifPresent(System.out::println); // 何も行われない
filter
, map
, flatMap
メソッド
Stream
と同様に、Optional#filter
, Optional#map
,Optional#flatMap
も用意されている。
これらのメソッドは値が存在する場合のみ引数のラムダ式を実行し、値が存在しない場合はOptional
自身をそのまま返すようになっている。
この仕組みにより、計算の結果が失敗となる場合であっても計算を重ねることができる。
例として、Mapからとあるキーで取得した値をキーとしてMapから更に値を取得するケースを考える。
従来の方式であれば、このような処理は,Map#containsKey
によるキーの存在判定が何度もネストしていく構造になる。
Map<String, String> map = new HashMap<String, String>(){{ put("A", "B"); put("B", "C"); put("C", "D"); }}; // 3階層まで取得。 String value = null; if (map.containsKey("A")) { String val1 = map.get("A"); if (map.containsKey(val1)) { String val2 = map.get(val1); if (map.containsKey(val2)) { value = map.get(val2); } } }
これを変更して、Map
を拡張してデフォルトメソッドを定義し、キーに対応する値をOptional
で返すようにしてみる。
(このケースだけであれば、Map
とキーを引数、Optional
を戻り値とするstatic
メソッドでも実現可能である。)
interface OptMap<K,V> extends Map<K,V> { default Optional<V> getWithOpt(K key) { if (this.containsKey(key)) { return Optional.of(this.get(key)); } else { return Optional.empty(); } } } class OptHashMap<K,V> extends HashMap<K, V> implements OptMap<K, V>{}
上記のOptMap
を用いることで、多段階の値の取得は以下のようになる。
OptMap<String, String> optMap = new OptHashMap<String, String>(){{ put("A", "B"); put("B", "C"); put("C", "D"); }}; optMap.getWithOpt("A") .flatMap(optMap::getWithOpt) .flatMap(optMap::getWithOpt) .ifPresent(System.out::println);
このコードでは、計算途中でoptMap
から値が取得できなくても実行時エラーにならない。
また、値を取得する回数を増やすには単純にflatMap
の呼び出しを付け加えればよい。
このように、Optional
を用いることによって、通常のMap#get
では煩雑になりがちな、値が取得できない場合(計算が失敗する場合)の処理を、安全にかつ柔軟に行うことができる。
関数型プログラミング
ここまでの説明でStream
とOptional
が似たような特徴を持っていることに気づいたかもしれない。
filter
,map
,flatMap
などのメソッドがある。- 上記のメソッドや(
Stream
における)中間操作を連結することで、内部データに対する演算を行う。 Stream
の終端操作(collect
や、anyMatch
など)や、Optional
のget
やgetOrElse
を行うまで内部のデータを触ることができない。
これらは、関数型プログラミングの作法に則っている。
Optional<T>
, Stream<T>
はいずれも Tという任意の型に対して、特定の性質(Optional
ならば「計算が失敗するかもしれない」、Stream
ならば「複数の値があり得る」)を付与したものと見ることができる。
関数型プログラミング言語では、Optional
やStream
のような仕組みの他にも、IO処理やエラー処理を行うために様々なコンテナ型を用いる。
(ちなみにHaskellでは、Optional
はMaybe
、Stream
はリスト[]
としてサポートしている)。
この様々なコンテナ型に対して、共通の振る舞いを定義したものが、map
やflatMap
関数である。
map
型Tから型T' を生成する関数を受け取り、コンテナ型はそのまま変えずに、中の型の値のみを変換する処理を行う。flatMap
型Tからコンテナ型を生成する関数を受け取り、既存のコンテナ型の値と合成を行う処理を行う。(これはモナドと呼ばれるパターンである)filter
flatMap
の亜種であり、条件に一致しない場合コンテナ型のデフォルト値(Stream
なら空のストリーム,Optional
ならEmpty)を生成する処理を行う。
関数型言語では、上記のようにして異なるコンテナ型であっても、同じ関数を使用できるようにし、抽象度や再利用性を高めている。
Java8を使って、簡潔で洗練されたプログラムが書くためには、このような関数型プログラム由来の機能をきちんと押さえておく必要がある。
[前多 賢太郎]