monolithic kernel

Java 8の新機能をエレガントに使いたい

Java 8ではラムダ式、Stream、Optional、インタフェースのデフォルト実装などなど、大量の新機能が追加されました。新機能を知らないという方は以下が参考になるかと思います。

こうした情報を見て、少なくとも私はJavaの時代が来たんじゃないかという印象を持ちました。もちろん、私の大好きなC#や人気のScalaと比べてしまうと、言語仕様の強力さ、記述の美しさでは劣ります。しかし、少なくとも現状ではWindows以外で使うならC#よりJavaですし、Scalaは一部の人が盛り上がっているという印象で、言語仕様以外の部分ではJavaに分があると思うのです。言語仕様の差が以前より縮まった現在、総合的に見ればJavaは選択肢に入るどころか、最強と言ってもいいのではないでしょうか。

とはいえ、使わずにどうこう言っても仕方ないので、実際に新機能でコードを書いて試してみました。よい点を紹介している記事はたくさん見かけるので、ここではその辺はさらっと一言程度に留めて、StreamとOptionalについての気になった点を中心に述べることにします。

Stream

Stream APIによって、コレクション操作の充実度は他の言語に劣らない程度にまで強化されました。コレクションに直接メソッドが追加されているわけではないため、一度streamを取得してから操作しなければならないというのは若干面倒ではありますが、第一印象は悪くありません。

ただ、実際に書いてみるとプリミティブ型がGenericsからハブられている関係で、まぁ使いにくいわけです。そのためにIntStreamとかDoubleStreamとかが用意されているのですが、これらの型は参照型全般で使われるStreamとは互換性がないため、開発者が意識して適宜変換しながら使い分ける必要があります。

StreamIntegerDoubleを入れてしまえばいいのでは、と思われるかもしれませんが、sum()average()などによる集計の機能はStreamには用意されていないので、mapToInt()mapToDouble()で一度型を変換してから集計するか、あるいは自分でreduce()で書くかということになります。どうでもいい車輪の再発明は避けるべきと考えると、mapToInt()なるダサいメソッドを呼び出すのが現実的でしょう。

List<String> strings = ...;
strings.stream().mapToInt(String::length).sum(); // 文字列の長さの総和

集計のことを考えると、プリミティブ型は基本的に専用のstreamにするのがよいと思いますが、プリミティブ型のstreamのmap()は同じ型のStreamしか返せません。他のプリミティブ型や参照型を返したい場合には、doubles().map()longs().map()mapToObj()を利用します。

IntStream.rangeClosed(1, 10).map(i -> i / 2).sum(); // 1から10までの数値を2で割ったものの総和 (小数点以下切り捨て)
IntStream.rangeClosed(1, 10).doubles().map(i -> i / 2).sum(); // 1から10までの数値を2で割ったものの総和 (小数点以下も保持)
IntStream.rangeClosed(1, 10).mapToObj(String::valueOf); // 1から10までの数値を文字列に変換したもの

map()には対応するmapToObj()があるのに対して、flatMap()に対応するflatMapToObj()は存在しません。プリミティブ型のstreamでflatMap()して参照型を返したい場合には、boxed()を呼び出してボクシングした型のStreamを得てからflatMap()を使う必要があります。

// IntStream.range(0, 10).flatMapToObj(i -> ...); // flatMapToObjは実際には存在しない
IntStream.range(0, 10).boxed().flatMap(i -> ...); // ボクシングしたのちにflatMap

Optional

Javaではnullが入り込む予知があることを見落としがちになるため、nullに対する適切な処理を強制する効果を見込めるOptionalは非常に有用だと思います。強いて難点を言うなら、Optionalに値が入っているかどうかで分岐して処理を行う場合に、あまりカッコよく書けないのが気になるところです。

といっても、以下のように値が入っている場合にだけ何らかの処理を行う、というのは問題ありません。

// Before
Object obj = /* ... */;
if (obj != null) {
    // ...
}
// After
Optional<Object> opt = /* ... */;
opt.ifPresent(obj -> {
    // ...
});

しかし、値が入っていない場合にも何らかの処理を行いたいとなると、Optionalを使わない場合と比べて一手間増えてしまいます。isPresent()get()が入ってしまうOptionalの使い方は避けたいところですね。

// Before
Object obj = /* ... */;
if (obj != null) {
    // ...
}
else {
    // ...
}
// After
Optional<Object> opt = /* ... */;
if (opt.isPresent()) {
    Object obj = opt.get();
    // ...
}
else {
    // ...
}

map()メソッドとorElseGet()メソッドで書けないこともないですが、nullを返しておいて捨てるというのはどう考えてもスマートではありません。nullを置き換えるためにOptionalを導入しているはずなのに、コード上にnullが現れるという皮肉さがあるのは面白いですが。今のところはisPresent()get()を使うのが現実的でしょう。

Optional<Object> opt = /* ... */;
opt.map(obj -> {
    // ...
    return null;
}).orElseGet(() -> {
    // ...
    return null;
});

おわりに

これまでと比べれば遥かによくなっていると思いますが、どことなく残念な感があるのは否めません。また、C#の匿名型に相当するものもTupleもないので、メソッドチェーンでカッコいいコードを書くのはなかなか難しい気がします。そもそも、業務で書くなら記述のエレガントさよりコードを見て誰でも理解できることのほうが重要ですから、エンタープライズ向けで強いJavaに求めるべき方向性ではないのかもしれません。ただ、少なくとも趣味のプログラミングにおいてのJava熱から目が覚めたのは確かです。