Java : Optionalの基本

従来(Java 7まで)、メソッドの戻り値が null となる可能性がある場合、使う側は if文を使った null チェックが必要でした。
Optionalを使うと、この null チェックをよりスマートに記述できます。

ただし、Optionalを使う上で注意しなければならないこともあります。
そのあたりも踏まえて、本記事では Optional の基本的な使い方をご紹介します。

また、ラムダ式を多用するため、もしラムダ式の理解に不安があるかたは、下記の記事も合わせてご参照ください。


概要

非null値を含んでも含まなくてもよいコンテナ・オブジェクト。 値が存在する場合、isPresent()はtrueを返します。 値が存在しない場合、オブジェクトはemptyとみなされ、isPresent()はfalseを返します。

クラス構成

Optional はとてもシンプルなクラスです。

対象となる1つの値を、持つこともあれば持たないこともあります。
この、持つ/持たない が、Optionalの大事な部分です。

Optional は、メソッドの戻り値の型として使われることを想定しています。

値を返す代わりに、その値を持った Optional を返し、
null を返す代わりに、値を持たない Optional を返します。

// 従来の null を返す例です。
public String getValue() {
    if (返す値がない場合) {
        return null;
    } else {
        return value;
    }
}
// Optional を返す例です。
public Optional<String> getValue() {
    if (返す値がない場合) {
        return Optional.empty();
    } else {
        return Optional.of(value);
    }
}

今までは、メソッドの戻り値が null を返す場合、使う側で null チェックが必要でした。
例えば、Map インタフェースの get メソッドは、指定したキーがない場合は null を返します。

final var map = Map.of(
        "key-a", "value-a",
        "key-b", "value-b");

final var a = map.get("key-a");
if (a != null) {
    System.out.println("key-a が見つかりました : " + a);
}

final var x = map.get("key-x");
if (x != null) {
    System.out.println("key-x が見つかりました : " + x);
}

// 結果
// ↓
//key-a が見つかりました : value-a

map.get("key-a") はキーが見つかるので、nullではない値 を返します。
map.get("key-x") はキーが見つからないので、null を返します。

期待どおりの結果ですね。

これを Optional を使うように書き換えてみましょう。
Optional を返すための getValue というメソッドを1つ追加します。

// Map.get の結果を Optional に変換するためだけのメソッドです。
public Optional<String> getValue(Map<String, String> map, String key) {
    return Optional.ofNullable(map.get(key));
}

先ほどのコードを getValue を使うように書き換えてみます。

final var map = Map.of(
        "key-a", "value-a",
        "key-b", "value-b");

getValue(map, "key-a").ifPresent(value -> {
    System.out.println("key-a が見つかりました : " + value);
});

getValue(map, "key-x").ifPresent(value -> {
    System.out.println("key-x が見つかりました : " + value);
});

// 結果
// ↓
//key-a が見つかりました : value-a

if文による null チェックがなくなりました。
代わりに、ifPresent メソッドで処理をします。

コード上から null表記がなくなるのが特徴ですね。

大事なこと

APIのノート:
Optionalは、主に"検索結果はありません、"を明示的に表示する必要があり、nullを使用するとエラーが発生する可能性のあるメソッドの戻り値の型として使用することを意図しています。 型がOptionalの変数は、それ自体決してnullであってはなりません。常にOptionalインスタンスを指す必要があります。

APIノートに、とても大事なことが記述されています。
Optionalを使うときは、このルールを守ることが必要です。

  1. メソッドの戻り値の型として使用する。
  2. Optional型自身の変数は、決して null にしてはならない。

つまり、戻り値の型をOptionalにした場合、戻り値自体は決して null を返してはいけません。

// ダメな例です。
public Optional<String> getValue() {
    if (返す値がない場合) {
        return null; // ★ null を返してはいけません。
    }
    ...

null ではなく、値を持たない Optional.empty() を返します。

// OKな例です。
public Optional<String> getValue() {
    if (返す値がない場合) {
        return Optional.empty(); // ★ OK!
    }
    ...

このルールのおかげで、使う側は Optional自体の null チェックをしないですみます。

// このような null チェックは不要です。
final var optional = getValue();
if (optional != null) {
    optional.ifPresent(value -> {
        System.out.println(value);
    });
}

// これで OK!
getValue().ifPresent(value -> {
    System.out.println(value);
});

使う側の書き方

使う側とは、メソッドの戻り値として Optional を受け取る側のことです。

よく使うメソッドとして、

  • ifPresent
  • ifPresentOrElse​
  • orGet

の3つがあります。
あと、あまり使わないほうがよい orElseThrow についてもご紹介します。

ifPresent

public void ifPresent​(Consumer<? super T> action)
値が存在する場合は、その値で指定されたアクションを実行し、そうでない場合は何もしません。

値が存在するときだけ action を実行します。

action に渡されるパラメータ (下記の例ではラムダ式に渡される value パラメータ) は、必ず null 以外になります。
つまり value の null チェックは不要です。

【Optionalを使う例です】

public void main() {
    final var map = Map.of(
            "key-a", "value-a",
            "key-b", "value-b");

    getValue(map, "key-a").ifPresent(value -> {
        System.out.println("key-a : " + value);
    });

    getValue(map, "key-x").ifPresent(value -> {
        System.out.println("key-x : " + value);
    });

    // 結果
    // ↓
    //key-a : value-a
}

// Map.get の結果を Optional に変換するためだけのメソッドです。
public Optional<String> getValue(Map<String, String> map, String key) {
    return Optional.ofNullable(map.get(key));
}

【Optionalを使わない例です】

final var map = Map.of(
        "key-a", "value-a",
        "key-b", "value-b");

final var a = map.get("key-a");
if (a != null) {
    System.out.println("key-a : " + a);
}

final var x = map.get("key-x");
if (x != null) {
    System.out.println("key-x : " + x);
}

// 結果
// ↓
//key-a : value-a

ifPresentOrElse​

public void ifPresentOrElse​(Consumer<? super T> action, Runnable emptyAction)
値が存在する場合は、指定されたアクションを値とともに実行し、そうでない場合は空のベースのアクションを実行します。

値が存在するときは action を実行します。
値が存在しないときは emptyAction を実行します。

【Optionalを使う例です】

public void main() {
    final var map = Map.of(
            "key-a", "value-a",
            "key-b", "value-b");

    getValue(map, "key-a").ifPresentOrElse(value -> {
        System.out.println("key-a : " + value);
    }, () -> {
        System.out.println("key-a : 見つかりませんでした");
    });

    getValue(map, "key-x").ifPresentOrElse(value -> {
        System.out.println("key-x : " + value);
    }, () -> {
        System.out.println("key-x : 見つかりませんでした");
    });

    // 結果
    // ↓
    //key-a : value-a
    //key-x : 見つかりませんでした
}

// Map.get の結果を Optional に変換するためだけのメソッドです。
public Optional<String> getValue(Map<String, String> map, String key) {
    return Optional.ofNullable(map.get(key));
}

【Optionalを使わない例です】

final var map = Map.of(
        "key-a", "value-a",
        "key-b", "value-b");

final var a = map.get("key-a");
if (a != null) {
    System.out.println("key-a : " + a);
} else {
    System.out.println("key-a : 見つかりませんでした");
}

final var x = map.get("key-x");
if (x != null) {
    System.out.println("key-x : " + x);
} else {
    System.out.println("key-x : 見つかりませんでした");
}

// 結果
// ↓
//key-a : value-a
//key-x : 見つかりませんでした

orElse

public T orElse​(T other)
値が存在する場合は値を返し、そうでない場合はotherを返します。

値が存在するときは、その値を返します。
値が存在しないときは、指定した other をそのまま返します。

【Optionalを使う例です】

public void main() {
    final var map = Map.of(
            "key-a", "value-a",
            "key-b", "value-b");

    final var a = getValue(map, "key-a").orElse("default");
    System.out.println("key-a : " + a);

    final var x = getValue(map, "key-x").orElse("default");
    System.out.println("key-x : " + x);

    // 結果
    // ↓
    //key-a : value-a
    //key-x : default
}

// Map.get の結果を Optional に変換するためだけのメソッドです。
public Optional<String> getValue(Map<String, String> map, String key) {
    return Optional.ofNullable(map.get(key));
}

【Optionalを使わない例です】

final var map = Map.of(
        "key-a", "value-a",
        "key-b", "value-b");

var a = map.get("key-a");
if (a == null) {
    a = "default";
}
System.out.println("key-a : " + a);

var x = map.get("key-x");
if (x == null) {
    x = "default";
}
System.out.println("key-x : " + x);

// 結果
// ↓
//key-a : value-a
//key-x : default

orElseThrow (もしくはget) は、なるべく使わないように

public T orElseThrow()
値がある場合は値を返し、そうでない場合はNoSuchElementExceptionをスローします。

orElseThrow は、Optionalから値を取得します。

もし空の場合は、非チェック例外の NoSuchElementException が発生します。
そのため、値を取得する前には、必ず isPresentisEmpty で値が存在するかチェックしましょう。

public void main() {
    final var map = Map.of(
            "key-a", "value-a",
            "key-b", "value-b");

    final var optionalA = getValue(map, "key-a");
    if (optionalA.isPresent()) {
        System.out.println("key-a : " + optionalA.orElseThrow());
    }

    final var optionalX = getValue(map, "key-x");
    if (optionalX.isPresent()) {
        System.out.println("key-x : " + optionalX.orElseThrow());
    }

    // 結果
    // ↓
    //key-a : value-a
}

// Map.get の結果を Optional に変換するためだけのメソッドです。
public Optional<String> getValue(Map<String, String> map, String key) {
    return Optional.ofNullable(map.get(key));
}

get はメソッド名が違うだけで orElseThrow と同じ挙動となります。
API仕様では、名前の分かりやすさから、get より orElseThrow を使うことをおすすめされています。

そんな orElseThrow ですが、なるべく使わないようにすることをおすすめします。

【理由】

orElseThrow を使う前には、必ず isPresentisEmpty で値をチェックする必要があります。
つまり、チェックを忘れると例外が発生します。
従来の null チェックを忘れて例外 … というリスクとあまり変わっていません。

ifPresentifPresentOrElse を使えば、nullチェックのし忘れがなくなります。

使いたくなる場面 (チェック例外)

とはいえ、orElseThrow を使いたくなる場面もあります。
IOException のようなチェック例外を扱うときは、orElseThrow で値を取得することになります。

例として、ファイルパスが空でないときにファイルから文字列を読み込む、というのを考えてみましょう。

public void main() throws IOException {

    getPath().ifPresent(path -> {
        final var str = readString(path); // ★コンパイルエラー
        System.out.println("ファイル内容 : " + str);
    });
}

public Optional<Path> getPath() {
    ... 省略 ...
}

public String readString(Path path) throws IOException {
    return Files.readString(path);
}

もし、ファイルの読み込みで IOException が発生した場合に、main メソッドの呼び出し元にも IOException を throw したいときに困ったことになります。

上記の例では、ラムダ式の IOException が発生する箇所でコンパイルエラーとなります。
ラムダ式の部分は、正確には関数型インタフェースの Consumer です。
そして、Consumer.accept メソッドには、throws IOException 宣言がありません。

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
...

よって、コンパイルエラーとなります。

このような場合は、orElseThrow で値を取り出してから処理する必要があります。
取り出す前に、必ず isPresentisEmpty で値が存在するかチェックしましょう。

public void main() throws IOException {

    final var path = getPath();
    if (path.isPresent()) {
        final var str = readString(path.orElseThrow());
        System.out.println("ファイル内容 : " + str);
    }
}

public Optional<Path> getPath() {
    ... 省略 ...
}

public String readString(Path path) throws IOException {
    return Files.readString(path);
}

閑話

  • Optional … というよりかは、標準APIで用意されている関数型インタフェースは、チェック例外と相性が悪いです。
    いろいろ小細工すると回避できたりはするようですが、いずれは Java言語仕様としてうまく解決して欲しいなぁ、と淡い期待をしておきます。

返す側の書き方

メソッドの戻り値として Optional を返す側について見ていきましょう。

Optionalには3つの生成方法が用意されています。

  • empty
  • of
  • ofNullable

empty

public static Optional empty()
空のOptionalインスタンスを返します。 このOptionalには値はありません。

empty は空の Optional を生成します。

public void main() {

    getValue().ifPresentOrElse(value -> {
        System.out.println("値 : " + value);
    }, () -> {
        System.out.println("値は空です。");
    });

    // 結果
    // ↓
    //値は空です。
}

public Optional<String> getValue() {
    return Optional.empty();
}

of

public static Optional of​(T value)
指定された非null値を記述するOptionalを返します。

指定した値を持つ Optional を返します。
ofNullable と違い、value が null の場合は NullPointerException が発生します。

もし、事前に value が null ではないことが分かっている場合は、ofNullable ではなく of を使うことをおすすめします。

public void main() {

    getValue().ifPresentOrElse(value -> {
        System.out.println("値 : " + value);
    }, () -> {
        System.out.println("値は空です。");
    });

    // 結果
    // ↓
    //値 : abcd
}

public Optional<String> getValue() {
    return Optional.of("abcd");
}

ofNullable​

public static Optional ofNullable​(T value)
非nullならば、指定された値を記述するOptionalを返し、それ以外の場合は空のOptionalを返します。

指定した値が null であれば空の Optional を返し、nullでなければ値を持つ Optional を返します。

public void main() {
    final var map = Map.of(
            "key-a", "value-a",
            "key-b", "value-b");

    getValue(map, "key-a").ifPresentOrElse(value -> {
        System.out.println("key-a : " + value);
    }, () -> {
        System.out.println("key-a : 見つかりませんでした");
    });

    getValue(map, "key-x").ifPresentOrElse(value -> {
        System.out.println("key-x : " + value);
    }, () -> {
        System.out.println("key-x : 見つかりませんでした");
    });

    // 結果
    // ↓
    //key-a : value-a
    //key-x : 見つかりませんでした
}

public Optional<String> getValue(Map<String, String> map, String key) {
    return Optional.ofNullable(map.get(key));
}

標準APIで使われている例

標準APIでは、主にStreamで使われています。
例として findFirst メソッドを見てみましょう。

Stream.findFirst メソッドは、ストリームの最初の要素を返します。
しかし、ストリームが空の場合は返す要素がないため、そんなときのために Optional を使っています。

final var stream = Stream.of("abcd", "xyz");

stream.findFirst().ifPresent(s -> {
    System.out.println("ストリームの最初の要素 : " + s);
});

// 結果
// ↓
//ストリームの最初の要素 : abcd
final var stream = Stream.empty();

stream.findFirst().ifPresentOrElse(s -> {
    System.out.println("ストリームの最初の要素 : " + s);
}, () -> {
    System.out.println("ストリームは空です。");
});

// 結果
// ↓
//ストリームは空です。

プリミティブ型用のOptional

Optional<Integer> ではなく、プリミティブ型の int としてOptionalを使いたい。
そんなときのために、標準APIには、いくつかプリミティブ型に対応した Optional が個別に用意されています。

戻り値以外で使うのは?

例えば、メソッドのパラメータに Optional を使うのはどうでしょうか?
結論としては、あまりおすすめはしません。

理由としては、Optionalは戻り値の型として使うように意図されていると、公式のAPI仕様で明記されているためです。
あとは、戻り値以外では、やはりいろいろと使いづらいです。

// おすすめしない例です。
public void func(Optional<String> param) {
    if (param == null) {
        throw new IllegalArgumentException();
    }

    param.ifPresent(str -> {
        System.out.println(str);
    });
}

契約プログラミングでは、通常、メソッドの先頭でパラメータをチェックします。
Optional パラメータの null チェックをして、さらに ifPresent で空かどうかチェックする…なんだか二度手間に感じないでしょうか。

外部のサイトで英語ですが、同じ内容が議論されているのでそちらも参考にしてみてください。

Optinalの隠れた?メリット

メソッドの戻り値の型が Optional であれば、

  • そのメソッドは を返す可能性がある

ということが、ドキュメントを書かなくても明確になります。
個人的に、これは Optional を使う大きなメリットだと考えています。

もし、多人数のプロジェクトでコーディング規約を決めているのであれば、

  • null を返すことがあるメソッドは、必ず Optional を使う
  • 逆に言うと、Optionalではないメソッドは null を返してはいけない

というルールを設けてみるのもよいかもしれません。

これで、Optional を返さないメソッドは、戻り値の null チェック不要という共通ルールができあがります。
多人数で開発している場合、このようなルールがあるほうがコーディングしやすくなるのではないかな、と思います。

残念ながら、標準APIではこのルールは適用されていません。
Optional は Java 8 から追加されたAPIなので、それ以前から存在する API はこのルールが適用されていないのでご注意ください。

Optionalの使いすぎには注意

便利だからと、わりと気軽に Optional を返していませんでしょうか。

Optionalは、使う側で、値を持つ/持たないの条件分岐を要求します。
つまり、コードが複雑になります。

Optionalを使わないで済む(メソッドがnullを返さない)のであれば、それにこしたことはありません。
コードもシンプルになります。

もし、エラーを返す用途で Optional を使っている場合は、代わりにチェック例外を使うことも検討してみましょう。
例外については、下記の記事も参考にしていただけたら幸いです。

まとめ

本記事では Optional の基本的な使い方をご紹介しました。

  • Optionalは戻り値の型として使いましょう。
  • 戻り値としてOptional型を返す場合、返す値自体は null を返してはいけません。
  • orElseThrowget は使わずに、代わりに ifPresentifPresentOrElse などを使いましょう。

Optionalを使うことで、使う側の null チェックがよりスマートになります。
有効に活用していきたいですね。


関連記事

ページの先頭へ