Java
java8
StreamAPI

インデックスを参照するループをJava8のStreamAPIで書きたい

More than 1 year has passed since last update.

みんな大好きStreamAPIですが、
拡張for文で書くようなループはキレイにかけますが、
ループ内部でインデックスを参照したいようなループはいまいちどうしたらいいのかわかりません。
(ぐぐっても独自クラス作っちゃうような方法がでてきたり:persevere:)

ということでちょっと頭をひねってみました。


「ループ内部でインデックスを参照したいようなループ」としては以下のようなケースを想定します。

  • 2つ以上のリストをまとめて回したい
  • 1つのリスト中の隣接する要素(iに対してi-1とかi+1とか)も参照したい

(iが参照できて、かつ好きな処理を書けるので、
「リストの最初/最後のときに処理を変えたい」とかのケースでも使えますが、
あんまりキレイじゃないかな、と思います。)

以下がコード例です。

    public static void main(String[] args) {
        // 乱数で作った適当なリスト
        List<Integer> listA = new ArrayList<>(Arrays.asList(12, 23, 66, 45, 10, 48, 21, 25, 68, 65));
        List<Integer> listB = new ArrayList<>(Arrays.asList(98, 96, 4, 33, 30, 38, 39, 25, 32, 64));

        // ケース1:2つのリストをまとめて扱いたい場合
        List<Boolean> isWinnerA = IntStream.range(0, Math.min(listA.size(), listB.size()))
                .mapToObj(i -> listA.get(i) > listB.get(i))
                .collect(Collectors.toList());
        System.out.println("isWinnerAList: " + isWinnerAList);
        // -> isWinnerA: [false, false, true, true, false, true, false, false, true, true]

        // ケース2:リストの中の前後をまとめて扱いたい場合
        List<Double> movingAverage = IntStream.range(0, listA.size() - 1)
                .mapToObj(i -> (listA.get(i) + listA.get(i + 1)) / 2d)
                .collect(Collectors.toList());
        System.out.println("movingAverage: " + movingAverage);
        // -> movingAverage: [17.5, 44.5, 55.5, 27.5, 29.0, 34.5, 23.0, 46.5, 66.5]
    }

構成としては、

  • for(int i; i < list.size(); i++)の部分
    • IntStreamのファクトリメソッドで作ります
    • 例のような単純な増加ならrange(startInclusive, endExclusive)でいけます
    • インクリメントの大きさを変えたいならiterate(seed, f).limit(maxSize)とか(参照する方でi*2してもいいですが)
    • generate(s)を使えば大抵のことはできるはず
  • ループ内の処理
    • mapToObj(/* 変換処理 */).collect(/* 集約処理 */);
    • 集約しなくて良いならforEach()で済みますが。

pro:標準で用意されてるものだけで書ける
con:Stream慣れてる人じゃないと困惑するかも

普通にfor文で書いた方がいいって言われますかね・・・?
まぁ、例に使ってる程度のものなら普通にfor文書いてもいいかもですが、filter()とか、集約処理とか、Streamの機能が活きるケースだと使えるかな?と思います。