JavaでNullを安全に扱おう(Optional)
こんにちは。
SREホールディングス株式会社でエンジニアをやっております田城です。
みなさん、NullPointerException(通称ぬるぽ)に苦しめられていませんか?
今回はNullableになりうる値を安全に扱うために、Optionalと呼ばれる機能を使おうというお話をしていきます。
対象読者
Nullのような「有るかもしれないし無いかもしれない」値を使ったときに、逆参照(ex.メソッドを呼ぶ)してNullPointerExceptionが発生したことがある人へ
Optionalとは?
Optionalは、値が含まれているかもしれないし、含まれていないかもしれない値を表現するのに使います。
Javaでは、値が存在することをPresent、存在しないことをEmptyと呼びます。
Optional の値を作ろう
Optionalの値を作るための3つのメソッドがあります。
empty
値が含まれていない(=Emptyな)Optionalの値を作成するメソッドです。
// 変数定義時は型を明示しないと、ジェネリクスの型が確定しない(=varは使えない)
Optional<String> emptyOpt = Optional.empty();
ofNullable
渡された値がNullでない場合はその値を含むOptionalを返し、Nullの場合は値を含まない(=Emptyな) Optionalを返します。
public static void printNullable(Optional<String> opt) {
System.out.println(opt);
}
// 呼び出し例(nullの場合)
String nullName = null;
printNullable(Optional.ofNullable(nullName)); // 結果: Optional.empty
// 呼び出し例(null以外の場合)
String actualName = "Taro";
printNullable(Optional.ofNullable(actualName)); // 結果: Optional[Taro]
of
渡された値がNullでない場合はその値を含むOptionalを返し、Nullの場合はNullPointerExceptionが発生します。
ofNullableがあるので、あまり使わない印象です。(そもそもNullじゃないと分かっているなら直接操作したほうが早いので・・・)
public static void printOf(Optional<String> opt) {
System.out.println(opt);
}
// 呼び出し例
String name = "Taro";
printOf(Optional.of(name)); // 結果: Optional[Taro]
// 以下は例外が発生する
String nullName = null;
printOf(Optional.of(nullName)); // NullPointerException
なぜOptionalが安全なのか?
そもそも、なぜNullPointerExceptionはおきるのでしょうか。
私は Nullであるかもしれない値を直接操作(逆参照)でき、それがコンパイル時に検知されないことが問題であると考えています。
例えば、以下のようなケース
public static void unsafeLength(String str) {
System.out.println(str.length());
}
このコードだと、コンパイル自体は正常に通りますがnameがNullだった場合、実行時にNullPointerExceptionが発生してしまいます。
これがOptionalだとどうでしょう?
public static void safeLength(Optional<String> str) {
System.out.println(str.length());
/*
エラー: シンボルを見つけられません
System.out.println(str.length());
^
シンボル: メソッド length()
場所: タイプOptional<String>の変数 str
エラー1個
*/
}
コンパイルエラーが出ますね。
Optionalは、元となった値を直接操作することはできません(=すなわち、元の値のメソッドを呼ぶことができない)。これがコンパイル時に発覚するので、有るかもしれないし無いかもしれない値に対して、どう処理するべきか?というのを考え定義することができます。
では、Optionalの値はどのように扱うのでしょうか?それは、Optional専用のメソッドを使って値を扱います。詳しく見ていきましょう。
Optionalのメソッドの紹介
値を変換するメソッド
あるOptionalから様々な方法で別なOptionalに変換するメソッド群です。
map
関数を渡すことで
- Presentの場合は渡された関数を適用して新しいOptionalを返します。
- Emptyの場合はEmptyなOptionalを返します。
Optional<String> nameOpt = Optional.of("Taro");
Optional<Integer> lengthOpt = nameOpt.map(String::length);
System.out.println(lengthOpt); // Optional[4]
flatMap
Optionalを返す関数を渡すことで
- Presentの場合は渡された関数を適用し、その関数がPresentを返した場合はその値を含めた新しいOptionalを返します。Emptyの場合はEmptyなOptionalを返します。
- Emptyの場合はEmptyなOptionalを返します。
Optional<Integer> flat = nameOpt.flatMap(name -> Optional.of(Optional.of(name.length())));
System.out.println(flat); // Optional[4]
Optional<String> nameOpt = Optional.of("Taro");
Optional<Optional<Integer>> nested = nameOpt.map(name ->Optional.of(name.length()));
System.out.println(nested); // Optional[Optional[4]]
mapと似ていますが、flatの名の通り、平坦化をします。すなわち、内側にできたOptionalで二重Optionalになるのを防いで、一重のOptionalの値を返します。
filter
述語(Booleanを返す関数)を渡すことで
- Presentの場合は述語を適用し、Trueなら同じ値が含まれるOptionalを、Falseなら値が含まれていないOptionalを返します。
- Emptyの場合は値が含まれてないOptionalを返します。
Optional<String> nameOpt = Optional.of("Taro");
Optional<String> filtered = nameOpt.filter(name -> name.startsWith("T"));
System.out.println(filtered); // Optional[Taro]
Optional<String> emptyFiltered = nameOpt.filter(name -> name.startsWith("A"));
System.out.println(emptyFiltered); // Optional.empty
値を取り出すメソッド
あるOptionalから値を取り出すためのメソッド群です。
Presentの場合はその値を返すのは同じですが、Emptyの場合の挙動が異なります。
orElse
Emptyの場合はデフォルト値(=引数で指定)を返します。
Optional<String> nameOpt = Optional.empty();
String name = nameOpt.orElse("名無し");
System.out.println(name); // 名無し
orElseGet
Emptyの場合は渡された関数を使用して値を作成し返します。
初期化に大きなコストがかかるクラス(=ArrayListなどメモリ初期化や、FileStreamなどIOアクセスを要するクラス等)の場合は、こちらのほうがオススメです。
Optional<ArrayList<String>> listOpt = Optional.empty();
ArrayList<String> list = listOpt.orElseGet(ArrayList::new);
System.out.println(list); // []
get
Emptyの場合は
Optionalのメリットを潰すことになるのであまり使いません。
Optional<String> nameOpt = Optional.of("Taro");
String name = nameOpt.get();
System.out.println(name); // Taro
// Optional.empty().get(); // 実行時にNoSuchElementExceptionが発生
orElseThrow
Emptyの場合は渡された関数から返された例外をThrowします。
Emptyの場合にこれ以上処理を続行できない場合(=異常系など)に使います。
public static void printOrThrow(Optional<String> opt) {
String result = opt.orElseThrow(() -> new IllegalArgumentException("値が存在しません"));
System.out.println(result);
}
値があるときに処理を実行するメソッド(ifPresent)
Presentの場合にその値を使って処理をするためのメソッドです。
public static void greetIfPresent(Optional<String> opt) {
opt.ifPresent(name -> System.out.println("こんにちは、" + name));
}
// 呼び出し例
greetIfPresent(Optional.of("Taro")); // 結果: こんにちは、Taro
値があるかどうか確認するメソッド(isPresent)
Presentの場合にTrueを返すメソッドです。
public static void checkPresence(Optional<String> opt) {
System.out.println(opt.isPresent());
}
// 呼び出し例
checkPresence(Optional.of("Taro")); // 結果: true
checkPresence(Optional.empty()); // 結果: false
まとめ
Optionalを用いることで、有るかもしれないし無いかもしれない値を安全に表現できることを学びました。
他の言語にも似たような機能があったりします。例えば、C++やRustであれば同じ名前のOptionalというクラスがあります。HaskellであればMaybeとか。
クラスやメソッドの名は違えど、基本的な機能は同じです。
みなさんも積極的にOptionalを使って、ガッとされないコーディングライフを送りましょう!
Discussion