2016-04-29 3 views
34

Wir wissen, dass Java 8 eine neue Stream-API einführt und java.util.stream.Collector die Schnittstelle ist, um zu definieren, wie der Datenstrom aggregiert/gesammelt wird.Warum ist die Java 8 'Collector' Klasse auf diese Weise entworfen?

Allerdings ist die Collector-Schnittstelle wie folgt gestaltet:

public interface Collector<T, A, R> { 
    Supplier<A> supplier(); 
    BiConsumer<A, T> accumulator(); 
    BinaryOperator<A> combiner(); 
    Function<A, R> finisher(); 
} 

Warum ist es nicht, wie die folgenden entwickelt?

public interface Collector<T, A, R> { 
    A supply(); 
    void accumulate(A accumulator, T value); 
    A combine(A left, A right); 
    R finish(A accumulator); 
} 

Letzteres ist viel einfacher zu implementieren. Was war die Überlegung, es als das frühere zu entwerfen?

+0

dreht sich alles um OOP und Struktur konsistent, einfach nur raten. – PSo

+3

Beachten Sie, dass Sie eine abstrakte Basisklasse implementieren können, die sich an das zweite Muster anpasst. – Thilo

+0

@Thilo Ich denke, dass sowohl das erste als auch das zweite Muster in jeder Richtung angepasst werden kann. Ich denke nur, dass der zweite intuitiver ist. – popcorny

Antwort

26

Eigentlich war es ursprünglich ähnlich wie Sie vorgeschlagen. Siehe the early implementation im Projekt Lambda-Repository (makeResult ist jetzt supplier). Es war später updated zum aktuellen Design. Ich glaube, das Grundprinzip einer solchen Aktualisierung besteht darin, Sammlerkombinatoren zu vereinfachen. Ich habe keine spezifische Diskussion zu diesem Thema gefunden, aber meine Vermutung wird durch die Tatsache unterstützt, dass Collector im selben Changeset erschien. Betrachten wir die Implementierung von Collectors.mapping:

public static <T, U, A, R> 
Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper, 
          Collector<? super U, A, R> downstream) { 
    BiConsumer<A, ? super U> downstreamAccumulator = downstream.accumulator(); 
    return new CollectorImpl<>(downstream.supplier(), 
           (r, t) -> downstreamAccumulator.accept(r, mapper.apply(t)), 
           downstream.combiner(), downstream.finisher(), 
           downstream.characteristics()); 
} 

Diese Implementierung nur accumulator Funktion neu zu definieren muss, so dass supplier, combiner und finisher wie es ist, so dass Sie keine zusätzlichen Indirektion haben, wenn supplier Aufruf combiner oder finisher: Sie rufen Sie einfach an direkt die Funktionen, die vom ursprünglichen Kollektor zurückgegeben wurden. Es ist noch wichtiger, mit collectingAndThen:

public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream, 
                  Function<R,RR> finisher) { 
    // ... some characteristics transformations ... 
    return new CollectorImpl<>(downstream.supplier(), 
           downstream.accumulator(), 
           downstream.combiner(), 
           downstream.finisher().andThen(finisher), 
           characteristics); 
} 

hier nur finisher geändert wird, aber original supplier, accumulator und combiner verwendet werden. Da accumulator für jedes Element aufgerufen wird, kann die Reduzierung der Indirektion sehr wichtig sein. Versuchen Sie, mapping und collectingAndThen mit Ihrem vorgeschlagenen Entwurf neu zu schreiben, und Sie werden das Problem sehen. Neue JDK-9-Kollektoren wie filtering und flatMapping profitieren ebenfalls vom aktuellen Design.

18

Composition is favored over inheritance.

Das erste Muster in Ihrer Frage ist eine Art von Modulkonfiguration. Die Implementierungen der Collector-Schnittstelle können verschiedene Implementierungen für Supplier, Accumulator usw. bereitstellen. Dies bedeutet Man kann Collector-Implementierungen aus einem vorhandenen Pool von Supplier, Accumulator usw. Implementierungen zusammenstellen. Dies hilft auch bei der Wiederverwendung, und zwei Collectors verwenden möglicherweise die gleiche Accumulator-Implementierung. Die Stream.collect() verwendet die angegebenen Verhaltensweisen.

Im zweiten Muster muss die Collector-Implementierung alle Funktionen selbst implementieren. Alle Arten von Variationen müssten die Elternimplementierung außer Kraft setzen. Nicht viel Spielraum für die Wiederverwendung, plus Code-Duplizierung, wenn zwei Kollektoren ähnliche Logik für einen Schritt haben, zum Beispiel Akkumulation.

+0

Danke für Ihre Antwort. Aber für meine Version ist es auch einfach, eine Helper-Wrapper-Version von Collector.of (....) durch diese Lambda-Funktionen zu implementieren. – popcorny

0

2 ähnliche Gründe

  • Funktionelle Zusammensetzung über combinators.(Hinweis: Sie können immer noch OO Zusammensetzung tun, als unter Punkt sehen)
  • Möglichkeit der Business-Logik Framing in prägnanten expressiven Code über Lambda-Ausdruck oder eine Methode Referenz, wenn Zuweisungsziel eine funktionale Schnittstelle ist.

    Funktionelle Zusammensetzung

    Sammler API ebnet einen Weg für die funktionale Zusammensetzung über Kombinatoren .i.e. bauen kleine/kleinste wiederverwendbare Funktionen auf und kombinieren einige davon oft auf interessante Weise zu einer erweiterten Funktion.

    prägnanter ausdruck Code

    Im Folgenden Funktionszeiger verwenden (Employee :: getSalary) die Funktionalität von Mapper von Employee-Objekt zu int zu füllen. summingInt füllt die Logik des Addierens von Ints, und daher kombinieren wir zusammen eine Summe von Gehältern, die in einer einzigen Zeile mit deklarativem Code geschrieben sind.

    // Berechne Summe der Gehälter der Mitarbeiter int total = employees.stream() .collect (Collectors.summingInt (Mitarbeiter :: getSalary)));

Verwandte Themen