LoginSignup
114
98

More than 5 years have passed since last update.

Javaのループ内でインデックス番号を取得する時の考察

Last updated at Posted at 2016-03-06

はじめに

Javaでループを書く場合は、拡張for文で書くことが一般的かと思います。
ですが、Rubyで言うところのeach_with_indexみたいにインデックス番号をつけてループ処理を行う方法はどのようにするべきか、考えてみました。

サンプルとして作ったソースはこちらにあります。

普通のループを駆使してみる

標準の機能を利用して実現した場合を考えてみます。

通常のforループを利用するパターン

インデックスを利用して、ループを回す方法として真っ先に思いつくパターンです。
サンプル本体

List<String> list = Arrays.asList("AAA", "BBB", "CCC");
// 通常のforループを利用するパターン
for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    Logic.doSomething(s, i);
}

この書き方の場合、要素取得時にランダムアクセスが発生してしまうところが弱点になりそうです。配列やArrayListならば問題ないですが、LinkedListなどのリスト構造の場合は不利になってしまいます。内部実装に依存するロジックというのも違和感がありますし、そもそもJavaでは拡張for文がありますので、そちらを利用したいところです。

拡張for文と変数を利用するパターン

上述の弱点を補いつつ、ループカウンタとなる変数によってインデックス番号を取得するパターンです。
サンプル本体

List<String> list = Arrays.asList("AAA", "BBB", "CCC");

// 拡張for文で実現するパターン
int i = 0;
for (String s : list) {
    Logic.doSomething(s, i++);
}

拡張for文はIterableを実装したクラスおよび配列で利用できます。拡張for文では、対象のイテレータを利用して順次アクセスを試みます。これなら上述のランダムアクセスによる弱点がなくなりますので、こちらの方がより推奨される形になるでしょう。

ただ、個人的に気になるのが、カウンタ変数のスコープがループの外になってしまう点です。この場合、後続ロジックでも変数iが参照できてしまう点がどうにも気持ち悪いかなぁと感じます。細かいところが気になるのであれば、{}で囲んでスコープを閉じる方法も考えられますが、それだとネストが深くなってしまうのでなんとも言えません・・・

forEachとラムダ式を利用してみるパターン

せっかくのJava8ということでforEachラムダ式を使ってみるパターンも考えてみます。
サンプル本体

List<String> list = Arrays.asList("AAA", "BBB", "CCC");
// forEachとラムダ式を利用するパターン
AtomicInteger i = new AtomicInteger();
list.forEach(s -> Logic.doSomething(s,i.getAndIncrement()));

ラムダ式の内部はあるインターフェースを実装する匿名クラスを作成する糖衣構文と見ることができます。したがって、ラムダ式内部から外側のスコープの変数にアクセスするためにはfinal(代入不可能)である必要があります。したがってループカウンタとしてintなどのプリミティブ型を使うことができません(インクリメントする用途にfinalな変数は使えません・・・つまり詰んでます・・・)。
上記はソース的にはすっきり書けてはいますが、AtomicIntegerの同期処理部分のオーバーヘッドや変数iのスコープの問題など、いろいろと不利になりそうなところが結構見受けられます。どうしてもラムダ式で書きたいこだわりがあるのなら、って感じでしょうか。

ラムダ式を使わずにforEachを利用するパターン

forEachメソッドの引数はConsumerインターフェースの実装クラス(のインスタンス)です。上述のラムダ式は、匿名クラスをその場で定義してインスタンスを渡しているだけでしたので、自分で匿名クラスを記述しても問題ないわけです。したがって以下のような実装が可能です。
サンプル本体

List<String> list = Arrays.asList("AAA", "BBB", "CCC");
// 匿名クラスを使って無理やり変数を閉じ込めたパターン
list.forEach(new Consumer<String>() {
    private int index = 0;

    @Override
    public void accept(String s) {
        Logic.doSomething(s, index++);
    }
});

変数のスコープや同期の問題など、様々な問題はクリアできているように見えますが・・・なんとなくイケテナイ気がしてしまいます・・・

自分でクラスを作ってみる

自分でクラスやメソッドを作成することで、より便利な方法はないかを検討してみます。
まずはループで利用するデータと、インデックス番号を保持するクラスを作成し、こちらを利用したいと思います。(今後こちらを共通で利用します)
サンプル本体

public class ValWithIndex<T> {
    private final T val;
    private final int index;

    public ValWithIndex(T val, int index) {
        this.val = val;
        this.index = index;
    }

    public T getVal() {
        return val;
    }

    public int getIndex() {
        return index;
    }
}

オブジェクトはジェネリクスで型を守りつつ、getterのみを保持する不変オブジェクトっぽくしてみました。(indexに引数チェックを入れても良さそうです)

拡張for文に適用させるパターン

拡張for文の中で上述のインスタンスを順次受け取ることができれば使い勝手が良さそうです。なので、以下のようなメソッドを作成します。(引数チェックなどは省略しています、ご了承ください・・・)
サンプル本体

public static <T> Iterable<ValWithIndex<T>> withIndex(Iterable<T> itr) {

    return () -> new Iterator<ValWithIndex<T>>() {
        private int index = 0;
        private Iterator<T> it = itr.iterator();

        @Override
        public boolean hasNext() {
            return it.hasNext();
        }

        @Override
        public ValWithIndex<T> next() {
            return new ValWithIndex<>(it.next(), index++);
        }
    };
}

ちょっと複雑になってしまいました・・・
このメソッドはイテレートしたい対象を引数で受け取り、そちらをラップしたIterableのインスタンスを返します。Iterableiteratorという唯一のメソッドしか持っていないため、ラムダ式で匿名クラスを記述することが可能です。
iteratorメソッドからは、Iteratorを実装する匿名クラスのインスタンスを返しています。こちらはメソッドが複数あるのでラムダ式では書けません。内部の処理は基本的に引数でもらったイテレータに処理を移譲しています。(nextの終端判定などはもう少し考慮の必要がありそうです)

メソッドの利用方法は以下の通りです。

List<String> list = Arrays.asList("AAA", "BBB", "CCC");

// 拡張for文に対応させたパターン
for (ValWithIndex<String> v : withIndex(list)) {
    Logic.doSomething(v.getVal(), v.getIndex());
}

利用する側は結構すっきりです。

Streamの途中に挟むパターン

上述の通り、処理対象となるオブジェクトとインデックス番号のペアを作成することができれば、ラムダ式の中でも利用できるかもしれません。Stream#mapにてオブジェクトを変換するパターンをやってみます。
サンプル本体

public static <T> Function<T, ValWithIndex<T>> withIndex() {
    return new Function<T, ValWithIndex<T>>() {
        private int index;

        @Override
        public ValWithIndex<T> apply(T t) {
            return new ValWithIndex<>(t, index++);
        }
    };
}

Stream#mapメソッドに適用できるFunctionインターフェースの実装クラスを作成しています。実際の利用シーンは以下の通りです。

List<String> list = Arrays.asList("AAA", "BBB", "CCC");

// Streamの間に挟んだパターン
list.stream().map(withIndex()).forEach(v -> Logic.doSomething(v.getVal(), v.getIndex()));

一見すると、良さそうなのですが、Stream#parallelStreamを利用された場合の動作が全く保障できません。そもそも並列実行される前提におけるオブジェクトの順序(インデックス番号)とはどういったものなのでしょうか・・・おそらくこの辺の問題があるので、標準ライブラリには実装が行われていないのかなぁと思います。
並列処理では使わない前提を置くのであれば、この書き方でも良いかと思います。しかしStreamAPIを前提とするメソッドという側面を考えるとやや矛盾している気もするので、少しだけ気持ち悪いです。

それでもせっかくなのでラムダ式を利用してみる

せっかくなのでラムダ式を・・・と言いたいところですが、StreamAPIを利用する場合、並列実行時に色々と問題があるようです。ですが、単にラムダ式ですっきり書きたければIterable#forEachを参考にするのが得策だと考えられます。

forEachメソッドの実装について

Java8でStreamAPIラムダ式が追加され、ついでにforEachメソッドも追加されました。個人的にも勘違いしていたのですが、Stream#forEachIterable#forEachは実は全くの別物でした。以下は実際のソースの一部です。

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

Iterableインターフェースのデフォルト実装として渡されています。(かなりワクワクしました)
ご覧の通りIterable#forEachは順次実行での動作を行っています。したがって、こちらを参考にすれば色々な問題は解決するような気がします。

forEachメソッドを参考に順次実行させるパターン

StreamAPIから切り離し、順次実行するメソッドとして定義した場合は以下のように書けそうです。(引数チェックなどは省略してます)
サンプル本体

public static <T> void eachWithIndex(Iterable<T> itr, ObjIntConsumer<T> action) {
    int index = 0;
    for (T t : itr) {
        action.accept(t, index++);
    }
}

インデックス番号と処理対象オブジェクトを受け取るために、ObjIntConsumerを引数で受け取ります。こうすることで、先ほどまでのように独自のクラスを使わなくても実現可能になりました。

利用方法は以下の通りです。

List<String> list = Arrays.asList("AAA", "BBB", "CCC");

// ラムダ式による順次実行のパターン
eachWithIndex(list, (v, i) -> Logic.doSomething(v, i));

かなりすっきりした気がします。

引数のシグニチャが若干気になる場合は、Iterableのサブインターフェースを利用してみるパターンも考えられます。(気持ち流れるような印象になりました)
サブインターフェースのサンプル

public interface WithIndexIterable<T> extends Iterable<T> {

    default void eachWithIndex(ObjIntConsumer<T> action) {
        int index = 0;
        for (T t : this) {
            action.accept(t, index++);
        }
    }

    static <T> WithIndexIterable<T> iterate(Iterable<T> itr) {
        return () -> itr.iterator();
    }
}

使い方サンプル

List<String> list = Arrays.asList("AAA", "BBB", "CCC");

// ラムダ式による順次実行の別パターン
WithIndexIterable.iterate(list).eachWithIndex((v, i) -> Logic.doSomething(v, i));

結論

ちょっと長くなってしまいましたが、個人的な見解です。

  • ライトに実現するなら拡張for文+ループカウンタが良い。
  • 順次処理のメソッドを用意する場合はIterable#forEachを参考にするのが良い。

といった感じでしょうか。

おまけ

そもそもインデックス番号って必要なのだろうか?

元も子もないことを書いてありますが、インデックス番号を使って行いたい処理ってそこまで多くのパターンが無いのかなぁと思うわけです。
この辺りの用途が非常によくまとまっていると感じるのは、JSTLのforEachにあるvarStatusかなぁと思います。
varStatusで参照できるプロパティは以下の通りです。

プロパティ 備考
current 現在のオブジェクト
index 現在のループのインデックス番号(0 origin)
count 現在のループのカウント数(1 origin)
first 現在がループの先頭かどうか
last 現在がループの最後かどうか
begin begin属性に指定した値
end end属性に指定した値
step step属性に指定した値

つまり、このあたりの情報があれば、利用する側としては使いやすいのではないかと思います。

varStatusっぽいクラスを自作したパターン

簡易的に行うために、内部クラスを利用しています。
サンプル本体

public static class Status<T> {
    private T current;
    private int index;
    private boolean isLast;

    private Status() {
    }

    private Status update(T current, int index, boolean isLast) {
        this.current = current;
        this.index = index;
        this.isLast = isLast;
        return this;
    }

    public T getCurrent() {
        return current;
    }

    public int getIndex() {
        return index;
    }

    public int getCount() {
        return index + 1;
    }

    public boolean isFirst() {
        return index == 0;
    }

    public boolean isLast() {
        return isLast;
    }
}

public static <T> void forEach(Iterable<T> itr, Consumer<Status<T>> action) {
    Status sts = new Status();
    int index = 0;
    Iterator<T> it = itr.iterator();
    while (it.hasNext()) {
        action.accept(sts.update(it.next(), index++, !it.hasNext()));
    }
}

ステータスのオブジェクトを使い回すパターンで実現しました。
今までの例では、オブジェクトを都度作成していましたが、ループ回数が多くなった場合にはパフォーマンスのオーバーヘッドが気になる場合もありますので、こちらの方がより良い形かと思われます。

実際の利用は以下の通りです。

List<String> list = Arrays.asList("AAA", "BBB", "CCC");

// ステータスを利用したパターン
forEach(list, v -> {
            if (v.isFirst()) {
                Logic.doFirst(v.getCurrent());
            } else if (v.isLast()) {
                Logic.doLast(v.getCurrent());
            } else {
                Logic.doOther(v.getCurrent());
            }
        }
);

まぁ、だいたいこんな感じですかね。

114
98
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
114
98