背景
「Optional型を使ってnullの場合分けをif文を使わないで書く」を見て考えたことをつらつら。
やること
こんな感じのインタフェースがあって
public interface Foo {
public abstract String foo();
}
public interface Bar {
public abstract String bar();
}
public interface Buzz {
public abstract String buzz();
}
それぞれ
Optional<Foo> foo = FooImpl.getInstance();
Optional<Bar> bar = BarImpl.getInstance();
Optional<Buzz> buzz = BuzzImpl.getInstance();
などで取れるとします。
foo が空なら bar、bar も空なら buzz、というふうに空でないオブジェクトを順に探して、最初の空でないオブジェクトのメソッドを呼んで値を取ります。
全て空の場合は適当なデフォルト値を設定します。
if-else / 三項演算子
String value;
if(foo.isPresent()){
value = foo.foo();
} else if(bar.isPresent()){
value = bar.bar();
} else if(buzz.isPresent()){
value = buzz.buzz();
} else {
value = "empty";
}
昔からの書き方です。
三項演算子でもほとんど同様に書けます。
String value =
foo.isPresent() ? foo.foo() :
bar.isPresent() ? bar.bar() :
buzz.isPresent() ? buzz.buzz() : "empty";
どちらを選ぶかは好き好きですかね。
Optional#orElse() / orElseGet()
orElse() で素直に書くとこういう感じになります。
String value =
foo.map(Foo::foo).orElse(
bar.map(Bar::bar).orElse(
buzz.map(Buzz::buzz).orElse("empty")));
これはこれで読めないわけではありませんが、チェック対象が増えるとネストが深くなっていくため、どうインデントするかというのが悩みどころです。
String value = foo.map(Foo::foo).orElse(
bar.map(Bar::bar).orElse(
buzz.map(Buzz::buzz).orElse("empty")));
もしくは
String value = foo.map(Foo::foo)
.orElse(bar.map(Bar::bar)
.orElse(buzz.map(Buzz::buzz)
.orElse("empty")));
のような整形の方がよいでしょうか。
orElseGet() の場合も同様に
String value = foo.map(Foo::foo).orElseGet(() ->
bar.map(Bar::bar).orElseGet(() ->
buzz.map(Buzz::buzz).orElseGet(() ->
"empty")));
もしくは
String value = foo.map(Foo::foo)
.orElseGet(() -> bar.map(Bar::bar)
.orElseGet(() -> buzz.map(Buzz::buzz)
.orElseGet(() -> "empty")));
などとすれば読めなくはないかな……という感じです。
こういう書き方がイディオムとして普及すれば、これでよいような気がします。
ただやはり、糖衣構文がないと煩雑な感じは否めないですね。
宣言的に処理する
発想を変えて、宣言的に扱えないか考えてみます。
わかりやすくするため、まず単一の型で考えてみましょう。
Optional<Foo> foo1 = FooImpl.getInstance();
Optional<Foo> foo2 = FooImpl.getInstance();
Optional<Foo> foo3 = FooImpl.getInstance();
String value = Stream.of(foo1, foo2, foo3)
.filter(Optional::isPresent)
// 1行で書くなら .map(o -> o.get().foo())
.map(Optional::get)
.map(Foo::foo)
.findFirst()
.orElse("empty");
これは簡単ですね。
もし元のインタフェース (Foo, Bar, Buzz) を改修することが可能で、これらに共通のスーパーインタフェースを付けることが悪くないと思われる場合は、そのような修正も一案だと思います。
findFirst() は短絡操作ですので、マッピングはフィルタでマッチした最初のオブジェクトに対してのみ行われ、不要な値の取得が行われることもありません。
異なる型をそのまま扱う場合には、共通の型にマッピングしてからストリームを作る必要があるでしょうか。
String value = Stream.of(foo.map(Foo::foo), bar.map(Bar::bar), buzz.map(Buzz::buzz))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElse("empty");
この方法の難点は、やはりマッピングの時点で末端のメソッド呼び出しが行われてしまう点です。
Bar#bar() や Buzz#buzz() がコストの掛かる処理の場合には避ける必要があります。
マッピングを遅延させるためには何らかのラッピングを行う必要がありますが……。
// ラムダで渡すには型を明示する必要がある
String value = Stream.<Supplier<Optional<String>>>of(
() -> foo.map(Foo::foo),
() -> bar.map(Bar::bar),
() -> buzz.map(Buzz::buzz))
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElseGet(() -> "empty");
微妙なところですね。
一般化して
@SafeVarargs
public static <R> R findFirstOrElseGet(
Supplier<R> defaultSupplier,
Supplier<Optional<R>>... suppliers) {
return Stream.of(suppliers)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElseGet(defaultSupplier);
}
こういうメソッドを用意すれば
String value = findFirstOrElseGet(
() -> "empty",
() -> foo.map(Foo::foo),
() -> bar.map(Bar::bar),
() -> buzz.map(Buzz::buzz));
これで済みますが、いずれにせよやりたいことの割には大げさかも知れません。
Optional#map() の注意点
なお、これらの Optional#map() を使った処理は if-else や三項演算子のバージョンと異なり、Foo#foo() などが null を返す場合にも orElse() 以下で指定された値が返ります。
つまり、if でいうと
String value = null;
if(foo.isPresent()){
value = foo.foo();
}
if(value == null && bar.isPresent()){
value = bar.bar();
}
if(value == null && buzz.isPresent()){
value = buzz.buzz();
}
if(value == null) {
value = "empty";
}
に近い処理になるので、その点は注意して下さい。
個人的なまとめ
- if-else / 三項演算子は低レベルですが現状ではノウハウが浸透しており、チーム内でのすり寄せもほぼ必要ないでしょう。
- Optional#orElse() / orElseGet() のネストは可読性に関して if-else / 三項演算子にやや劣りますが、終端まで null を気にせず処理を行うことができます。
- Stream での宣言的な処理は、型の変換にコストが掛からない場合には強力です。
ケースバイケースで間違いのなさそうな実装を選びたいところです。