web-dev-qa-db-de.com

Aufteilen der Liste in Unterlisten nach Elementen

Ich habe diese Liste (List<String>):

["a", "b", null, "c", null, "d", "e"]

Und ich hätte so etwas gern:

[["a", "b"], ["c"], ["d", "e"]]

Mit anderen Worten, ich möchte meine Liste in Unterlisten aufteilen, wobei der Wert null als Trennzeichen verwendet wird, um eine Liste mit Listen (List<List<String>>) zu erhalten. Ich suche eine Java 8-Lösung. Ich habe es mit Collectors.partitioningBy ausprobiert, bin aber nicht sicher, wonach ich suche. Vielen Dank!

64
Oneiros

Die einzige Lösung, die ich im Moment vorstelle, ist die Implementierung eines eigenen benutzerdefinierten Collectors. 

Bevor ich die Lösung lese, möchte ich noch einige Anmerkungen dazu hinzufügen. Ich habe diese Frage eher als Programmierübung genommen und bin mir nicht sicher, ob dies mit einem parallelen Stream möglich ist. 

Sie müssen sich also bewusst sein, dass still unterbrochen wird, wenn die Pipeline in parallel ausgeführt wird. 

Dies ist nicht ein wünschenswertes Verhalten und sollte vermieden werden. Deshalb führe ich im Combiner-Teil eine Ausnahme aus (anstelle von (l1, l2) -> {l1.addAll(l2); return l1;}), da diese beim Kombinieren der beiden Listen parallel verwendet wird, sodass Sie anstelle eines falschen Ergebnisses eine Ausnahme haben.

Dies ist auch aufgrund des Listenkopierens nicht sehr effizient (obwohl das zugrunde liegende Array mit einer systemeigenen Methode kopiert wird).

Also hier ist die Collector-Implementierung:

private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) {
    final List<String> current = new ArrayList<>();
    return Collector.of(() -> new ArrayList<List<String>>(),
        (l, elem) -> {
            if (sep.test(elem)) {
                l.add(new ArrayList<>(current));
                current.clear();
            }
            else {
                current.add(elem);
            }
        },
        (l1, l2) -> {
            throw new RuntimeException("Should not run this in parallel");
        },
        l -> {
            if (current.size() != 0) {
                l.add(current);
                return l;
            }
        );
}

und wie man es benutzt:

List<List<String>> ll = list.stream().collect(splitBySeparator(Objects::isNull));

Ausgabe:

[[a, b], [c], [d, e]]


Da die Antwort von Joop Eggen out ist, scheint es, dass dies parallel erfolgen kann (geben Sie ihm die Anerkennung dafür!). Dadurch wird die Implementierung des benutzerdefinierten Collektors auf Folgendes reduziert:

private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) {
    return Collector.of(() -> new ArrayList<List<String>>(Arrays.asList(new ArrayList<>())),
                        (l, elem) -> {if(sep.test(elem)){l.add(new ArrayList<>());} else l.get(l.size()-1).add(elem);},
                        (l1, l2) -> {l1.get(l1.size() - 1).addAll(l2.remove(0)); l1.addAll(l2); return l1;});
}

was den Paragraph über Parallelität ein wenig obsolet erscheinen lässt, aber ich lasse es, da es eine gute Erinnerung sein kann.


Beachten Sie, dass die Stream-API nicht immer ein Ersatz ist. Es gibt Aufgaben, die mit den Streams einfacher und geeigneter sind, und Aufgaben, die dies nicht tun. In Ihrem Fall können Sie auch eine Utility-Methode dafür erstellen:

private static <T> List<List<T>> splitBySeparator(List<T> list, Predicate<? super T> predicate) {
    final List<List<T>> finalList = new ArrayList<>();
    int fromIndex = 0;
    int toIndex = 0;
    for(T elem : list) {
        if(predicate.test(elem)) {
            finalList.add(list.subList(fromIndex, toIndex));
            fromIndex = toIndex + 1;
        }
        toIndex++;
    }
    if(fromIndex != toIndex) {
        finalList.add(list.subList(fromIndex, toIndex));
    }
    return finalList;
}

und nennen es List<List<String>> list = splitBySeparator(originalList, Objects::isNull);

Es kann für die Überprüfung von Edge-Fällen verbessert werden. 

31
Alexis C.

Obwohl es bereits mehrere Antworten gibt und eine akzeptierte Antwort, fehlen in diesem Thema noch einige Punkte. Erstens scheint der Konsens zu sein, dass die Lösung dieses Problems mit Streams lediglich eine Übung ist und dass der konventionelle For-Loop-Ansatz vorzuziehen ist. Zweitens haben die bisherigen Antworten einen Ansatz übersprungen, bei dem Array- oder Vektor-Techniken eingesetzt wurden, die meiner Meinung nach die Streams-Lösung erheblich verbessern.

Hier ist zunächst eine herkömmliche Lösung zum Zwecke der Diskussion und Analyse:

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;
}

Das ist meistens unkompliziert, aber es ist ein bisschen subtiler. Ein Punkt ist, dass eine ausstehende Unterliste von prev bis cur immer geöffnet ist. Wenn wir auf null stoßen, schließen wir es, fügen es der Ergebnisliste hinzu und bringen prev vor. Nach der Schleife schließen wir die Unterliste bedingungslos.

Eine weitere Beobachtung ist, dass dies eine Schleife über Indizes ist, nicht über die Werte selbst. Daher verwenden wir eine arithmetische for-Schleife anstelle der erweiterten "for-each" -Schleife. Es legt jedoch nahe, dass wir mithilfe der Indizes Streams erstellen können, um Unterbereiche zu generieren, anstatt Werte zu strömen und die Logik in den Collector zu stellen (wie dies von Joop Eggens Lösungsvorschlag getan wurde).

Sobald wir das erkannt haben, können wir sehen, dass jede Position von null in der Eingabe das Trennzeichen für eine Unterliste ist: Es ist das rechte Ende der Unterliste links und es (plus eine) ist das linke Ende der Unterliste das Recht. Wenn wir die Edge-Fälle handhaben können, führt dies zu einem Ansatz, bei dem wir die Indizes finden, in denen null-Elemente vorkommen, diese zu Unterlisten zuordnen und die Unterlisten sammeln.

Der resultierende Code lautet wie folgt:

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());
}

Die Indexe zu finden, bei denen null vorkommt, ist ziemlich einfach. Der Stolperblock fügt am linken Ende -1 und am rechten Ende size hinzu. Ich habe mich für Stream.of entschieden, um das Anhängen durchzuführen, und dann flatMapToInt, um sie zu reduzieren. (Ich habe mehrere andere Ansätze ausprobiert, aber diese schien am saubersten zu sein.)

Es ist etwas bequemer, Arrays für die Indizes hier zu verwenden. Erstens ist die Notation für den Zugriff auf ein Array schöner als für eine Liste: indexes[i] vs. indexes.get(i). Zweitens vermeidet die Verwendung eines Arrays das Boxen.

Zu diesem Zeitpunkt ist jeder Indexwert im Array (außer dem letzten) um eins niedriger als die Anfangsposition einer Unterliste. Der Index rechts davon ist das Ende der Unterliste. Wir streamen einfach über das Array und ordnen jedes Indexpaar einer Unterliste zu und sammeln die Ausgabe.

Diskussion

Der Streams-Ansatz ist etwas kürzer als die For-Loop-Version, aber er ist dichter. Die for-loop-Version ist bekannt, weil wir dies ständig in Java erledigen. Wenn Sie jedoch noch nicht wissen, was diese Schleife tun soll, ist das nicht offensichtlich. Möglicherweise müssen Sie einige Schleifenausführungen simulieren, bevor Sie wissen, was prev tut und warum die geöffnete Unterliste nach dem Ende der Schleife geschlossen werden muss. (Ich habe es anfangs vergessen, aber ich habe es beim Testen erwischt.)

Ich denke, der Streams-Ansatz ist leichter zu verstehen, was los ist: Holen Sie sich eine Liste (oder ein Array), die die Grenzen zwischen den Unterlisten angibt. Das ist ein einfacher Stream für zwei Liner. Die Schwierigkeit besteht, wie bereits erwähnt, darin, die Edge-Werte an den Enden zu befestigen. Wenn es dafür eine bessere Syntax gäbe, z.

    // Java plus Pidgin Scala
    int[] indexes =
        [-1] ++ IntStream.range(0, input.size())
                         .filter(i -> input.get(i) == null) ++ [input.size()];

es würde die Dinge viel unübersichtlich machen. (Was wir wirklich brauchen, ist das Verständnis von Arrays oder Listen.) Sobald Sie die Indizes haben, können Sie sie einfach in tatsächliche Unterlisten abbilden und in der Ergebnisliste sammeln.

Und das ist natürlich sicher, wenn Sie parallel laufen.

UPDATE 2016-02-06

Hier ist eine schönere Methode zum Erstellen eines Arrays von Unterlistenindizes. Es basiert auf den gleichen Prinzipien, passt jedoch den Indexbereich an und fügt dem Filter einige Bedingungen hinzu, um zu vermeiden, dass die Indizes verkettet und flach abgebildet werden müssen.

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());
}

UPDATE 2016-11-23

Bei der Devoxx Antwerp 2016, einem Vortrag mit Brian Goetz, habe ich "Thinking In Parallel" ( video ) vorgestellt, in dem dieses Problem und meine Lösungen beschrieben wurden. Das dort vorgestellte Problem ist eine kleine Variation, die sich auf "#" anstatt auf null teilt, aber ansonsten ist es dasselbe. In dem Vortrag erwähnte ich, dass ich eine Reihe von Komponententests für dieses Problem hatte. Ich habe sie unten als eigenständiges Programm zusammen mit meinen Implementierungen von Loop und Streams angehängt. Eine interessante Übung für die Leser ist es, Lösungen zu finden, die in anderen Antworten gegen die hier vorgestellten Testfälle vorgeschlagen wurden, und um zu sehen, welche der Gründe scheitern. (Die anderen Lösungen müssen angepasst werden, um sie basierend auf einem Prädikat aufzuteilen, anstatt sie auf null aufzuteilen.)

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);
    }
}
62
Stuart Marks

Die Lösung ist, Stream.collect zu verwenden. Das Erstellen eines Collectors mit seinem Builder-Muster ist bereits als Lösung angegeben. Die Alternative ist, dass der andere überladene collect etwas winziger ist.

    List<String> strings = Arrays.asList("a", "b", null, "c", null, "d", "e");
    List<List<String>> groups = strings.stream()
            .collect(() -> {
                List<List<String>> list = new ArrayList<>();
                list.add(new ArrayList<>());
                return list;
            },
            (list, s) -> {
                if (s == null) {
                    list.add(new ArrayList<>());
                } else {
                    list.get(list.size() - 1).add(s);
                }
            },
            (list1, list2) -> {
                // Simple merging of partial sublists would
                // introduce a false level-break at the beginning.
                list1.get(list1.size() - 1).addAll(list2.remove(0));
                list1.addAll(list2);
            });

Wie man sieht, mache ich eine Liste von Stringlisten, in denen immer mindestens eine letzte (leere) Stringliste vorhanden ist.

  • Die erste Funktion erstellt eine Startliste mit Stringlisten. Gibt das Ergebnisobjekt an.
  • Die zweite Funktion wird aufgerufen, um jedes Element zu verarbeiten. Es ist eine Aktion auf das Teilergebnis und ein Element.
  • Das dritte wird nicht wirklich verwendet, es kommt beim Parallelisieren der Verarbeitung zum Tragen, wenn Teilergebnisse kombiniert werden müssen.

Eine Lösung mit Akku:

@StuartMarks weist darauf hin, dass der Kombinierer den Vertrag für Parallelität nicht erfüllt.

Aufgrund des Kommentars von @ArnaudDenoyelle eine Version, die reduce verwendet.

    List<List<String>> groups = strings.stream()
            .reduce(new ArrayList<List<String>>(),
                    (list, s) -> {
                        if (list.isEmpty()) {
                            list.add(new ArrayList<>());
                        }
                        if (s == null) {
                            list.add(new ArrayList<>());
                        } else {
                            list.get(list.size() - 1).add(s);
                        }
                        return list;
                    },
                    (list1, list2) -> {
                            list1.addAll(list2);
                            return list1;
                    });
  • Der erste Parameter ist das akkumulierte Objekt.
  • Die zweite Funktion sammelt sich.
  • Der dritte ist der zuvor erwähnte Kombinierer.
23
Joop Eggen

Bitte stimmen Sie nicht. Ich habe nicht genug Platz, um dies in Kommentaren zu erklären .

Dies ist eine Lösung mit einer Stream und einer foreach, aber dies entspricht streng der Lösung von Alexis oder einer foreach-Schleife (und weniger klar, und ich konnte den Kopierkonstruktor nicht loswerden):

List<List<String>> result = new ArrayList<>();
final List<String> current = new ArrayList<>();
list.stream().forEach(s -> {
      if (s == null) {
        result.add(new ArrayList<>(current));
        current.clear();
      } else {
        current.add(s);
      }
    }
);
result.add(current);

System.out.println(result);

Ich verstehe, dass Sie mit Java 8 eine elegantere Lösung finden möchten, aber ich glaube wirklich, dass sie nicht für diesen Fall entwickelt wurde. Und wie Herr Löffel gesagt hat, bevorzugen Sie in diesem Fall den naiven Weg.

8

Obwohl die Antwort von Marks Stuart prägnant, intuitiv und parallel sicher (und die beste) ist, möchte ich eine andere interessante Lösung vorstellen, die nicht den Anfangs-/Endgrenzen-Trick erfordert.

Wenn wir die Problemdomäne betrachten und über Parallelität nachdenken, können wir dies mit einer Divide-and-Conquer-Strategie leicht lösen. Anstatt das Problem als eine serielle Liste zu betrachten, die wir durchlaufen müssen, können wir das Problem als eine Zusammensetzung desselben grundlegenden Problems betrachten: Aufteilen einer Liste mit einem null-Wert. Wir können intuitiv leicht erkennen, dass wir das Problem mit der folgenden rekursiven Strategie rekursiv abbauen können:

split(L) :
  - if (no null value found) -> return just the simple list
  - else -> cut L around 'null' naming the resulting sublists L1 and L2
            return split(L1) + split(L2)

In diesem Fall suchen wir zuerst nach einem beliebigen null-Wert. Sobald wir einen Wert gefunden haben, schneiden wir die Liste sofort ab und rufen einen rekursiven Aufruf in den Unterlisten auf. Wenn wir null (den Basisfall) nicht finden, sind wir mit diesem Zweig fertig und geben einfach die Liste zurück. Wenn Sie alle Ergebnisse zusammenführen, wird die Liste zurückgegeben, nach der Sie suchen. 

Ein Bild sagt mehr als tausend Worte:

 enter image description here

Der Algorithmus ist einfach und vollständig: Wir benötigen keine speziellen Tricks, um die Edge-Fälle am Anfang/Ende der Liste zu behandeln. Wir benötigen keine speziellen Tricks, um Edge-Fälle wie leere Listen oder Listen mit nur null-Werten zu behandeln. Oder Listen, die mit null enden oder mit null beginnen.

Eine einfache naive Umsetzung dieser Strategie sieht folgendermaßen aus:

public List<List<String>> split(List<String> input) {

    OptionalInt index = IntStream.range(0, input.size())
                                 .filter(i -> input.get(i) == null)
                                 .findAny();

    if (!index.isPresent())
        return asList(input);

    List<String> firstHalf  = input.subList(0, index.getAsInt());
    List<String> secondHalf = input.subList(index.getAsInt()+1, input.size());

    return asList(firstHalf, secondHalf).stream()
                 .map(this::split)
                 .flatMap(List::stream)
                 .collect(toList());

}

Wir suchen zuerst nach dem Index eines beliebigen null-Werts in der Liste. Wenn wir keine finden, geben wir die Liste zurück. Wenn wir eine finden, teilen wir die Liste in zwei Unterlisten auf, streamen über sie und rufen rekursiv die split-Methode auf. Die resultierenden Listen des Unterproblems werden dann extrahiert und für den Rückgabewert kombiniert.

Beachten Sie, dass die beiden Streams problemlos parallel () erstellt werden können und der Algorithmus aufgrund der funktionalen Zerlegung des Problems weiterhin funktioniert.

Obwohl der Code bereits recht knapp ist, kann er immer auf vielfältige Weise angepasst werden. Anstatt den optionalen Wert im Basisfall zu prüfen, können wir die orElse-Methode für die OptionalInt-Methode nutzen, um den Endindex der Liste zurückzugeben, sodass wir den zweiten Stream und zusätzlich wiederverwenden können Leere Listen filtern:

public List<List<String>> split(List<String> input) {

    int index =  IntStream.range(0, input.size())
                          .filter(i -> input.get(i) == null)
                          .findAny().orElse(input.size());

    return asList(input.subList(0, index), input.subList(index+1, input.size())).stream()
                 .map(this::split)
                 .flatMap(List::stream)
                 .filter(list -> !list.isEmpty())
                 .collect(toList());
}

Das Beispiel soll nur auf die Einfachheit, Anpassungsfähigkeit und Eleganz eines rekursiven Ansatzes hinweisen. In der Tat würde diese Version einen geringen Leistungsnachteil verursachen und fehlschlagen, wenn die Eingabe leer wäre (und als solche möglicherweise eine zusätzliche leere Prüfung erforderlich ist)

In diesem Fall ist Rekursion möglicherweise nicht die beste Lösung ( Stuart Marks Algorithmus zum Suchen von Indizes ist nur O(N) und Mapping/Splitting-Listen sind mit erheblichen Kosten verbunden). Sie drückt die Lösung jedoch mit einem einfachen, intuitiven parallelisierbaren Algorithmus ohne Nebeneffekte aus.

Ich werde nicht tiefer in die Komplexität und die Vor- und Nachteile oder Anwendungsfälle mit Stoppkriterien und/oder der Verfügbarkeit von Teilergebnissen eintauchen. Ich hatte gerade das Bedürfnis, diese Lösungsstrategie zu teilen, da die anderen Ansätze lediglich iterativ waren oder einen zu komplexen Lösungsalgorithmus verwendeten, der nicht parallelisierbar war.

4

Hier ist ein weiterer Ansatz, der eine Gruppierungsfunktion verwendet, die Listenindizes für die Gruppierung verwendet.

Hier gruppiere ich das Element nach dem ersten Index, der diesem Element folgt, mit dem Wert null. In Ihrem Beispiel würden also "a" und "b"2 zugeordnet werden. Außerdem ordne ich null value dem -1 Index zu, der später entfernt werden sollte.

List<String> list = Arrays.asList("a", "b", null, "c", null, "d", "e");

Function<String, Integer> indexGroupingFunc = (str) -> {
             if (str == null) {
                 return -1;
             }
             int index = list.indexOf(str) + 1;
             while (index < list.size() && list.get(index) != null) {
                 index++;
             }
             return index;
         };

Map<Integer, List<String>> grouped = list.stream()
               .collect(Collectors.groupingBy(indexGroupingFunc));

grouped.remove(-1);  // Remove null elements grouped under -1
System.out.println(grouped.values()); // [[a, b], [c], [d, e]]

Sie können auch vermeiden, jedes Mal den ersten Index des null-Elements abzurufen, indem Sie den aktuellen min-Index in einer AtomicInteger zwischenspeichern. Die aktualisierte Function wäre wie folgt:

AtomicInteger currentMinIndex = new AtomicInteger(-1);

Function<String, Integer> indexGroupingFunc = (str) -> {
        if (str == null) {
            return -1;
        }
        int index = names.indexOf(str) + 1;

        if (currentMinIndex.get() > index) {
            return currentMinIndex.get();
        } else {
            while (index < names.size() && names.get(index) != null) {
              index++;
            }
            currentMinIndex.set(index);
            return index;
        }
    };
4
Rohit Jain

Dies ist ein sehr interessantes Problem. Ich habe eine einzeilige Lösung gefunden. Es ist vielleicht nicht sehr performant, aber es funktioniert.

List<String> list = Arrays.asList("a", "b", null, "c", null, "d", "e");
Collection<List<String>> cl = IntStream.range(0, list.size())
    .filter(i -> list.get(i) != null).boxed()
    .collect(Collectors.groupingBy(
        i -> IntStream.range(0, i).filter(j -> list.get(j) == null).count(),
        Collectors.mapping(i -> list.get(i), Collectors.toList()))
    ).values();

Es ist eine ähnliche Idee, die @Rohit Jain hatte. Ich gruppiere das Leerzeichen zwischen den Nullwerten . Wenn Sie wirklich einen List<List<String>> wünschen, können Sie Folgendes hinzufügen:

List<List<String>> ll = cl.stream().collect(Collectors.toList());
3
Flown

Nun, nach ein bisschen Arbeit haben Sie eine einzeilige Stream-basierte Lösung gefunden. Letztendlich verwendet es reduce() für die Gruppierung, was die natürliche Wahl war, aber es war ein wenig hässlich, die Zeichenfolgen in den List<List<String>> zu bringen, der von reduz:

List<List<String>> result = list.stream()
  .map(Arrays::asList)
  .map(x -> new LinkedList<String>(x))
  .map(Arrays::asList)
  .map(x -> new LinkedList<List<String>>(x))
  .reduce( (a, b) -> {
    if (b.getFirst().get(0) == null) 
      a.add(new LinkedList<String>());
    else
      a.getLast().addAll(b.getFirst());
    return a;}).get();

Es is jedoch 1 Zeile!

Wenn mit Eingabe von der Frage ausgeführt,

System.out.println(result);

Produziert:

[[a, b], [c], [d, e]]
2
Bohemian

Hier ist der Code von AbacusUtil

List<String> list = N.asList(null, null, "a", "b", null, "c", null, null, "d", "e");
Stream.of(list).splitIntoList(null, (e, any) -> e == null, null).filter(e -> e.get(0) != null).forEach(N::println);

Erklärung: Ich bin der Entwickler von AbacusUtil.

1
user_3380739

In meiner StreamEx - Bibliothek gibt es eine groupRuns -Methode, die Ihnen beim Lösen dieses Problems helfen kann:

List<String> input = Arrays.asList("a", "b", null, "c", null, "d", "e");
List<List<String>> result = StreamEx.of(input)
        .groupRuns((a, b) -> a != null && b != null)
        .remove(list -> list.get(0) == null).toList();

Die groupRuns-Methode verwendet eine BiPredicate, die für das Paar benachbarter Elemente den Wert true zurückgibt, wenn sie gruppiert werden sollen. Danach entfernen wir Gruppen mit Nullen und sammeln den Rest der Liste.

Diese Lösung ist parallel: Sie können sie auch für parallelen Stream verwenden. Es funktioniert auch mit jeder Stream-Quelle (nicht nur Listen mit wahlfreiem Zugriff, wie in anderen Lösungen), und es ist etwas besser als auf Kollektor basierende Lösungen, da hier jeder Terminalbetrieb ohne Zwischenablage genutzt werden kann.

0
Tagir Valeev

Ich habe mir das Video zu "Thinking in Parallel" von Stuart angesehen. Also entschied er sich, es zu lösen, bevor er seine Antwort im Video sah. Wird die Lösung mit der Zeit aktualisieren. zur Zeit

Arrays.asList(IntStream.range(0, abc.size()-1).
filter(index -> abc.get(index).equals("#") ).
map(index -> (index)).toArray()).
stream().forEach( index -> {for (int i = 0; i < index.length; i++) {
                    if(sublist.size()==0){
                        sublist.add(new ArrayList<String>(abc.subList(0, index[i])));
                    }else{

                    sublist.add(new ArrayList<String>(abc.subList(index[i]-1, index[i])));
                    }
                }
    sublist.add(new ArrayList<String>(abc.subList(index[index.length-1]+1, abc.size())));
});
0

Mit String kann man:

String s = ....;
String[] parts = s.split("sth");

Wenn alle sequentiellen Sammlungen (da der String eine Folge von Zeichen ist) diese Abstraktion hätten, könnte dies auch für sie möglich sein:

List<T> l = ...
List<List<T>> parts = l.split(condition) (possibly with several overloaded variants)

Wenn wir das ursprüngliche Problem auf "List of Strings" beschränken (und einige Einschränkungen für den Inhalt der Elemente festlegen), könnten wir es folgendermaßen hacken:

String als = Arrays.toString(new String[]{"a", "b", null, "c", null, "d", "e"});
String[] sa = als.substring(1, als.length() - 1).split("null, ");
List<List<String>> res = Stream.of(sa).map(s -> Arrays.asList(s.split(", "))).collect(Collectors.toList());

(bitte nicht ernst nehmen :))

Ansonsten funktioniert auch die einfache alte Rekursion:

List<List<String>> part(List<String> input, List<List<String>> acc, List<String> cur, int i) {
    if (i == input.size()) return acc;
    if (input.get(i) != null) {
        cur.add(input.get(i));
    } else if (!cur.isEmpty()) {
        acc.add(cur);
        cur = new ArrayList<>();
    }
    return part(input, acc, cur, i + 1);
}

(Hinweis: In diesem Fall muss null an die Eingabeliste angehängt werden.)

part(input, new ArrayList<>(), new ArrayList<>(), 0)
0
janek

Gruppieren Sie nach verschiedenen Token, wenn Sie eine Null (oder ein Trennzeichen) finden. Ich habe hier eine andere ganze Zahl verwendet (atomar nur als Halter)

Ordnen Sie dann die generierte Karte neu zu, um sie in eine Liste mit Listen zu transformieren.

AtomicInteger i = new AtomicInteger();
List<List<String>> x = Stream.of("A", "B", null, "C", "D", "E", null, "H", "K")
      .collect(Collectors.groupingBy(s -> s == null ? i.incrementAndGet() : i.get()))
      .entrySet().stream().map(e -> e.getValue().stream().filter(v -> v != null).collect(Collectors.toList()))
      .collect(Collectors.toList());

System.out.println(x);
0
Shadi Moadad