So fügen Sie einer vorhandenen Liste Elemente eines Java8-Streams hinzu

155

Javadoc of Collector zeigt, wie Elemente eines Streams in einer neuen Liste gesammelt werden. Gibt es einen Einzeiler, der die Ergebnisse zu einer vorhandenen ArrayList hinzufügt?

codefx
quelle
1
Es gibt bereits eine Antwort hier . Suchen Sie nach dem Artikel "Zu einem vorhandenen Collection
Holger

Antworten:

197

HINWEIS : Die Antwort von nosid zeigt, wie Sie mithilfe von zu einer vorhandenen Sammlung hinzufügen können forEachOrdered(). Dies ist eine nützliche und effektive Technik zum Mutieren vorhandener Sammlungen. In meiner Antwort geht es darum, warum Sie a nicht verwenden sollten Collector, um eine vorhandene Sammlung zu mutieren.

Die kurze Antwort lautet: Nein , zumindest nicht im Allgemeinen. Sie sollten a nicht verwenden Collector, um eine vorhandene Sammlung zu ändern.

Der Grund dafür ist, dass Kollektoren Parallelität unterstützen, auch über Sammlungen, die nicht threadsicher sind. Die Art und Weise, wie sie dies tun, besteht darin, dass jeder Thread unabhängig mit seiner eigenen Sammlung von Zwischenergebnissen arbeitet. Jeder Thread erhält eine eigene Sammlung, indem er die aufruft, die Collector.supplier()erforderlich ist, um jedes Mal eine neue Sammlung zurückzugeben.

Diese Sammlungen von Zwischenergebnissen werden dann wieder in einer auf Threads beschränkten Weise zusammengeführt, bis eine einzige Ergebnissammlung vorliegt. Dies ist das Endergebnis der collect()Operation.

Einige Antworten von Balder und Assylias haben vorgeschlagen, Collectors.toCollection()einen Lieferanten zu verwenden und dann zu übergeben, der eine vorhandene Liste anstelle einer neuen Liste zurückgibt. Dies verstößt gegen die Anforderung an den Lieferanten, dass er jedes Mal eine neue, leere Sammlung zurückgibt.

Dies funktioniert für einfache Fälle, wie die Beispiele in ihren Antworten zeigen. Dies schlägt jedoch fehl, insbesondere wenn der Stream parallel ausgeführt wird. (Eine zukünftige Version der Bibliothek kann sich auf unvorhergesehene Weise ändern, was dazu führt, dass sie selbst im sequentiellen Fall fehlschlägt.)

Nehmen wir ein einfaches Beispiel:

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

Wenn ich dieses Programm starte, bekomme ich oft eine ArrayIndexOutOfBoundsException. Dies liegt daran, dass mehrere Threads bearbeitet werden ArrayList, eine thread-unsichere Datenstruktur. OK, machen wir es synchronisiert:

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

Dies wird mit einer Ausnahme nicht mehr fehlschlagen. Aber anstelle des erwarteten Ergebnisses:

[foo, 0, 1, 2, 3]

es gibt seltsame Ergebnisse wie diese:

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

Dies ist das Ergebnis der oben beschriebenen akkumulierten Akkumulations- / Zusammenführungsoperationen. Bei einem parallelen Stream ruft jeder Thread den Lieferanten auf, um eine eigene Sammlung für die Zwischenakkumulation zu erhalten. Wenn Sie einen Lieferanten übergeben, der dieselbe Sammlung zurückgibt , hängt jeder Thread seine Ergebnisse an diese Sammlung an. Da es keine Reihenfolge zwischen den Threads gibt, werden die Ergebnisse in einer beliebigen Reihenfolge angehängt.

Wenn diese Zwischensammlungen dann zusammengeführt werden, wird die Liste im Grunde genommen mit sich selbst zusammengeführt. Listen werden mit zusammengeführt List.addAll(), was bedeutet, dass die Ergebnisse undefiniert sind, wenn die Quellensammlung während des Vorgangs geändert wird. In diesem Fall ArrayList.addAll()wird ein Array-Kopiervorgang ausgeführt, sodass er sich selbst dupliziert, was ungefähr so ​​ist, wie man es erwarten würde, denke ich. (Beachten Sie, dass andere List-Implementierungen möglicherweise ein völlig anderes Verhalten aufweisen.) Dies erklärt jedoch die seltsamen Ergebnisse und doppelten Elemente im Ziel.

Sie könnten sagen: "Ich werde nur sicherstellen, dass mein Stream nacheinander ausgeführt wird" und fortfahren und Code wie diesen schreiben

stream.collect(Collectors.toCollection(() -> existingList))

wie auch immer. Ich würde davon abraten. Wenn Sie den Stream steuern, können Sie sicher sein, dass er nicht parallel ausgeführt wird. Ich gehe davon aus, dass ein Programmierstil entsteht, bei dem Streams anstelle von Sammlungen weitergegeben werden. Wenn Ihnen jemand einen Stream übergibt und Sie diesen Code verwenden, schlägt dies fehl, wenn der Stream zufällig parallel ist. Schlimmer noch, jemand könnte Ihnen einen sequentiellen Stream übergeben, und dieser Code funktioniert eine Weile einwandfrei, besteht alle Tests usw. Einige Zeit später kann sich der Code an einer anderen Stelle im System ändern, um parallele Streams zu verwenden, die Ihren Code verursachen brechen.

OK, dann denken sequential()Sie daran, einen Stream aufzurufen, bevor Sie diesen Code verwenden:

stream.sequential().collect(Collectors.toCollection(() -> existingList))

Natürlich wirst du daran denken, dies jedes Mal zu tun, oder? :-) Nehmen wir an, Sie tun es. Dann wird sich das Leistungsteam fragen, warum all ihre sorgfältig ausgearbeiteten parallelen Implementierungen keine Beschleunigung bieten. Und noch einmal, sie werden es auf Ihren Code zurückführen, der den gesamten Stream zwingt, nacheinander ausgeführt zu werden.

Tu es nicht.

Stuart Marks
quelle
Tolle Erklärung! - Danke für die Klarstellung. Ich werde meine Antwort bearbeiten, um zu empfehlen, dies niemals mit möglichen parallelen Streams zu tun.
Balder
3
Wenn die Frage lautet, ob es einen Einzeiler gibt, um Elemente eines Streams zu einer vorhandenen Liste hinzuzufügen, lautet die kurze Antwort Ja . Siehe meine Antwort. Ich stimme Ihnen jedoch zu, dass die Verwendung von Collectors.toCollection () in Kombination mit einer vorhandenen Liste der falsche Weg ist.
Nosid
Wahr. Ich denke, der Rest von uns hat alle an Sammler gedacht.
Stuart Marks
Gute Antwort! Ich bin sehr versucht, die sequentielle Lösung zu verwenden, auch wenn Sie eindeutig davon abraten, da sie wie angegeben gut funktionieren muss. Aber die Tatsache, dass der Javadoc verlangt, dass das Lieferantenargument der toCollectionMethode jedes Mal eine neue und leere Sammlung zurückgibt, überzeugt mich davon. Ich möchte wirklich den Javadoc-Vertrag der Java-Kernklassen brechen.
Zoom
1
@AlexCurvers Wenn Sie möchten, dass der Stream Nebenwirkungen hat, möchten Sie ihn mit ziemlicher Sicherheit verwenden forEachOrdered. Zu den Nebenwirkungen gehört das Hinzufügen von Elementen zu einer vorhandenen Sammlung, unabhängig davon, ob bereits Elemente vorhanden sind. Wenn Sie möchten, dass die Elemente eines Streams in eine neue Sammlung eingefügt werden, verwenden Sie collect(Collectors.toList())oder toSet()oder toCollection().
Stuart Marks
169

Soweit ich sehen kann, haben alle anderen Antworten bisher einen Kollektor verwendet, um einem vorhandenen Stream Elemente hinzuzufügen. Es gibt jedoch eine kürzere Lösung, die sowohl für sequentielle als auch für parallele Streams funktioniert. Sie können die Methode forEachOrdered einfach in Kombination mit einer Methodenreferenz verwenden.

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

Die einzige Einschränkung besteht darin, dass Quelle und Ziel unterschiedliche Listen sind, da Sie keine Änderungen an der Quelle eines Streams vornehmen dürfen, solange dieser verarbeitet wird.

Beachten Sie, dass diese Lösung sowohl für sequentielle als auch für parallele Streams funktioniert. Es profitiert jedoch nicht von der Parallelität. Die an forEachOrdered übergebene Methodenreferenz wird immer nacheinander ausgeführt.

nosid
quelle
6
+1 Es ist lustig, wie viele Leute behaupten, dass es keine Möglichkeit gibt, wenn es eine gibt. Übrigens. Ich habe es vor zwei MonatenforEach(existing::add) als Möglichkeit in eine Antwort aufgenommen . Ich hätte auch hinzufügen sollen forEachOrdered...
Holger
5
Gibt es einen Grund, den Sie forEachOrderedanstelle von verwendet haben forEach?
Membersound
6
@membersound: Funktioniert forEachOrderedsowohl für sequentielle als auch für parallele Streams. Im Gegensatz forEachdazu kann das übergebene Funktionsobjekt für parallele Streams gleichzeitig ausgeführt werden. In diesem Fall muss das Funktionsobjekt ordnungsgemäß synchronisiert werden, z Vector<Integer>. B. mithilfe von a .
Nosid
@BrianGoetz: Ich muss zugeben, dass die Dokumentation von Stream.forEachOrdered etwas ungenau ist. Allerdings kann ich keine vernünftige Interpretation dieser siehe Spezifikation , in dem es keine ist passiert-vor - Beziehung zwischen zwei Anrufen von target::add. Unabhängig davon, von welchen Threads die Methode aufgerufen wird, gibt es kein Datenrennen . Ich hätte erwartet, dass Sie das wissen.
Nosid
Dies ist für mich die nützlichste Antwort. Es zeigt tatsächlich eine praktische Möglichkeit, Elemente aus einem Stream in eine vorhandene Liste einzufügen.
Dies
12

Die kurze Antwort lautet nein (oder sollte nein sein). EDIT: Ja, es ist möglich (siehe Assylias 'Antwort unten), aber lesen Sie weiter. EDIT2: Aber siehe Stuart Marks Antwort aus einem weiteren Grund, warum Sie es immer noch nicht tun sollten!

Die längere Antwort:

Der Zweck dieser Konstrukte in Java 8 besteht darin, einige Konzepte der funktionalen Programmierung in die Sprache einzuführen. In der funktionalen Programmierung werden Datenstrukturen normalerweise nicht geändert, sondern neue werden aus alten durch Transformationen wie Map, Filter, Fold / Reduce und viele andere erstellt.

Wenn Sie die alte Liste ändern müssen , sammeln Sie einfach die zugeordneten Elemente in einer neuen Liste:

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

und dann list.addAll(newList)- wieder: wenn Sie wirklich müssen.

(oder erstellen Sie eine neue Liste, die die alte und die neue verkettet, und weisen Sie sie wieder der listVariablen zu - dies ist ein bisschen mehr im Sinne von FP als addAll)

Was die API betrifft: Auch wenn die API dies zulässt (siehe auch die Antwort von Assylias), sollten Sie versuchen, dies zumindest im Allgemeinen zu vermeiden. Es ist am besten, das Paradigma (FP) nicht zu bekämpfen und zu versuchen, es zu lernen, anstatt es zu bekämpfen (obwohl Java im Allgemeinen keine FP-Sprache ist), und nur dann auf "schmutzigere" Taktiken zurückzugreifen, wenn dies unbedingt erforderlich ist.

Die wirklich lange Antwort: (dh wenn Sie die Mühe mit einbeziehen, ein FP-Intro / Buch wie vorgeschlagen tatsächlich zu finden und zu lesen)

Um herauszufinden, warum das Ändern vorhandener Listen im Allgemeinen eine schlechte Idee ist und zu weniger wartbarem Code führt - es sei denn, Sie ändern eine lokale Variable und Ihr Algorithmus ist kurz und / oder trivial, was außerhalb des Bereichs der Frage der Code-Wartbarkeit liegt - Finden Sie eine gute Einführung in die funktionale Programmierung (es gibt Hunderte) und beginnen Sie mit dem Lesen. Eine "Vorschau" -Erklärung wäre so etwas wie: Es ist mathematisch fundierter und einfacher zu überlegen, Daten nicht zu ändern (in den meisten Teilen Ihres Programms) und führt zu einem höheren Level und weniger technisch (sowie menschlicherfreundlich, sobald Ihr Gehirn Übergänge weg von den imperativen Denkweisen des alten Stils) Definitionen der Programmlogik.

Erik Kaplun
quelle
@assylias: logischerweise war es nicht falsch, weil es das oder Teil gab; trotzdem, fügte eine Notiz hinzu.
Erik Kaplun
1
Die kurze Antwort ist richtig. Die vorgeschlagenen Einzeiler werden in einfachen Fällen erfolgreich sein, im allgemeinen Fall jedoch fehlschlagen.
Stuart Marks
Die längere Antwort ist größtenteils richtig, aber beim Design der API geht es hauptsächlich um Parallelität und weniger um funktionale Programmierung. Obwohl es natürlich viele Dinge an FP gibt, die der Parallelität zugänglich sind, sind diese beiden Konzepte gut aufeinander abgestimmt.
Stuart Marks
@StuartMarks: Interessant: In welchen Fällen bricht die in der Antwort von Assylias angegebene Lösung zusammen? (und gute Punkte über Parallelität - ich glaube, ich war zu eifrig, FP zu befürworten)
Erik Kaplun
@ErikAllik Ich habe eine Antwort hinzugefügt, die dieses Problem abdeckt.
Stuart Marks
11

Erik Allik gab bereits sehr gute Gründe an, warum Sie höchstwahrscheinlich keine Elemente eines Streams in einer vorhandenen Liste sammeln möchten.

Auf jeden Fall können Sie den folgenden Einzeiler verwenden, wenn Sie diese Funktionalität wirklich benötigen.

Aber wie Stuart Marks in seiner Antwort erklärt, sollten Sie dies niemals tun, wenn die Streams parallele Streams sein könnten - die Verwendung erfolgt auf eigenes Risiko ...

list.stream().collect(Collectors.toCollection(() -> myExistingList));
Balder
quelle
ahh, das ist eine Schande: P
Erik Kaplun
2
Diese Technik schlägt schrecklich fehl, wenn der Stream parallel ausgeführt wird.
Stuart Marks
1
Es liegt in der Verantwortung des Sammlungsanbieters, sicherzustellen, dass dies nicht fehlschlägt - z. B. durch die Bereitstellung einer gleichzeitigen Sammlung.
Balder
2
Nein, dieser Code verstößt gegen die Anforderung von toCollection (), wonach der Lieferant eine neue, leere Sammlung des entsprechenden Typs zurückgibt. Selbst wenn das Ziel threadsicher ist, führt das Zusammenführen für den parallelen Fall zu falschen Ergebnissen.
Stuart Marks
1
@Balder Ich habe eine Antwort hinzugefügt, die dies klarstellen soll.
Stuart Marks
4

Sie müssen nur auf Ihre ursprüngliche Liste verweisen, um diejenige zu sein, die Collectors.toList()zurückgegeben wird.

Hier ist eine Demo:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

Und so können Sie die neu erstellten Elemente in nur einer Zeile zu Ihrer ursprünglichen Liste hinzufügen.

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

Das bietet dieses Paradigma der funktionalen Programmierung.

Aman Agnihotri
quelle
Ich wollte sagen, wie man eine bestehende Liste hinzufügt / sammelt und nicht nur neu zuweist.
Codefx
1
Technisch gesehen kann man so etwas im Paradigma der funktionalen Programmierung, um das es bei Streams geht, nicht machen. In der funktionalen Programmierung wird der Status nicht geändert. Stattdessen werden neue Status in persistenten Datenstrukturen erstellt, wodurch er für Parallelitätszwecke sicher und funktionaler wird. Der Ansatz, den ich erwähnt habe, ist das, was Sie tun können, oder Sie können auf den objektorientierten Ansatz alten Stils zurückgreifen, bei dem Sie über jedes Element iterieren und die Elemente nach Belieben beibehalten oder entfernen.
Aman Agnihotri
0

targetList = sourceList.stream (). flatmap (List :: stream) .collect (Collectors.toList ());

AS Ranjan
quelle
0

Ich würde die alte und die neue Liste als Streams verketten und die Ergebnisse in der Zielliste speichern. Funktioniert auch parallel gut.

Ich werde das Beispiel einer akzeptierten Antwort von Stuart Marks verwenden:

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

Ich hoffe es hilft.

Nikos Stais
quelle