【Java】Optional orElseとorElseGetの使い分け

Java

はじめに

実装中に、Optional.orElseとOptional.orElseGetの使い分けで悩んだので自身の学習結果も兼ねて作成した記事です。どういう時にorElseを採用するのか/orElseGetを採用するのか。サンプルコードも交えて記載しています。

想定する読者

  • Javaに触れて長いけど、Java7以前に触れる機会が多かった方
  • かつ、最近Java8以降のバージョンに触れるようになった方

判断基準

最初に結論(自分の中で)

次の場合はOptional.orElseGet()をつかう。としました。

  • 副作用のあるメソッド呼び出しを行う時

文章だけだと正直わかりづらいので、次項でコード例を出しますが、自身の判断基準としてはこれになります。

Javaによる関数型プログラミング Java 8ラムダ式とStream [ ヴェンカット・サブラマニアム ]

価格:2,860円
(2021/2/3 23:04時点)
感想(0件)

実装例

Optional.orElse()を使った事例

副作用があるメソッドを呼び出すときにorElseを使用してはいけないかを、サンプルコードで少し追いかけてみます。getSequenceId()が副作用アリのメソッドです。(フィールド変数を更新します)

//【OptionalTest.java】
public class OptionalTest {
    private static Integer sequenceId = 0;

    public static void main(String[] args) {

        // sequenceStr には、getSequenceId()の値が返される。
        Optional<String> nullValue = Optional.ofNullable(null);
        String sequenceStr = nullValue.orElse(getSequenceId());
        System.out.println("sequenceId = " + sequenceStr);

        // sequenceStr には、notNullValue.value()が返される。けど、getSequenceId()も呼び出される
        Optional<String> notNullValue = Optional.ofNullable("nonNull");
        sequenceStr = notNullValue.orElse(getSequenceId());
        System.out.println("sequenceId = " + sequenceStr);
    }

    private static String getSequenceId() {
        // メソッドコールの都度、フィールド変数をインクリメントする。
        // なので、呼び出しをするたびに戻り値が変わる。
        sequenceId++;
        return String.valueOf(sequenceId);
    }
}

次に、Optional.orElse()のコードを見てみます。引数はなんらかのオブジェクトを受け取ります。

public T orElse(T other) { 
    return value != null ? value : other;
}

Optional.orElse()のコードとOptionalTest.javaのコードを踏まえて、呼び出し順を整理すると次のコードのようになります。

Optional<String> notNullValue = Optional.ofNullable("nonNull");

// String sequenceStr = notNullValue.orElse(getSequenceValue());
// ↓
// ↓ 分解して呼び出し順を整理
// ↓
String val = getSequenceValue();                  // フィールド変数のsequenceIdがインクリメントされる。
String sequenceStr = notNullValue.orElse(val);    // "nonNull"がsequenceStrに設定される。valは捨てられる

上述のように、getSequenceValue()を呼び出した後に、Optional.orElse()で評価を行います。したがって、Optionalの中身如何に関わらず、getSequenceValue()は呼び出され、取得した値は使われることなく捨てられてしまう可能性があります。

このサンプルコード(OptionalTest.java)では、getSequenceValue()を呼びだすと、必ずフィールド変数がインクリメントされます。そのため、フィールド変数を連番として使おうとすると、歯抜けの番号が割り振られる可能性が生じます。

Optional.orElseGet()に置き換える

OptionalTest.javaのnotNullケースをorElseGet()に置き換えた場合のコードで考えてみます。この場合、副作用アリのメソッドはコールされず、よってフィールド変数も更新されません。

//【OptionalTest2.java】
public class OptionalTest2 {
    private static Integer sequenceId = 0;

    public static void main(String[] args) {
        // sequenceStr に"nonNull"が設定される。getSequenceId()は実行されない
        Optional<String> notNullValue = Optional.ofNullable("nonNull");
        sequenceStr = notNullValue.orElseGet(() -> getSequenceId());
        System.out.println("sequenceId = " + sequenceStr);
    }
    private static String getSequenceId() {
        sequenceId++;
        return String.valueOf(sequenceId);
    }
}

次に、Optional.orElseGet()のコードを見てみます。引数はSupplierで受け取ります。

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

orElseGet()の場合、valueがnullの場合に初めてotherの要素が動作します。前述のorElse()の時と同様に、Optional.orElseGet()のコードとOptionalTest2.javaのコードを踏まえて、呼び出し順を整理すると次のコードのようになります。

Optional<String> notNullValue = Optional.ofNullable("nonNull");

// String sequenceStr = notNullValue.orElseGet(() -> getSequenceValue());
// ↓
// ↓ 分解して呼び出し順を整理
// ↓
if (notNullValue.isPresent()) {
    sequenceStr = notNullValue.get();             // "nonNull"がsequenceStrに設定される
} else {
    sequenceStr = getSequenceValue();             // getSequenceValue()の結果がsequenceStrに設定される
}

このように、何らかの変更があるメソッドを呼び出す仕組みになっているコードでは、orElseGet()を使うことで想定しない状態の変化や副作用を防止することができます。

最後に

今回のサンプルコードのように、グローバルにアクセスできるフィールド変数を更新するようなコードはほとんどないと信じていますが、データベースにアクセスするような処理を書く場合もあるでしょう。その時に、orElse()の中にデータベースアクセスを行うようなコードを書いてしまうと、無駄なI/Oの発生や想定しないレコードの追加・変更などが生じてしまうかもしれません。

逆に、定数などの決まった値を取り出す場合はorElse()を使用すればいいと思います。

Optional<String> notNullValue = Optional.ofNullable("nonNull");
notNullValue.orElse("else case.");
notNullValue.orElseGet(() -> "else case.");

記述量も減りますのでわざわざorElseGet()を使う必要がないですね。

場面ごとに適切なモノは変わりますから、その場その場で良い選択ができるようになりたいですね。

コメント

タイトルとURLをコピーしました