NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

null安全 と Optional クラスについて

こんにちは、5月にネットコムに入社した清水です。
以前の会社では Kotlin を使ってアプリ開発をしていました。
今は Java を使ってアプリ開発をしており、その傍ら Java の研修を受けています。

今回はその研修で学んだ Java の Optionalクラスの使い方についてまとめようと思います。

null 安全ってなに?

少し前置きです。

先述のとおり、わたしは以前は Kotlin を使っていました。
Java と Kotlin 違いはいろいろありますが、大きな違いの一つとして null安全かどうか という違いがあります。

  • Kotlin:null安全 である
  • Java:null安全 ではない

そもそも null安全 とはなんでしょうか。

コードで具体例を見ていきましょう。

/* Java */
String str = null; 
var strLength = str.length(); // str が null なので NullPointerException が発生

全く同じコードを Kotlin で書いてみます。

/* Kotlin */
var str: String? = null
var stringLength = str.length // コンパイルエラー
// 実行できない

Kotlin の場合はコンパイルエラーとなります。そのため実行ができません。
なぜでしょうか?
答えは null が入っている可能性があるため です。

  1. 中身が null かもしれない!
  2. NullPointerException が発生するかも!
  3. コンパイルさせません!

ってことですね

このように null が入っている可能性がある時点で、 null チェックなしにそれ以上は進めない設計や仕様を null安全である といいます。
null安全な言語の一番大きなメリットは、NullPointerException の発生をほぼ完全に無くすことができることです。*1

ちなみに上記の Kotlin のコードは、null のチェック処理を入れることでコンパイルおよび実行ができるようになります。

var str : String? = null
var stringLength = 0
if (str != null) {
    stringLength  = str.length // str は null ではないことが確定しているのでコンパイルが通る
}

このように null安全の言語は必ず null チェック等の対応が必須となります。

Optional<T> ってなに?

前述のとおり、Java は null 安全な言語 ではありません。ですが、Javaも null安全のように扱うことができる方法があります。
それが今回紹介する Optional<T> クラス です。
オブジェクトが null だった場合の処理を簡単かつ少ない記述で書くことができます。

Optional は オブジェクトが中に入っている箱のイメージです。
この箱から中身のオブジェクトを安全に取りだしたり、処理をしたりする操作が基本になります。
<T>は中身のクラスの型です。 Optional<String> になったり、Optional<Student>になったり、何かしらの型が入ります。

サンプルのコードがあった方が直感的にわかりやすいと思うので、よく使用されるメソッドを使って例を用意しました。

Optional を生成するメソッド

Optional のインスタンスがないと始まらないので、まずはそれを作成します。

of ・・・ 中身の入ったOptionalを作る

Optional<String> strOptional = Optional.of("Hello"); 

これで "Hello" が中身にあるOptionalクラスのインスタンスができました。
ですが、ofメソッドは引数に null を指定すると「中身がない!」となり NullPointerException が出てしまいます。
確実に中身が入っているとわかっている Optional ではあまり意味がないので次の ofNullable のほうが使い勝手が良いです。

ofNullable ・・・ 中身が null かもしれない Optional を作る

String str = "Hello";
Optional<String> strOptional = Optional.ofNullable(str); 
String strNull = null;
Optional<String> strOptionalNull = Optional.ofNullable(strNull); 

これでどちらのパターンにも対応できる Optional のインスタンスを作成できました。

empty ・・・ 中身が null な Optional を作る

中身が null であるとわかっている場合は empty が良いでしょう。
実際は ofNullable に null を指定した時と同じ動きをします。

Optional<String> strOptionalNull = Optional.empty(); 
// Optional<String> strOptionalNull = Optional.ofNullable(null); ← これと全く同じ

中身を返すメソッド

orElse ・・・ 中身がない場合は代わりを返す

orElse は中身がなかった場合、シンプルに中身を指定できます。 中身がある場合はそのまま取り出します。

Optional<String> strOptional = Optional.of("Hello");
int strLength = strOptional.orElse("").length(); // 中身がある場合は、中身の文字列の長さを取得
System.out.println("strLength : " + strLength);
// strLength : 5

Optional<String> strOptionalNull = Optional.empty();
int strLengthNull = strOptionalNull.orElse("").length(); // 中身が null の場合は、空の文字列の長さを取得
System.out.println("strLengthNull : " + strLengthNull);
// strLengthNull : 0

orElseGet ・・・ 中身がない場合は処理をして、代わりを返す

orElseGet は中身がなかった場合、ラムダで指定した処理をして値を返します。
中身がある場合はそのまま取り出します。orElse と同じです。

@Data
class Student {
    private int id;
    private String name;
}

Optional<Student> studentOptional = Optional.empty();
Student student = studentOptional.orElseGet(() -> {
    Student taro = new Student();
    taro.setId(1);
    taro.setName("taro");
    return taro;
}); // 中身が null の場合は、Student を生成する
System.out.println("ID:" + student.id);
System.out.println("Name:" + student.name);
// ID:1
// 名前:taro

orElseとは、処理をするかどうかで使い分けるといいと思います。

orElseThrow ・・・ 中身がない場合は例外を投げる

orElseGet は中身がなかった場合、NoSuchElementException を投げます。
中身がある場合はそのまま取り出します。orElse と同じです。

Optional<String> strOptionalNull = Optional.empty();
int strLengthNull = strOptionalNull.orElseThrow().length();
// Exception in thread "main" java.util.NoSuchElementException: No value present

引数に例外を指定した場合、その例外を投げます。こちらのほうが使い勝手が良いですね。
引数はラムダで指定するため、例外時の処理を入れることもできます。

Optional<String> strOptionalNull = Optional.empty();
int strLengthNull = strOptionalNull.orElseThrow(() -> new IllegalArgumentException("content is null.")).length();
// Exception in thread "main" java.lang.IllegalArgumentException: content is null.

処理をするメソッド

ifPresent ・・・ 中身がある場合、処理をする

処理はラムダで指定します。
orElseGet との違いは返却値の有無です。
ifPresent は中身を取り出して処理をするだけであり、中身の返却はしません。void 型のメソッドです。

Optional<String> strOptional = Optional.of("Hello");
strOptional.ifPresent(str -> System.out.println(str.length())); // 中身がある場合、長さを表示
// 5

Optional<String> strOptionalNull = Optional.empty();
strOptionalNull.ifPresent(str -> System.out.println(str.length())); // 中身がない場合はなにもしない

ifPresentOrElse ・・・ 中身があるかないか判定して、ある場合は処理①、ない場合は処理②をする

ifPresentOrElse は中身がない場合の処理も指定できます。
こちらも ifPresent と同じく処理をするだけであり、中身の返却はしません。

Optional<String> strOptional = Optional.of("Hello");
strOptional.ifPresentOrElse(
        str -> System.out.println(str.length()), // ①中身がある場合は長さを表示
        () -> System.out.println(0)
); 
// 5

Optional<String> strOptionalNull = Optional.empty();
strOptionalNull.ifPresentOrElse(
        str -> System.out.println(str.length()),
        () -> System.out.println(0) // ②中身がない場合は 0 を表示
); 
// 0

ifPresentOrElse は値を返しませんが、中身がある場合とない場合それぞれで値を返したい場合はどうすれば良いでしょうか。
その場合は以下の map を使うと実現可能です。

中身を変換するメソッド

map・・・ 中身がある場合、中身の型を変換して Optional を返す

map は中身があった場合、中身の型をラムダの戻り値の型に変換できます。
また、続けて orElse を使うことで、中身がある場合とない場合それぞれで値を返すことができます。(実質 ifPresentOrElse の値を返すバージョンですね)

Optional<String> strOptional = Optional.of("Hello");
int strLength = strOptional.map(str -> str.length()).orElse(0); // length() で Optional<Integer> に変換する
System.out.println("strLength : " + strLength);
// strLength : 5

Optional<String> strOptionalNull = Optional.empty();
int strLengthNull = strOptionalNull.map(str -> str.length()).orElse(0); // 中身がない場合は 0 を返す
System.out.println("strLengthNull : " + strLengthNull);
// strLengthNull : 0

Stream の map と似ていますね。使い勝手はほとんど同じだと思います。

おわりに

今回はサンプルコードを書いて Optional クラスの使い方をざっくりまとめてみました。

一見複雑に見えますが、ひとつずつ考えればそこまで難しくないかもしれません。
こうして見返してみると中間処理や終端処理をもつ Stream クラスと少し似ている気がしますね。
Stream や ラムダ記法 と一緒に勉強すると良いかもしれません。

ラムダについてはこちらの記事が参考になります。
駆け出しエンジニアの成長日記 ~ラムダ式編~ - NRIネットコムBlog

この記事は自身の研修のアウトプットとして投稿したものですが、どなたかのお役に立てば幸いです。

*1:強制 null 解除の演算子(not-null assertion operator)を使うと発生することがあります(参考:Null safety)。