はじめに
ぬるぽの危険を排除し、Nullチェックが乱立するコードを排除してくれるOptionalですが、使い方を誤ると可読性を低下させるだけになります。
Optionalを使いこなして洗練されたコードを書きましょう。
参考:
Java 8 "Optional" ~ これからのnullとの付き合い方 ~
JavaのOptionalについて考える
Optionalとは
箱です。
箱の中には値があるかもしれないし、ないかもしれません。
何のための存在か
NPEの回避
NullPointerExceptionを回避するための存在です。
Nullに直接触ることで、NullPointerExceptionが発生します。
そこで、Nullかもしれない値を箱に入れることで、NullPointerExceptionを回避しようという考えです。
String name = null;
// ~~~
// 長い処理、どこかでnameに値が入る
// ~~~
// ・・・と思っていたのか?
System.out.println(name.length()); // NPE!!!
特に、メソッドの戻り値としてNullを返さないために設計されています。
(戻り値としてNullを返すと、呼び出し元でNPEが発生するため、安全な箱に入れて返す)
Nullチェックからの解放
NullPointerExceptionを発生させないためには、Nullチェックが必要です。
漏れないように実装しましょう。
public String getCity(User user) {
if(user == null) {
return "不明";
}
Profile profile = user.getProfile();
if(profile == null) {
return "不明";
}
Address address = profile.getAddress();
if(address == null) {
return "不明";
}
City city = address.getCity();
if(city == null) {
return "不明";
}
return city.getName();
}
うーん長くて読みにくいですね。Nullチェックが邪魔で何をやっているのかパッとわかりません。
Optionalを使うと以下のように書けます。鮮やかです。やりたいことだけが書いてあります。
public String getCity(User user) {
return Optional.ofNullable(user)
.map(User::getProfile)
.map(Profile::getAddress)
.map(Address::getCity)
.map(City::getName)
.orElse("不明");
Nullを排除するための存在ではない
NPEを回避できるとか、Nullチェックをしなくていいとか、つまりOptionalを使ってNullを完全に排除するべきなんだ!・・・というわけではありません。
OptionalはNullを完全に排除するためのオブジェクトではありません。
メソッドの外にNullを露出させないため、くらいの位置付けです。
ですから、ドメイン設計を放棄して「Nullかもしれないドメインオブジェクト」「Nullかもしれない値」を作らないでください。
Optionalの使い方
基本的なところだけを書いています。
作り方
まずはOptionalを作ってみましょう。
以下の3パターンがあります。
Optional<String> name = Optional.of("Alice"); // 値を箱に入れる
Optional<String> name = Optional.ofNullable(possibleNullValue); // Nullかもしれない値を箱に入れる
Optional<String> name = Optional.empty(); // からの箱を用意する
受け取った後の処理
Optionalを受け取った後、どのような操作をするのか。基本は以下の4つです。
// 箱の中の値が無ければ固定のデフォルト値、あれば値を取得する
String n = name.orElse("ゲスト");
// 箱の中の値が無ければ取得メソッドを実行、あれば値を取得する
String n = name.orElseGet(() -> loadFromDB());
// 箱の中の値が無ければ例外を投げる、あれば値を取得する
String n = name.orElseThrow(() -> new IllegalStateException());
// 箱の中の値があれば処理を実行する、無ければ何もしない
name.ifPresent(n -> System.out.println("Hello " + n));
アンチパターン
Optional#getを使う
Optional#getはせっかく箱に入れて安全にしたものを、取り出して危険に晒す行為です。
正直、非推奨メソッドだと考えてます。
Optionalを使い慣れていない方ほど使ってしまうメソッドだと思いますが、このメソッドは普通使わない、最終手段だと考えてください。
例えば、中身があるかどうかを確認せずにOptional#getを呼び出すと、NoSuchElementExceptionが発生します。
この例外は非検査例外のため、コンパイルエラーにならず、実装時に気づくことができないのも難点です。
NoSuchElementExceptionが発生しないように、値があるかを事前にチェックしましょうか。
Optional<String> nameOpt = getName();
if(nameOpt.isPresent()) {
String name = nameOpt.get();
// ...
}
これ、Nullチェックと全く同じですね。
これではOptionalを使っている意味がありません。
中身を箱から取り出した場合、NPEの危険と、Nullチェックの地獄が帰ってきます。
取り出すなら相応の覚悟が必要です。
上記のコードのように「値があれば処理をする」というのはOptional#ifPresentで書けます。
箱から取り出す必要はないのです。箱から取り出さないでください。
メソッドの戻り値以外にOptionalを使う
冒頭で触れた通り、Optionalはメソッドの戻り値でNullを返さないために設計されています。
そのため、メソッドの戻り値以外にOptionalを使うのはアンチパターンです。
フィールドでOptionalを持つ
以下のようにフィールドにOptionalを使うことはやめましょう。
public class User {
private Optional<String> name; // ダメ、ゼッタイ
public Optional<String> getName() {
return name;
}
}
通常、フィールドがNullになるかどうかはドメイン知識として決まっているはずです。「Nullかどうかわからない」のであれば、ドメイン設計を見直すべきです。
また、Optionalは単なる箱なので軽いですがオブジェクトであることに変わりありません。
一定のメモリを使いますので、このクラスのインスタンスが大量に作られる場合、OOMなどの問題に発展する可能性もあります。
メソッドの引数でoptionalを持つ
以下のようにメソッドの引数にOptionalを持つことはやめましょう。
public void updateName(Optional<String> name) { // ダメ、ゼッタイ
name.ifPresent(n -> this.name = n);
}
入力として、「値を持つかもしれないし持たないかもしれない」値を型で強制しています。
不自然ですよね。何のため?という疑問が浮かびます。
そうです。こんなことをする必要はないんです。やめましょう。
メソッド内の一時変数としてOptionalを作る
以下のように、一時変数としてOptionalを作ることはやめましょう。
Optional<User> u = Optional.ofNullable(findUser());
if (u.isPresent()) {
doSomething(u.get());
}
使っているメソッドがOptionalを返さないなら、そのままOptionalではない値として使いましょう。
User u = findUser();
if(u != null) {
doSomething(u);
}
普通にNullチェックした方が見やすいですよね
例外:メソッドの戻り値以外に使ってもいいケース
Streamの中で使う
OptionalはStreamAPIと相性がよく、組み合わせることで関数的な処理を書くことができます。
また、stream内のみで利用する場合は短命なのでヒープ領域を圧迫しにくく、パフォーマンス観点での影響は最小限です。
Optional型の処理が長い場合
受け取ったOptionalには次々に関数を呼び出して連鎖的なコーディングをしますが、場合によっては「長くて読みにくく」なります。
この場合は、区切りのいい場所で一旦Optional型の一時変数とすることがあります。
可読性を考慮してこのような選択は問題ないと言えます。
テストコード内で使う
テストコード内では「便利のため」「可読性(テスト意図を伝える)のため」、好きに使いましょう。
Optionalをコレクションに入れる
コレクションには元々、値があるかないかを取り扱うための機能が揃っています。
コレクションにOptionalをいれるのは、二重の箱に入れているようなもので、非常に取り扱いにくくなります。
List<Optional<String>> names = new ArrayList<>();
names.add(Optional.of("Alice"));
names.add(Optional.empty());
上記のようにしたとき、namesの中には名前が何個入っているでしょうか?
OptionalでなければList#sizeで瞬殺なこの質問も、「リストという箱を開けて、Optionalという箱を開けて、中身が入っている個数を数える」という面倒なことをしなくてはいけません。
コレクションには元々機能が揃っていますから、Optionalをわざわざ使う必要がないということです。
おわりに
Optionalを使ってうまーくコーディングできると、かっこいいコードになります。(※個人の意見です)
それこそ冒頭で書いた「洗練された」コードに見えます。(※個人の意見です)
Optionalの正しい使い方が結局わかんないなーと思う方は、まずはOptional#getは絶対使わない、Optional#getを使っているコードをレビューする場合必ず指摘する、ということだけ覚えておけば、それなりの正しさで使えるのではないか、と思います。