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?
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?
Antworten:
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.
toCollection
Methode jedes Mal eine neue und leere Sammlung zurückgibt, überzeugt mich davon. Ich möchte wirklich den Javadoc-Vertrag der Java-Kernklassen brechen.
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()
.
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.
forEach(existing::add)
als Möglichkeit in eine Antwort aufgenommen . Ich hätte auch hinzufügen sollen forEachOrdered
...
forEachOrdered
anstelle von verwendet haben forEach
?
forEachOrdered
sowohl für sequentielle als auch für parallele Streams. Im Gegensatz forEach
dazu 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 .
target::add
. Unabhängig davon, von welchen Threads die Methode aufgerufen wird, gibt es kein Datenrennen . Ich hätte erwartet, dass Sie das wissen.
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 list
Variablen 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 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));
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.
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.
Collection