Javaで数値のリストを合計する
2020年5月22日
0 分で読めますJavaでちょっとした処理をするとき、最適な方法は何だろうと考えることがあります。数日前にもそんなことがありました。数値のリストを合計するだけの簡単な処理が必要だったのですが、合計(sum)する方法はいくつもあることがわかりました。
昔ながらの方法
単純なループを作って処理できます。ここではJava 11を使っています。たとえばJava 8をお使いの場合、List.ofやvarが使えないことはご了承ください。それでも、言いたいことは伝わると思います。
もちろん、Java 5以降は拡張forループが使えるので、同じコードを次のように書き換えられます。
違いはわずかですが、より意図が明確になっています。「listOfNumbersから取り出した各numberについて、次の処理を行う……」という意味になります。
Java Streamを使う方法
私をよく知る人なら、大学時代にHaskellでのプログラミングを叩き込まれたことをご存じでしょう。つまり、純粋関数型プログラミングが大好きなのです。Javaで同じことができるわけではありませんが、Stream APIを使えば、関数型プログラミングの表現力をある程度利用できます。
JavaのStream APIでは、MapReduceプログラミングモデルを実行できます。今回解決したい問題では、数値自体は変わらないのでmapは不要です。一方、Listを合計という1つの数値にreduceする必要があります。
collect
おそらく99%のケースでは、標準のtoList()コレクターとcollect関数を使って、Streamを再びListにまとめます。たとえば次のようになります。
Streamを再びListにまとめる以外にも、できることはあります。Collectorsライブラリを見ると、summingInt()、summingDouble()、summingLong()などの関数があります。これらを使って、Listを合計値にまとめる(reduceする)ことができます。
summingInt関数には、入力値をintに変換する関数が必要です。この場合は、単純に「恒等関数」を使えます。i -> iで十分です。
この恒等関数は少し不格好に見えるかもしれません。その場合は、代わりにInteger.intValue()を使えます。
このように書くと、私のIDE(ここではIntelliJ IDEA)が、次のようにリファクタリングしてmapToInt()関数を使うことを提案してきます。
技術的には、各要素をすでにその型であるintにマッピングし ¯\(ã)/¯、sum()関数でreduceしています。
推論される型を見ると、よりわかりやすくなります。プリミティブ型のListは作れません。そのため、Listの要素はInteger(オブジェクト)です。つまり、sum()を使うには、リスト内の各要素をプリミティブ型のintに戻す必要があります。コレクターで恒等関数を使った先ほどの例が動作するのは、Javaのアンボクシングのおかげです。
Javaでプリミティブ型のListを使いたい場合は、Eclipse Collectionsライブラリを確認してみてください。
reduce
Javaでは、Stream APIのいくつかの関数を使ってリダクションを行います。collect()に加えて、名前のとおりreduce()という関数もあります。
この場合、reduce関数は開始値とBiFunctionのラムダ式を受け取ります。BiFunctionは開始値と最初の数値に適用され、その関数の結果が次の数値に適用され、これが繰り返されます。
上記のコードは、次のような処理を行います。0 + 1、1 + 2、3 + 3、6 + 4……
開始値の0は省略することもできます。ただし、この場合、reduce関数はOptionalを返します。reduce対象のListが空の可能性があるためです。
まとめ
ご覧のとおり、この問題には複数の解決方法があります。間違いなく、さらに風変わりな方法を思いつく人もいるでしょう。個人的にはreduce()を使う方法が気に入っています。Javaで最も意図が伝わりやすく、純粋な解決策だと思うからです。リストを1つの数値にreduceしたいだけなので、ラッパー型からプリミティブ型への変換を気にする必要がありません。また、別の型のListをreduceする必要があるときも、目的に合ったリダクション用のラムダ関数を書けば、この方法を再利用できます。
