Java : Optionalの基本
従来(Java 7まで)、メソッドの戻り値が null となる可能性がある場合、使う側は if 文を使った null チェックが必要でした。
Optional クラスを使うと、この null チェックをよりスマートに記述できます。
ただし、Optional を使う上で注意しなければならないこともあります。
そのあたりも踏まえて、本記事では Optional の基本的な使い方をご紹介します。
また、ラムダ式を多用するため、もしラムダ式の理解に不安があるかたは、下記の記事も合わせてご参照ください。
- 「ラムダ式の基本」
概要
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 を使うときは、このルールを守ることが必要です。
- メソッドの戻り値の型として使用する。
- 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
値が存在するときだけ 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
値が存在するときは 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
値が存在するときは、その値を返します。
値が存在しないときは、指定した 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)
orElseThrow は、Optional から値を取得します。
もし空の場合は、非チェック例外 の NoSuchElementException が発生します。
そのため、値を取得する前には、必ず isPresent か isEmpty メソッドで値が存在するかチェックしましょう。
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 と同じ挙動になります。
公式ドキュメントでは、名前の分かりやすさから、get の代わりに orElseThrow を使うように推奨されています。
そんな orElseThrow ですが、もし可能な状況であれば、代わりに ifPresent や ifPresentOrElse メソッドを使うことを検討してみましょう。
【理由】
orElseThrow を使う前には、必ず isPresent か isEmpty メソッドで値をチェックする必要があります。
つまり、チェックを忘れると例外が発生します。
従来の null チェックを忘れて例外 … というリスクとあまり変わっていません。
ifPresent や ifPresentOrElse メソッドを使えば、nullチェックのし忘れがなくなります。
(とはいえ、orElseThrow が必要となる状況もあるとは思います)
返す側の書き方
メソッドの戻り値として Optional を返す側について見ていきましょう。
Optional には3つのオブジェクト生成方法が用意されています。
- empty
- of
- ofNullable
empty
empty は空の Optional を生成します。
public void main() {
getValue().ifPresentOrElse(value -> {
System.out.println("値 : " + value);
}, () -> {
System.out.println("値は空です。");
});
// 結果
// ↓
//値は空です。
}
public Optional<String> getValue() {
return Optional.empty();
}
of
指定した値を持つ 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
指定した値が 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で使われている例
Optional は、標準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クラス |
---|---|
int | OptionalInt |
long | OptionalLong |
double | OptionalDouble |
戻り値以外で使うのは?
例えば、メソッドのパラメータに 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 チェック不要という共通ルールができあがります。
多人数で開発している場合、このようなルールがあるほうがコーディングしやすくなるのではないかな、と思います。
残念ながら、Java 標準API ではこのルールは適用されていません。
Optional は Java 8 から追加された API なので、それ以前から存在する API はこのルールが適用されていないのでご注意ください。
Optionalの使いすぎには注意
便利だからと、わりと気軽に Optional を返していませんでしょうか。
Optional は、使う側で、値を持つ/持たないの条件分岐を要求します。
つまり、コードが複雑になります。
Optional を使わないで済む(メソッドが null を返さない) のであれば、それにこしたことはありません。
コードもシンプルになります。
もし、エラーを返す用途で Optional を使っている場合は、代わりにチェック例外を使うことも検討してみましょう。
例外については、下記の記事にもまとめているので参考にしていただけたら幸いです。
まとめ
本記事では Optional の基本的な使い方をご紹介しました。
- Optional は戻り値の型として使いましょう。
- 戻り値として Optional 型を返す場合、返す値自体は null を返してはいけません。
- orElseThrow や get は使わずに、代わりに ifPresent や ifPresentOrElse などを使いましょう。
Optional を使うことで、使う側の null チェックがよりスマートになります。
有効に活用していきたいですね。