はじめに
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
のインスタンスを返します。Iterable
はiterator
という唯一のメソッドしか持っていないため、ラムダ式で匿名クラスを記述することが可能です。
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#forEach
とIterable#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());
}
}
);
まぁ、だいたいこんな感じですかね。