Warum bietet Iterable <T> keine stream () - und parallelStream () -Methoden an?

241

Ich frage mich, warum die IterableSchnittstelle die stream()und parallelStream()Methoden nicht bereitstellt . Betrachten Sie die folgende Klasse:

public class Hand implements Iterable<Card> {
    private final List<Card> list = new ArrayList<>();
    private final int capacity;

    //...

    @Override
    public Iterator<Card> iterator() {
        return list.iterator();
    }
}

Es ist eine Implementierung einer Hand, da Sie beim Spielen eines Sammelkartenspiels Karten auf der Hand haben können.

Im Wesentlichen umschließt es a List<Card>, stellt eine maximale Kapazität sicher und bietet einige andere nützliche Funktionen. Es ist besser, es direkt als zu implementieren List<Card>.

Aus Bequemlichkeitsgründen dachte ich, es wäre schön, es so zu implementieren Iterable<Card>, dass Sie erweiterte for-Schleifen verwenden können, wenn Sie eine Schleife darüber erstellen möchten. (Meine HandKlasse bietet auch eine get(int index)Methode, daher Iterable<Card>ist die meiner Meinung nach gerechtfertigt.)

Die IterableSchnittstelle bietet Folgendes (ausgelassenes Javadoc):

public interface Iterable<T> {
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Jetzt können Sie einen Stream erhalten mit:

Stream<Hand> stream = StreamSupport.stream(hand.spliterator(), false);

Also auf die eigentliche Frage:

  • Warum gibt es Iterable<T>keine Standardmethoden, die implementiert werden, stream()und parallelStream()ich sehe nichts, was dies unmöglich oder unerwünscht machen würde?

Eine verwandte Frage, die ich gefunden habe, lautet jedoch: Warum implementiert Stream <T> Iterable <T> nicht?
Was seltsamerweise darauf hindeutet, es etwas anders herum zu machen.

Skiwi
quelle
1
Ich denke, das ist eine gute Frage für die Lambda-Mailingliste .
Edwin Dalorzo
Warum ist es seltsam, über einen Stream iterieren zu wollen? Wie sonst könnten Sie möglicherweise break;eine Iteration? (Ok, Stream.findFirst()könnte eine Lösung sein, aber das könnte nicht alle Anforderungen erfüllen ...)
glglgl

Antworten:

298

Dies war keine Auslassung; Im Juni 2013 wurde die EG-Liste ausführlich erörtert.

Die endgültige Diskussion der Expertengruppe basiert auf diesem Thread .

Während es "offensichtlich" schien (anfangs sogar für die Expertengruppe), stream()was Sinn machte Iterable, wurde die Tatsache, dass Iterablees so allgemein war, zu einem Problem, weil die offensichtliche Unterschrift:

Stream<T> stream()

war nicht immer das, was du wolltest. Bei einigen Dingen Iterable<Integer>würde beispielsweise die Stream-Methode lieber eine zurückgeben IntStream. Aber die stream()Methode so hoch in der Hierarchie zu platzieren, würde dies unmöglich machen. Stattdessen haben wir es wirklich einfach gemacht, Streamaus einem zu machen Iterable, indem wir eine spliterator()Methode bereitgestellt haben . Die Implementierung von stream()in Collectionist nur:

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

Jeder Client kann den gewünschten Stream von einem Iterablemit erhalten:

Stream s = StreamSupport.stream(iter.spliterator(), false);

Am Ende schlossen wir , dass das Hinzufügen stream()zu Iterableeinem Fehler wäre.

Brian Goetz
quelle
8
Ich sehe zunächst einmal vielen Dank für die Beantwortung der Frage. Ich bin immer noch neugierig, warum ein Iterable<Integer>(ich denke, Sie sprechen davon?) Einen zurückgeben möchte IntStream. Wäre das iterable dann nicht lieber ein PrimitiveIterator.OfInt? Oder meinst du vielleicht einen anderen Anwendungsfall?
Skiwi
139
Ich finde es seltsam, dass die obige Logik angeblich auf Iterable angewendet wurde (ich kann stream () nicht haben, weil jemand möchte, dass es IntStream zurückgibt), während nicht gleich viel darüber nachgedacht wurde, Collection genau dieselbe Methode hinzuzufügen ( Ich möchte vielleicht, dass der Stream () meiner Sammlung <Integer> auch IntStream zurückgibt.) Ob er bei beiden vorhanden war oder bei beiden nicht vorhanden war, die Leute wären wahrscheinlich gerade mit ihrem Leben weitergekommen, aber weil er bei einem vorhanden ist und bei einem nicht vorhanden ist das andere, es wird eine ziemlich
krasse
6
Brian McCutchon: Das macht für mich mehr Sinn. Es hört sich so an, als hätten die Leute es einfach satt zu streiten und beschlossen, auf Nummer sicher zu gehen.
Jonathan Locke
44
Obwohl dies sinnvoll ist, gibt es einen Grund, warum es keine alternative statische Aufladung gibt Stream.of(Iterable), die die Methode zumindest durch Lesen der API-Dokumentation einigermaßen auffindbar machen würde - als jemand, der nie wirklich mit den Interna von Streams gearbeitet hat, würde ich nie selbst sah auf StreamSupport, die in der Dokumentation beschrieben wird als „Low-Level - Operationen“ bietet die „vor allem für Bibliothek Autoren“ sind.
Jules
9
Ich stimme Jules voll und ganz zu. Anstelle von StreamSupport.stream (iter.spliterator (), false) sollte eine statische Methode Stream.of (Iteratable iter) oder Stream.of (Iterator iter) hinzugefügt werden.
user_3380739
23

Ich habe in mehreren Lambda-Mailinglisten des Projekts Nachforschungen angestellt und einige interessante Diskussionen gefunden.

Ich habe bisher keine zufriedenstellende Erklärung gefunden. Nachdem ich das alles gelesen hatte, kam ich zu dem Schluss, dass es nur eine Auslassung war. Aber Sie können hier sehen, dass es im Laufe der Jahre während des Entwurfs der API mehrmals diskutiert wurde.

Lambda Libs Spec Experts

Ich habe eine Diskussion darüber in der Mailingliste von Lambda Libs Spec Experts gefunden :

Unter Iterable / Iterator.stream () sagte Sam Pullara:

Ich habe mit Brian zusammengearbeitet, um herauszufinden, wie die Limit / Substream-Funktionalität [1] implementiert werden kann, und er schlug vor, dass die Konvertierung auf Iterator der richtige Weg ist. Ich hatte über diese Lösung nachgedacht, aber keinen offensichtlichen Weg gefunden, einen Iterator in einen Stream umzuwandeln. Es stellt sich heraus, dass es dort drin ist. Sie müssen nur zuerst den Iterator in einen Spliterator und dann den Spliterator in einen Stream konvertieren. Dies bringt mich dazu, noch einmal zu überlegen, ob diese direkt an einem von Iterable / Iterator oder an beiden hängen sollen.

Mein Vorschlag ist, es zumindest auf Iterator zu haben, damit Sie sich sauber zwischen den beiden Welten bewegen können, und es wäre auch leicht zu entdecken, anstatt es tun zu müssen:

Streams.stream (Spliterators.spliteratorUnknownSize (Iterator, Spliterator.ORDERED))

Und dann antwortete Brian Goetz :

Ich denke, Sams Punkt war, dass es viele Bibliotheksklassen gibt, die Ihnen einen Iterator geben, aber Sie nicht unbedingt Ihren eigenen Spliterator schreiben lassen. Sie können also nur den Stream (spliteratorUnknownSize (Iterator)) aufrufen. Sam schlägt vor, dass wir Iterator.stream () definieren, um dies für Sie zu tun.

Ich möchte die Methoden stream () und spliterator () für Bibliotheksschreiber / fortgeschrittene Benutzer beibehalten.

Und später

"Da das Schreiben eines Spliterators einfacher ist als das Schreiben eines Iterators, würde ich es vorziehen, nur einen Spliterator anstelle eines Iterators zu schreiben (Iterator ist so 90er Jahre :)"

Sie verpassen jedoch den Punkt. Es gibt Myriaden von Klassen gibt, die bereits Hand Sie einen Iterator. Und viele von ihnen sind nicht spliterator-fähig.

Frühere Diskussionen in der Lambda-Mailingliste

Dies ist möglicherweise nicht die Antwort, nach der Sie suchen, aber in der Projekt-Lambda-Mailingliste wurde dies kurz besprochen. Vielleicht hilft dies, eine breitere Diskussion zu diesem Thema anzuregen.

In den Worten von Brian Goetz unter Streams von Iterable :

Zurück gehen...

Es gibt viele Möglichkeiten, einen Stream zu erstellen. Je mehr Informationen Sie zur Beschreibung der Elemente haben, desto mehr Funktionalität und Leistung kann Ihnen die Streams-Bibliothek bieten. In der Reihenfolge der wenigsten bis zu den meisten Informationen sind dies:

Iterator

Iterator + Größe

Spliterator

Spliterator, der seine Größe kennt

Spliterator, der seine Größe kennt und außerdem weiß, dass alle Sub-Splits ihre Größe kennen.

(Einige mögen überrascht sein, dass wir Parallelität sogar aus einem dummen Iterator extrahieren können, wenn Q (Arbeit pro Element) nicht trivial ist.)

Wenn Iterable eine stream () -Methode hätte, würde es nur einen Iterator mit einem Spliterator ohne Größeninformationen umschließen. Die meisten Dinge, die iterierbar sind, haben jedoch Größeninformationen . Das heißt, wir bedienen mangelhafte Streams. Das ist nicht so gut

Ein Nachteil der von Stephen hier beschriebenen API-Praxis, Iterable anstelle von Collection zu akzeptieren, besteht darin, dass Sie Dinge durch eine "kleine Pipe" zwingen und daher Größeninformationen verwerfen, wenn dies nützlich sein könnte. Das ist in Ordnung, wenn Sie nur für jedes tun, aber wenn Sie mehr tun möchten, ist es besser, wenn Sie alle gewünschten Informationen beibehalten können.

Die von Iterable bereitgestellte Standardeinstellung wäre in der Tat beschissen - sie würde die Größe verwerfen, obwohl die überwiegende Mehrheit der Iterables diese Informationen kennt.

Widerspruch?

Es sieht jedoch so aus, als ob die Diskussion auf den Änderungen basiert, die die Expertengruppe am ursprünglichen Design von Streams vorgenommen hat, das ursprünglich auf Iteratoren basierte.

Trotzdem ist es interessant festzustellen, dass in einer Schnittstelle wie Collection die Stream-Methode wie folgt definiert ist:

default Stream<E> stream() {
   return StreamSupport.stream(spliterator(), false);
}

Dies könnte genau derselbe Code sein, der in der Iterable-Schnittstelle verwendet wird.

Deshalb habe ich gesagt, dass diese Antwort wahrscheinlich nicht zufriedenstellend, aber dennoch interessant für die Diskussion ist.

Nachweis von Refactoring

Wenn Sie mit der Analyse in der Mailingliste fortfahren, sieht es so aus, als ob sich die splitIterator-Methode ursprünglich in der Collection-Oberfläche befand, und irgendwann im Jahr 2013 wurde sie auf Iterable verschoben.

Ziehen Sie splitIterator von Collection nach Iterable .

Schlussfolgerung / Theorien?

Dann besteht die Möglichkeit, dass das Fehlen der Methode in Iterable nur eine Auslassung ist, da es so aussieht, als hätten sie auch die Stream-Methode verschieben sollen, wenn sie den splitIterator von Collection nach Iterable verschoben haben.

Wenn es andere Gründe gibt, sind diese nicht offensichtlich. Hat noch jemand andere Theorien?

Edwin Dalorzo
quelle
Ich schätze Ihre Antwort, aber ich bin mit der Begründung dort nicht einverstanden. Im Moment , dass Sie das außer Kraft setzen spliterator()der Iterable, dann werden alle Ausgaben dort festgelegt sind, und Sie können trivialer implementieren stream()und parallelStream()..
skiwi
@skiwi Deshalb habe ich gesagt, das ist wahrscheinlich nicht die Antwort. Ich versuche nur, der Diskussion etwas hinzuzufügen, weil es schwierig ist zu wissen, warum die Expertengruppe die Entscheidungen getroffen hat, die sie getroffen haben. Ich denke, wir können nur versuchen, in der Mailingliste etwas Forensik zu betreiben und zu prüfen, ob wir irgendwelche Gründe finden können.
Edwin Dalorzo
1
@skiwi Ich habe andere Mailinglisten überprüft und mehr Beweise für die Diskussion und vielleicht einige Ideen gefunden, die helfen, eine Diagnose zu theoretisieren.
Edwin Dalorzo
Vielen Dank für Ihre Bemühungen, ich sollte wirklich lernen, wie man diese Mailinglisten effizient aufteilt. Es wäre hilfreich, wenn sie auf eine ... moderne Art und Weise wie ein Forum oder so etwas visualisiert werden könnten, da das Lesen von E-Mails im Klartext mit Zitaten nicht gerade effizient ist.
Skiwi
6

Wenn Sie die Größe kennen, die Sie verwenden können java.util.Collection, um die stream()Methode bereitzustellen:

public class Hand extends AbstractCollection<Card> {
   private final List<Card> list = new ArrayList<>();
   private final int capacity;

   //...

   @Override
   public Iterator<Card> iterator() {
       return list.iterator();
   }

   @Override
   public int size() {
      return list.size();
   }
}

Und dann:

new Hand().stream().map(...)

Ich hatte das gleiche Problem und war überrascht, dass meine IterableImplementierung AbstractCollectiondurch einfaches Hinzufügen der size()Methode sehr einfach auf eine Implementierung erweitert werden konnte (zum Glück hatte ich die Größe der Sammlung :-)

Sie sollten auch in Betracht ziehen, zu überschreiben Spliterator<E> spliterator().

Du tust
quelle