Skip to main content

Javaで数値のリストを合計する

Java engineering feature

2020年5月22日

0 分で読めます

Javaでちょっとした処理をするとき、最適な方法は何だろうと考えることがあります。数日前にもそんなことがありました。数値のリストを合計するだけの簡単な処理が必要だったのですが、合計(sum)する方法はいくつもあることがわかりました。

昔ながらの方法

単純なループを作って処理できます。ここではJava 11を使っています。たとえばJava 8をお使いの場合、List.ofやvarが使えないことはご了承ください。それでも、言いたいことは伝わると思います。

var listOfNumbers = List.of(1,2,3,4,5,6,7,8,9,10);

var sum = 0;
for (int i = 0; i < listOfNumbers.size() ; i++) {
    sum += listOfNumbers.get(i);
}

もちろん、Java 5以降は拡張forループが使えるので、同じコードを次のように書き換えられます。

var listOfNumbers = List.of(1,2,3,4,5,6,7,8,9,10);

var sum = 0;
for (int number : listOfNumbers) {
    sum += number;
}

違いはわずかですが、より意図が明確になっています。「listOfNumbersから取り出した各numberについて、次の処理を行う……」という意味になります。

Java Streamを使う方法

私をよく知る人なら、大学時代にHaskellでのプログラミングを叩き込まれたことをご存じでしょう。つまり、純粋関数型プログラミングが大好きなのです。Javaで同じことができるわけではありませんが、Stream APIを使えば、関数型プログラミングの表現力をある程度利用できます。

JavaのStream APIでは、MapReduceプログラミングモデルを実行できます。今回解決したい問題では、数値自体は変わらないのでmapは不要です。一方、Listを合計という1つの数値にreduceする必要があります。

collect

おそらく99%のケースでは、標準のtoList()コレクターとcollect関数を使って、Streamを再びListにまとめます。たとえば次のようになります。

       var time2ToList = listOfNumbers.stream()
                .map(i -> i * 2)
                .collect(Collectors.toList());

Streamを再びListにまとめる以外にも、できることはあります。Collectorsライブラリを見ると、summingInt()、summingDouble()、summingLong()などの関数があります。これらを使って、Listを合計値にまとめる(reduceする)ことができます。

summingInt関数には、入力値をintに変換する関数が必要です。この場合は、単純に「恒等関数」を使えます。i -> iで十分です。

        var listOfNumbers = List.of(1,2,3,4,5,6,7,8,9,10);
        var sum = listOfNumbers.stream()
                    .collect(Collectors.summingInt(i -> i));

この恒等関数は少し不格好に見えるかもしれません。その場合は、代わりにInteger.intValue()を使えます。

       var listOfNumbers = List.of(1,2,3,4,5,6,7,8,9,10);
       var sum = listOfNumbers.stream()
                .collect(Collectors.summingInt(Integer::intValue));

このように書くと、私のIDE(ここではIntelliJ IDEA)が、次のようにリファクタリングしてmapToInt()関数を使うことを提案してきます。

       var listOfNumbers = List.of(1,2,3,4,5,6,7,8,9,10);
       var sum = listOfNumbers.stream()
                .mapToInt(Integer::intValue).sum();

技術的には、各要素をすでにその型であるintにマッピングし ¯\(ツ)/¯、sum()関数でreduceしています。

推論される型を見ると、よりわかりやすくなります。プリミティブ型のListは作れません。そのため、Listの要素はInteger(オブジェクト)です。つまり、sum()を使うには、リスト内の各要素をプリミティブ型のintに戻す必要があります。コレクターで恒等関数を使った先ほどの例が動作するのは、Javaのアンボクシングのおかげです。

Javaでプリミティブ型のListを使いたい場合は、Eclipse Collectionsライブラリを確認してみてください。

reduce

Javaでは、Stream APIのいくつかの関数を使ってリダクションを行います。collect()に加えて、名前のとおりreduce()という関数もあります。

       var listOfNumbers = List.of(1,2,3,4,5,6,7,8,9,10);
       var sum = listOfNumbers.stream()
                .reduce(0 , (num1, num2) -> num1 + num2);

この場合、reduce関数は開始値とBiFunctionのラムダ式を受け取ります。BiFunctionは開始値と最初の数値に適用され、その関数の結果が次の数値に適用され、これが繰り返されます。

上記のコードは、次のような処理を行います。0 + 1、1 + 2、3 + 3、6 + 4……

開始値の0は省略することもできます。ただし、この場合、reduce関数はOptionalを返します。reduce対象のListが空の可能性があるためです。

まとめ

ご覧のとおり、この問題には複数の解決方法があります。間違いなく、さらに風変わりな方法を思いつく人もいるでしょう。個人的にはreduce()を使う方法が気に入っています。Javaで最も意図が伝わりやすく、純粋な解決策だと思うからです。リストを1つの数値にreduceしたいだけなので、ラッパー型からプリミティブ型への変換を気にする必要がありません。また、別の型のListをreduceする必要があるときも、目的に合ったリダクション用のラムダ関数を書けば、この方法を再利用できます。

カテゴリー: