2016-09-14 4 views
1

Ich muss Code schreiben, der den Inhalt eines Java 8-Streams mehrfach zu einer Liste hinzufügt, und ich habe Probleme herauszufinden, wie das am besten funktioniert. Nach dem, was ich lese auf SO (vor allem diese Frage: How to add elements of a Java8 stream into an existing List) und anderswo, ich habe es verengt auf die folgenden Optionen:Was ist der beste Weg, Elemente aus einem Stream zu einer bestehenden Liste hinzuzufügen?

import java.util.ArrayList; 
import java.util.List; 
import java.util.function.Function; 
import java.util.stream.Collectors; 

public class Accumulator<S, T> { 


    private final Function<S, T> transformation; 
    private final List<T> internalList = new ArrayList<T>(); 

    public Accumulator(Function<S, T> transformation) { 
     this.transformation = transformation; 
    } 

    public void option1(List<S> newBatch) { 
     internalList.addAll(newBatch.stream().map(transformation).collect(Collectors.toList())); 
    } 

    public void option2(List<S> newBatch) { 
     newBatch.stream().map(transformation).forEach(internalList::add); 
    } 
} 

Die Idee ist, dass die Verfahren mehrmals für die gleiche Instanz aufgerufen werden würde von Accumulator. Die Auswahl besteht darin, eine Zwischenliste zu verwenden und Collection.addAll() einmal außerhalb des Streams aufzurufen oder collection.add() aus dem Stream für jedes Element aufzurufen.

Ich würde eher Option 2 bevorzugen, die mehr im Sinne der funktionalen Programmierung ist, und vermeiden Sie die Erstellung einer Zwischenliste, jedoch könnte es Vorteile zu rufen addAll() n 0 Mal anrufen, wenn n groß ist.

Ist eine der beiden Optionen deutlich besser als die andere?

BEARBEITEN: JB Nizet hat eine sehr coole answer, die die Umwandlung verzögert, bis alle Chargen hinzugefügt wurden. In meinem Fall ist es erforderlich, dass die Transformation sofort durchgeführt wird.

PS: In meinem Beispiel-Code, habe ich transformation als Platzhalter für das, was Operationen verwendet, die auf dem Strom

+0

Der disassemblierte Bytecode (javap) könnte Ihnen helfen, herauszufinden –

+1

Tun Sie keine vorzeitigen Optimierungen. Tun Sie, was sauberer ist, und nur wenn Sie Leistungsprobleme haben, überprüfen Sie diesen Code mit einem Profiler. –

+0

Ich glaube nicht, dass es irgendwelche Vorteile für den Aufruf von 'addAll()' gibt. – shmosel

Antwort

4

Zunächst einmal tun, Ihre zweite Variante sollte sein:

public void option2(List<S> newBatch) { 
    newBatch.stream().map(transformation).forEachOrdered(internalList::add); 
} 

korrekt sein.

Abgesehen davon, ist der Vorteil von addAll in

public void option1(List<S> newBatch) { 
    internalList.addAll(newBatch.stream().map(transformation).collect(Collectors.toList())); 
} 

ist strittig, wie die Collector API nicht der Strom erlaubt Hinweise über die erwartete Größe an den Kollektor zu schaffen und erfordert den Strom, der die Akkumulatorfunktion auszuwerten für jedes Element, das nichts anderes als ArrayList::add in der aktuellen Implementierung ist. So

bevor dieser Ansatz keinen Nutzen von addAll bekommen konnte, füllte es eine ArrayList durch wiederholte add auf einer ArrayList Aufruf, einschließlich möglicher Kapazitätserhöhung Operationen. So können Sie ohne Reue bei option2 bleiben.

Eine Alternative ist, einen Strom-Builder für temporäre Sammlungen zu verwenden:

public class Accumulator<S, T> { 
    private final Function<S, T> transformation; 
    private final Stream.Builder<T> internal = Stream.builder(); 

    public Accumulator(Function<S, T> transformation) { 
     this.transformation = transformation; 
    } 

    public void addBatch(List<S> newBatch) { 
     newBatch.stream().map(transformation).forEachOrdered(internal); 
    } 

    public List<T> finish() { 
     return internal.build().collect(Collectors.toList()); 
    } 
} 

Der Stream-Builder verwendet einen spined Puffer, der nicht den Inhalt Kopieren erfordert, wenn die Kapazität erhöht, aber die Lösung leidet nach wie vor von der Tatsache, dass der letzte Sammlungsschritt das Füllen eines ArrayList ohne eine geeignete Anfangskapazität (in der aktuellen Implementierung) beinhaltet.

Mit der aktuellen Implementierung ist es wesentlich effizientes den letzten Schritt als

public List<T> finish() { 
    return Arrays.asList(internal.build().toArray(…)); 
} 

Dies erfordert jedoch entweder zu implementieren, ein IntFunction<T[]> vom Anrufer zur Verfügung gestellt (wie wir nicht, dass für einen generischen Array-Typen tun), oder um eine unkontrollierte Operation auszuführen (eine Object[] als T[] vortäuschend, was hier in Ordnung wäre, aber immer noch eine böse ungeprüfte Operation).

5

Die beste Lösung wäre ein dritte durchgeführt werden müssen, dass die interne Liste zu vermeiden vollständig . Lassen Sie einfach den Strom für Sie die endgültige Liste erstellen:

Sie List<List<S>> haben Angenommen, Ihre N Chargen enthalten, auf denen die gleiche Transformation angewendet werden müssen, würden Sie

List<T> result = 
    batches.stream() 
      .flatMap(batch -> batch.stream()) 
      .map(transformation) 
      .collect(Collectors.toList()); 
+0

Dies ist die beste Option, da dies eine Liste fester Größe erzeugt. –

+2

Gute Antwort, aber es erfordert, dass Sie die Verarbeitung früherer Chargen verschieben können, bis alle Chargen bereit sind. Das ist möglicherweise nicht immer der Fall. – Andreas

+0

Gute Antwort, daran hätte ich nicht gedacht, obwohl ich in meinem Fall versuche, die Verarbeitung nicht zu verschieben, wie @Andreas erwähnt. Ich bearbeite die Frage entsprechend – ahelix

Verwandte Themen