エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

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では、結果が存在しない場合(isPresentfalseになる場合)に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では煩雑になりがちな、値が取得できない場合(計算が失敗する場合)の処理を、安全にかつ柔軟に行うことができる。

関数型プログラミング

ここまでの説明でStreamOptionalが似たような特徴を持っていることに気づいたかもしれない。

  • filter, map, flatMap などのメソッドがある。
  • 上記のメソッドや(Streamにおける)中間操作を連結することで、内部データに対する演算を行う。
  • Streamの終端操作(collectや、anyMatchなど)や、OptionalgetgetOrElseを行うまで内部のデータを触ることができない。

これらは、関数型プログラミングの作法に則っている。

Optional<T>, Stream<T> はいずれも Tという任意の型に対して、特定の性質(Optionalならば「計算が失敗するかもしれない」、Streamならば「複数の値があり得る」)を付与したものと見ることができる。 関数型プログラミング言語では、OptionalStreamのような仕組みの他にも、IO処理やエラー処理を行うために様々なコンテナ型を用いる。
(ちなみにHaskellでは、OptionalMaybeStreamはリスト[]としてサポートしている)。

この様々なコンテナ型に対して、共通の振る舞いを定義したものが、mapflatMap関数である。

  • map
    型Tから型T' を生成する関数を受け取り、コンテナ型はそのまま変えずに、中の型の値のみを変換する処理を行う。
  • flatMap
    型Tからコンテナ型を生成する関数を受け取り、既存のコンテナ型の値と合成を行う処理を行う。(これはモナドと呼ばれるパターンである)
  • filter
    flatMapの亜種であり、条件に一致しない場合コンテナ型のデフォルト値(Streamなら空のストリーム, OptionalならEmpty)を生成する処理を行う。

関数型言語では、上記のようにして異なるコンテナ型であっても、同じ関数を使用できるようにし、抽象度や再利用性を高めている。

Java8を使って、簡潔で洗練されたプログラムが書くためには、このような関数型プログラム由来の機能をきちんと押さえておく必要がある。

[前多 賢太郎]