[Java8] Optionalで脱Exception!

  • 36
    いいね
  • 5
    コメント

はじめに

Javaで「失敗」を表現する場合、従来は例外(Exception)が用いられてきました。しかしJava8からはOptioanlクラスが登場し、例外を発生させないようにプログラムを組むことができるようになりました。

この記事ではそんなOptionalの使い方を紹介いたします!

従来方法

とにもかくにも従来方法をまずは復習しましょう。コマンドライン引数を1つとって、それを数値に変換して1を足すプログラムを作ってみます。

class Try {
  public static void main(String[] args) {
    try {
      // ここで例外が発生
      int num = Integer.parseInt(args[0]);
      System.out.println(num);
    } catch (ArrayIndexOutOfBoundsException e) {
      System.out.println("Missing argument.");
    } catch (NumberFormatException e) {
      System.out.println("Please input a number.");
    }
}

ご存じの通り、まず配列にアクセスする際にArrayIndexOutOfBoundsExceptionが出る可能性があるのでこれをキャッチする必要があります。さらに、parseIntNumberFormatExceptionを出す可能性があるのでこれもキャッチする必要があります。

もし、キャッチし忘れてしまうと誤った値が入力された場合にプログラムがクラッシュしてしまいます。

ここでOptionalを利用することで、キャッチのし忘れを防ぐことができます。

Optionalとは

Optionalは、「値が有るかもしれない無いかもしれない」という意味(文脈)を表すクラスです。

例えばOptional<Integer>は「Integerクラスの値が入っているかもしれないし、入っていないかもしれない」という状況を表すことができます。

今までのメソッドでは、「◦型の引数をとって◦型の値を返す」という一連の処理とは別に、計算に失敗した場合は「◦◦Exceptionを出す」という規定がありました。しかしOptionalを使えば、型を見ただけで計算が失敗しそうだと言うことが伝わります。

Exceptionalの存在に気づかず、実行時に初めてExceptionを出してしまってクラッシュさせるという事態を、未然に防ぐことができるようになります。

Optionalの値を生成する

Optionalにくるまれた値を生成することは非常に簡単です。Optional#ofあるいはOptional#ofNullableを使うことで簡単に実現できます。java.util.Optionalに格納されているので、あらかじめインポートしておいてください。

Optional#ofOptional#ofNullableの違いですが、前者は絶対にnullの可能性がない値からOptional値を生成するときに用い、後者はnullになるかもしれない値からOptional値を生成するために用いるという違いがあります。

例えば1という値は絶対にnullになる可能性がないためOptional#ofを用いてOptional値を生成しますが、例えばargs[0]はnullになる可能性があるため、Optional#ofNullableを用いる必要があります。

また、計算に失敗したなどで空のOptional値を返したい場合はOptional#emptyを使ってください。

例として、配列に安全にアクセスするメソッドを作ってみましょう。

public static <T> Optional<T> getN(T[] arr, int n) {
  try {
    return Optional.ofNullable(arr[n]);
  } catch (ArrayIndexOutOfBoundsException e) {
    return Optional.empty();
}

ArrayIndexOutOfBoundsExceptionをキャッチしつつ、Optionalにつつんで配列のn番目を返すメソッドができました。(一応ofNullableを使っていますが、結局catch側でemptyを返しているのであんまり意味ないかも)

続いて、文字列をint型に変換するメソッドを作ってみましょう。

public static Optional<Integer> strToInt(String s) {
  try {
    return Optional.ofNullable(Integer.valueOf(s));
  } catch (NumberFormatException e) {
    return Optional.empty();
  }
}

こちらも同様に、Optionalで包んでIntegerの値を返しています。

Optional値を使う

さて、Optionalに入っている値を得ることには成功しましたが、ほとんどのメソッドはOptional型に対応していません。String型を受け取るメソッドはあっても、Optoinal<String>を受け取るメソッドはそうそうないものです。

しかしそんなことは気にしなくてもかまいません。Optionalクラスの様々なメソッドを駆使すれば、既存の関数を何ら改造することなく用いることができるのです。

getメソッド

まず紹介するのはgetメソッドです。しかしこのメソッドはよっぽどのことが無い限り使ってはいけません。もしOptionalの中に値が入っていればその値を返すので問題はありませんが、入っていなければこのメソッドはNoSuchElementExceptionを吐きます。

せっかくExceptionをはき出さないように作ったプログラムが落ちてしまいます。

この問題は、後述するorElseを使うことで解決することができます。

orElseメソッド

orElseメソッドはgetメソッドを改良したものです。Optional値の中に値が入っていればその値を取り出し、入っていなければorElseメソッドの引数の値をデフォルト値として返します。

こうすることで例外が発生することなく、安心してプログラムを書くことができます。orElseメソッドにはデフォルト値を引数で与えることが必須になっているので、デフォルト値を与え忘れていてもコンパイル時にエラーを出してくれます。

mapメソッド

mapメソッドは、Optional値の中に入っている値に対して、関数(宣言としてはmapper関数インタフェース)を適用するためのメソッドです。

例えばこんな風に。(さらっとラムダ式を使っていますが、関数ならなんでもOK)

public static void main(String[] args) {
  Optional.of(1)
    .map(n -> n * n) // mapで関数をそのまま適用
}

計算に成功すればOptionalに包まれた計算結果が、失敗すればemptyが返ってきます。

返り値は何でもいいのですが、voidだけはダメです。また、Optional値が返り値になる関数を適用してしまうとOptionalが二重になってしまうのでこれも避けた方がいいでしょう。これらについての解決方法は後述します。

flatMapメソッド

flatMapは二重になったOptionalを一重にしてくれるメソッドです。具体的には、二重になっている方の内側を取り除いているのではないかと考えています1

例えばこんな風に失敗が続きそうな場所に使うと便利です。(strToIntは前述のもの)

public static void main(String[] args) {
  Optional.of("str")
    .flatMap(s -> strToInt(s)) // flatMapで2重のOptionalを1重に
}

flatMapも、計算に成功すればOptionalに包まれた計算結果が、失敗すればemptyが返ってきます。

ifPresentメソッド

ifPresentメソッドはvoid型の返り値を持つ関数を適用したいときに使います。例えばprintlnとか。早速やってみましょう。

public static void main(String[] args) {
  getN(args, 0) // Optionalに包んで配列にアクセス
    .flatMap(s -> strToInt(s))
    .map(n -> n * n)
    .ifPresent(System.out::println); // 結果の表示も楽々
}

このコードの場合値がOptional値の中に入っているので無事値が表示されますが、emptyだった場合ifPresentは何もしません。ここら辺の処理をif文を一切書かずに行えるのは大変楽ができていいですね。

Optionalの欠点

Optionalは「失敗or成功」の2値しか表現できません。したがって、失敗の原因を表現することが非常に難しくなっています。

ScalaやHaskellにはEitherという型があって成功すればその値、失敗すれば失敗の原因を返すことができますが、JavaにはEitherが存在しないのでどうしても使いたい場合は頑張って実装してください...

おわりに

さて、あらかたOptionalの使い方も説明し終わりました。Optionalは例外の存在を型自体に規定して、メソッドを利用する際に注意を引き立ててくれる貴重な存在です。是非とも活用してあげてください。

今までに作ってきた大量のメソッドも、mapメソッドを使えばそのままOptional値に適用することが可能です。これから作る危険なメソッドは、ぜひOptionalにくるんで返してみましょう。

ぜひとも安全なJavaライフをお楽しみください!

おまけ

ところでJavaのOptionalはFunctorっぽさそうで、Monadっぽさそうな感じがしてきませんか...?

ということで早速証明してみましょう。

Functor則

OptionalがFunctorかどうかを調べるためにはFunctor則を満たすかどうかを調べればOkです。テストコードは以下のようになります。

import java.util.Optional;

class FunctorLaw {
    private static <T> T id(T a) {
        return a;
    }

    private static int f(int n) {
        return n + 1;
    }

    private static int g(int n) {
        return n * 2;
    }

    public static boolean test1() {
        return Optional.of(1).map(FunctorLaw::id)
                .equals( id(Optional.of(1)) );
    }

    public static boolean test2() {
        return Optional.of(1).map(n -> f(g(n)))
                .equals( Optional.of(1).map(n -> g(n)).map(n -> f(n)) );
    }
}

test1とtest2を実行して、これらがtrueになれば証明成功ということで実行してみたところ見事両方ともtrueが返ってきました。

要するに、以下の2つの法則が満たされているかどうかを調べています。

  1. Optionalの中の値にidを適用したものとOptional値まるごとにidを適用したものが等しい
  2. fとgの合成関数をOptionalに適用したものと、gとfを順番にOptionalに適用したものが等しい

こういった場合、一見問題なくFunctor則を満たしているように見えます。

しかしながら、Functor則を満たさない場合が存在します。次のようなコードを考えてみます。

import java.util.Optional;

class FuncLaw {
  public static void main(String[] args) {
    System.out.println(test2());
  }

  private static Integer f(Integer n) {
    return 1;
  }

  private static Integer g(Integer n) {
    return null;
  }

  private static boolean test2() {
    return Optional.of(1).map(n -> f(g(n)))
      .equals( Optional.of(1).map(n -> g(n)).map(n -> f(n)) );
  }
}

この場合、f(g(n))の結果は1になりますが、map(g).map(f)の場合Emptyになります。したがってFunctor則は満たされないことになります。(おのれnull)

Monad則

さて、こちらもMonad則を満たすかどうか調べてみましょう。以下のようなテストコードを書いてみました。

import java.util.Optional;

public class MonadLaw {
    private static Optional<Integer> f(int n) {
        return Optional.of(n + 1);
    }

    public static Optional<Integer> g(int n) {
        return Optional.of(n * 2);
    }

    public static boolean test1() {
        return Optional.of(1).flatMap(MonadLaw::f)
                .equals( f(1) );
    }

    public static boolean test2() {
        return Optional.of(1).flatMap(n -> Optional.of(n))
                .equals( Optional.of(1) );
    }

    public static boolean test3() {
        return ( Optional.of(1).flatMap(n -> f(n)) ).flatMap(n -> g(n))
                .equals( Optional.of(1).flatMap(n -> f(n).flatMap(m -> g(m))) );
    }
}

これはtest1〜3を実行してtrueが返ってくればOKです。OKでした。

Monad則は以下の3つの法則です。

  1. Optionalに入れた値にflatMapを使ってfを適用させたものと、fにその値を入れたものは等しい(左恒等性)
  2. Optionalに値を入れて、flatMapを使って今入れた値から新たなOptional値を作ったものと、その値から生成したOptional値が等しい(右恒等性)
  3. どの順番でflatMapを使っても計算結果が不変(結合法則)

これらがすべて満たされていることがわかったので、やはりJavaのOptionalはMonadだと言い張ることができるでしょう。

おまけのまとめ

JavaのOptionalはFunctorではありませんでしたが、Monadでした。この性質を満たしているからこそ、Optionalなんて考慮されていなかった関数をそのまま食わせたり、Optional同士の結合がやりやすかったりするわけなので、感心するばかりです。

実はStreamでも試そうかと思ったのですが、equalsメソッドの実装が悪いのか、内容が同じでもfalseが返ってくるのであきらめました。そこまで実装し直す気力も無かったので...

更新履歴

  1. Optional#getがnullを返すという大嘘を訂正(@heignamericanさんより)
  2. Opotional#orElseOptional#getOrElseだと勘違いしてたのも訂正(@heignamericanさんより)
  3. Optionalの欠点について言及(@felisさんより)
  4. Functor則を満たさない場合について言及(@gakuzzzzさんより)
  5. Functor則は満たされないということを明記(@gakuzzzzさんより)

注釈


  1. 詳しく調べたわけではないので...