すでにいくつかの回答があり、受け入れられた回答がありますが、このトピックにはまだいくつかの点が欠けています。まず、コンセンサスは、ストリームを使用してこの問題を解決することは単なる演習であり、従来の for ループ アプローチが望ましいということです。第 2 に、これまでの回答では、ストリーム ソリューションを大幅に改善すると思われる配列またはベクトル スタイルの手法を使用したアプローチを見落としていました。
まず、議論と分析のために、従来の解決策を次に示します。
static List<List<String>> splitConventional(List<String> input) {
List<List<String>> result = new ArrayList<>();
int prev = 0;
for (int cur = 0; cur < input.size(); cur++) {
if (input.get(cur) == null) {
result.add(input.subList(prev, cur));
prev = cur + 1;
}
}
result.add(input.subList(prev, input.size()));
return result;
}
これはほとんど簡単ですが、少し微妙な点があります。1 つのポイントは、保留中のサブリスト from prev
tocur
が常に開いていることです。遭遇したらnull
、それを閉じて結果リストに追加し、先に進みprev
ます。ループの後、無条件にサブリストを閉じます。
もう 1 つの観察結果は、これは値自体ではなくインデックスのループであるため、拡張された「for-each」ループの代わりに算術 for ループを使用することです。ただし、値をストリーミングしてコレクターにロジックを入れる代わりに、インデックスを使用してストリーミングして部分範囲を生成できることを示唆しています ( Joop Eggen の提案されたソリューションで行われたように)。
これに気付くと、入力内の の各位置がサブリストの区切り文字であることがわかりますnull
。これはサブリストの右端から左にあり、(プラス 1) はサブリストの左端から右。null
エッジケースを処理できる場合、要素が発生するインデックスを見つけ、それらをサブリストにマップし、サブリストを収集するアプローチにつながります。
結果のコードは次のとおりです。
static List<List<String>> splitStream(List<String> input) {
int[] indexes = Stream.of(IntStream.of(-1),
IntStream.range(0, input.size())
.filter(i -> input.get(i) == null),
IntStream.of(input.size()))
.flatMapToInt(s -> s)
.toArray();
return IntStream.range(0, indexes.length-1)
.mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1]))
.collect(toList());
}
発生するインデックスを取得するのnull
は非常に簡単です。つまずきは-1
、左端とsize
右端に追加されています。Stream.of
追加を行っflatMapToInt
てからそれらを平坦化するために使用することを選択しました。(他にもいくつかのアプローチを試しましたが、これが最もクリーンなように思えました。)
ここでは、インデックスに配列を使用する方が少し便利です。まず、配列にアクセスするための表記法は、リストよりも優れていindexes[i]
ますindexes.get(i)
。第 2 に、配列を使用するとボックス化が回避されます。
この時点で、配列内の各インデックス値 (最後のものを除く) は、サブリストの開始位置よりも 1 つ少なくなります。そのすぐ右のインデックスは、サブリストの終わりです。配列をストリーミングし、インデックスの各ペアをサブリストにマップして、出力を収集するだけです。
討論
ストリーム アプローチは for ループ バージョンよりもわずかに短くなりますが、密度が高くなります。for ループ バージョンはよく知られています。これは常に Java で行われているためです。ただし、このループが何を行うべきかをまだ認識していない場合は、明らかではありません。何prev
が行われているか、ループの終了後に開いているサブリストを閉じる必要がある理由を理解する前に、いくつかのループ実行をシミュレートする必要がある場合があります。(最初は忘れていましたが、テストでこれを見つけました。)
サブリスト間の境界を示すリスト (または配列) を取得します。これは簡単なストリーム 2 ライナーです。上で述べたように、問題は端にエッジ値を追加する方法を見つけることです。これを行うためのより良い構文があれば、たとえば、
// Java plus pidgin Scala
int[] indexes =
[-1] ++ IntStream.range(0, input.size())
.filter(i -> input.get(i) == null) ++ [input.size()];
物事がぐちゃぐちゃになりません。(実際に必要なのは、配列またはリストの理解です。) インデックスを取得したら、それらを実際のサブリストにマップし、それらを結果リストに収集するのは簡単なことです。
もちろん、これは並行して実行しても安全です。
更新 2016-02-06
サブリスト インデックスの配列を作成するより良い方法を次に示します。これは同じ原則に基づいていますが、インデックスの範囲を調整し、いくつかの条件をフィルターに追加して、インデックスを連結してフラットマップする必要がないようにします。
static List<List<String>> splitStream(List<String> input) {
int sz = input.size();
int[] indexes =
IntStream.rangeClosed(-1, sz)
.filter(i -> i == -1 || i == sz || input.get(i) == null)
.toArray();
return IntStream.range(0, indexes.length-1)
.mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1]))
.collect(toList());
}
更新 2016-11-23
私は Devoxx Antwerp 2016 で Brian Goetz と共同で、この問題と私の解決策を取り上げた"Thinking In Parallel" (ビデオ) という講演を行いました。そこに提示された問題は、null の代わりに "#" で分割されるわずかなバリエーションですが、それ以外は同じです。講演の中で、私はこの問題に対してたくさんの単体テストを行っていると述べました。ループとストリームの実装とともに、スタンドアロン プログラムとして以下に追加しました。読者にとって興味深い演習は、ここで提供したテスト ケースに対して他の回答で提案されたソリューションを実行し、失敗したものとその理由を確認することです。(他のソリューションは、null で分割するのではなく、述語に基づいて分割するように適合させる必要があります。)
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.util.Arrays.asList;
public class ListSplitting {
static final Map<List<String>, List<List<String>>> TESTCASES = new LinkedHashMap<>();
static {
TESTCASES.put(asList(),
asList(asList()));
TESTCASES.put(asList("a", "b", "c"),
asList(asList("a", "b", "c")));
TESTCASES.put(asList("a", "b", "#", "c", "#", "d", "e"),
asList(asList("a", "b"), asList("c"), asList("d", "e")));
TESTCASES.put(asList("#"),
asList(asList(), asList()));
TESTCASES.put(asList("#", "a", "b"),
asList(asList(), asList("a", "b")));
TESTCASES.put(asList("a", "b", "#"),
asList(asList("a", "b"), asList()));
TESTCASES.put(asList("#"),
asList(asList(), asList()));
TESTCASES.put(asList("a", "#", "b"),
asList(asList("a"), asList("b")));
TESTCASES.put(asList("a", "#", "#", "b"),
asList(asList("a"), asList(), asList("b")));
TESTCASES.put(asList("a", "#", "#", "#", "b"),
asList(asList("a"), asList(), asList(), asList("b")));
}
static final Predicate<String> TESTPRED = "#"::equals;
static void testAll(BiFunction<List<String>, Predicate<String>, List<List<String>>> f) {
TESTCASES.forEach((input, expected) -> {
List<List<String>> actual = f.apply(input, TESTPRED);
System.out.println(input + " => " + expected);
if (!expected.equals(actual)) {
System.out.println(" ERROR: actual was " + actual);
}
});
}
static <T> List<List<T>> splitStream(List<T> input, Predicate<? super T> pred) {
int[] edges = IntStream.range(-1, input.size()+1)
.filter(i -> i == -1 || i == input.size() ||
pred.test(input.get(i)))
.toArray();
return IntStream.range(0, edges.length-1)
.mapToObj(k -> input.subList(edges[k]+1, edges[k+1]))
.collect(Collectors.toList());
}
static <T> List<List<T>> splitLoop(List<T> input, Predicate<? super T> pred) {
List<List<T>> result = new ArrayList<>();
int start = 0;
for (int cur = 0; cur < input.size(); cur++) {
if (pred.test(input.get(cur))) {
result.add(input.subList(start, cur));
start = cur + 1;
}
}
result.add(input.subList(start, input.size()));
return result;
}
public static void main(String[] args) {
System.out.println("===== Loop =====");
testAll(ListSplitting::splitLoop);
System.out.println("===== Stream =====");
testAll(ListSplitting::splitStream);
}
}