2014-05-14 4 views
5

Ich versuche, die neuen Java 8 Stream APIs zu verstehen.Suche nach Durchschnitt mit reduzieren und sammeln

http://docs.oracle.com/javase/tutorial/collections/streams/reduction.html

fand ich das Beispiel der Verwendung collect API durchschnittlich Zahlen findet. Aber ich habe das gespürt, das gleiche kann auch mit reduce() gemacht werden.

public class Test { 

    public static void main(String[] args) { 
     // Using collect 
     System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 
      .collect(Averager::new, Averager::accept, Averager::combine) 
      .average()); 

     // Using reduce 
     System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 
      .reduce(new Averager(), (t, u) -> { 
       t.accept(u); 
       return t; 
      }, (t, u) -> { 
       t.combine(u); 
       return t; 
      }).average()); 
    } 

    private static class Averager { 
     private int total = 0; 
     private int count = 0; 

     public Averager() { 
      // System.out.println("Creating averager"); 
     } 

     public double average() { 
      // System.out.println("Finding average"); 
      return count > 0 ? ((double) total)/count : 0; 
     } 

     public void accept(int i) { 
      // System.out.println("Accepting " + i); 
      total += i; 
      count++; 
     } 

     public void combine(Averager other) { 
      // System.out.println("Combining the averager : " + other); 
      total += other.total; 
      count += other.count; 
     } 

     @Override 
     public String toString() { 
      return "[total : " + total + ", count: " + count + "]"; 
     } 
    } 
} 

1) Gibt es einen Grund, dass ich sammeln statt hier reduzieren sollte?
2) Wenn ich alle Debug-SYSOuts aktivieren, kann ich sehen, dass die ausgeführten Operationen genau dieselben sind, sammeln und reduzieren. Und der Combiner wurde in beiden Fällen überhaupt nicht benutzt.
3) Wenn ich die Streams parallel mache, gibt das Collect immer das korrekte Ergebnis zurück. Die reduce() gibt mir jedes Mal andere Ergebnisse.
4) Sollte ich nicht reduzieren, in parallelen Streams?

Danke,
Paul

Antwort

17

Der Unterschied zwischen reduce und collect ist, dass collect eine erweiterte Form der Reduktion, die mit veränderbaren Objekten parallel umgehen kann. Der collect Algorithmus-Thread beschränkt die verschiedenen Ergebnisobjekte, so dass sie sicher mutiert werden können, auch wenn sie nicht threadsicher sind. Deshalb funktioniert Averager mit collect. Für die sequentielle Berechnung mit reduce ist dies normalerweise nicht wichtig, aber für die parallele Berechnung wird es falsche Ergebnisse geben, wie Sie beobachtet haben.

Ein wichtiger Punkt ist, dass reduce funktioniert, solange es sich um Werte, aber nicht veränderbare Objekte handelt. Sie können dies sehen, indem Sie das erste Argument zu reduce betrachten. Der Beispielcode übergibt , das ein einzelnes Objekt ist, das als Identitätswert von mehreren Threads in der parallelen Reduktion verwandt wird. Die Art und Weise, wie parallele Streams arbeiten, besteht darin, dass die Arbeitslast in Segmente aufgeteilt wird, die von einzelnen Threads verarbeitet werden. Wenn mehrere Threads dasselbe (nicht Thread-sichere) Objekt mutieren, sollte klar sein, warum dies zu falschen Ergebnissen führt.

Es ist möglich, reduce zu verwenden, um einen Durchschnitt zu berechnen, aber Sie müssen Ihr Akkumulationsobjekt unveränderlich machen. Betrachten wir ein Objekt ImmutableAverager:

static class ImmutableAverager { 
    private final int total; 
    private final int count; 

    public ImmutableAverager() { 
     this.total = 0; 
     this.count = 0; 
    } 

    public ImmutableAverager(int total, int count) { 
     this.total = total; 
     this.count = count; 
    } 

    public double average() { 
     return count > 0 ? ((double) total)/count : 0; 
    } 

    public ImmutableAverager accept(int i) { 
     return new ImmutableAverager(total + i, count + 1); 
    } 

    public ImmutableAverager combine(ImmutableAverager other) { 
     return new ImmutableAverager(total + other.total, count + other.count); 
    } 
} 

Bitte beachte, dass ich die Unterschriften von accept angepasst haben und combine einen neuen ImmutableAverager statt mutiert this zurückzukehren. (Diese Veränderungen machen auch die Methoden, um die Funktionsargumente zu reduce übereinstimmen, damit wir Methode Referenzen verwenden.) Sie ImmutableAverager wie folgt verwenden würde:

System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 
      .parallel() 
      .reduce(new ImmutableAverager(), 
        ImmutableAverager::accept, 
        ImmutableAverager::combine) 
      .average()); 

Mit unveränderlichen Wert Objekte mit reduce sollten die richtigen Ergebnisse parallel geben.

Schließlich ist zu beachten, dass IntStream und DoubleStream haben summaryStatistics() Methoden und Collectors hat averagingDouble, averagingInt und averagingLong Methoden, die diese Berechnungen für Sie tun können. Ich denke jedoch, dass sich die Frage eher auf die Mechanismen der Sammlung und Reduktion bezieht als darauf, wie Mittelung am prägnantesten vorgenommen wird.

+0

Danke für so eine detaillierte Antwort. –

+2

Eine kleine Korrektur: Sammlung ist eigentlich keine Spezialisierung der Reduktion, es ist umgekehrt. Jede Reduktion kann als eine Sammlung ausgedrückt werden, während es keine allgemeine Möglichkeit gibt, eine Sammlung als Reduktion auszudrücken (oder zumindest nicht, ohne den Client-Code zur Verwaltung der Parallelität zu zwingen). In der Tat ist Reduktion eine spezialisierte Form der Sammlung. –