Betrachten wir die Bedeutung von "Stream" und "Collect" in der Stream-API von Java.

Einführung

Java hat bereits vor langer Zeit eine funktionale Programmiertechnik namens Stream API eingeführt. Funktionale Programmierung ist bereits weit verbreitet und Java ist ein ziemlich später Neuling, wird jedoch immer noch eingeführt, da es den Vorteil hat, bei gut verwendeter Verwendung gut lesbaren Code effizient schreiben zu können. nachdenken über. (Referenz: "[Funktionale Programmierung für mittelmäßige Programmierer](https://anopara.net/2016/04/14/%E5%B9%B3%E5%87%A1%E3%81%AA%E3%" 83% 97% E3% 83% AD% E3% 82% B0% E3% 83% A9% E3% 83% 9E% E3% 81% AB% E3% 81% A8% E3% 81% A3% E3% 81% A6% E3% 81% AE% E9% 96% A2% E6% 95% B0% E5% 9E% 8B% E3% 83% 97% E3% 83% AD% E3% 82% B0% E3% 83% A9% E3% 83% 9F% E3% 83% B3 /) ")

Neulich hörte ich jedoch in einem Vortrag, dass "Javas Erfassungsoperation (Stream API) erzwungener ist als reine Funktionssprachen wie Haskell, und ich möchte sie nicht verwenden." Insbesondere für funktionale Sprachen reicht `list.map (...)` aus, für Java jedoch nacheinander.

list.stream().map(/* */).collect(Collectors.toList())


```mögen```stream()```Ich dachte, dass es ein redundanter Schreibstil sein würde, weil es notwendig war, ihn dazwischen zu setzen.

 Ich bevorzuge die Stream-API, weil sie besser als nichts ist, aber ich habe mich gefragt, ob es einige Aspekte gibt, die Leute, die funktionale Sprachen mögen, nicht akzeptieren.

 Warum verwendet Java keinen einfachen Schreibstil wie `` `list.map (...)` ``, sondern ruft `` `stream ()` `` einzeln auf und konvertiert ihn in einen anderen Typ namens Stream. Haben Sie dann versucht, erneut mit `` `collect`` `zu konvertieren? Java hat eine Kultur des sorgfältigen Sprachdesigns, und es muss einen Grund für die Vor- und Nachteile des Ergebnisses geben. Ich denke, dafür gibt es zwei Hauptgründe.

 1. Bewertung verzögern
 2. Objektorientierte Einschränkungen

 Im Folgenden werde ich meine Gedanken im Detail geben.

## Was ist eine Verzögerungsbewertung?
 Wenn Sie einen Erfassungsvorgang ausführen, ist es im Allgemeinen sinnlos, die Erfassung nacheinander neu zu erstellen, und es bestehen Bedenken hinsichtlich der Leistung.
 Um dies zu verhindern, sollte der Code, obwohl er eine allmähliche Änderung in der Sammlung zu sein scheint, am Ende tatsächlich in großen Mengen generiert werden. Diese Berechnungsmethode, bei der der Wert erst berechnet wird, wenn er benötigt wird, wird als ** Verzögerungsauswertung ** bezeichnet.
 Zum Beispiel

```java
List<String> list = Arrays.asList("foo", "bar", "hoge", "foo", "fuga");
list.stream()
  .filter(s -> s.startsWith("f"))
  .map(s -> s.toUpperCase())
  .collect(Collectors.toSet()); // ["FOO", "FUGA"]

So wie das

Beim Ausführen des Erfassungsvorgangs wird beim Aufrufen von "Filter" keine neue Sammlung mit 3 Elementen erstellt. Die eigentliche Sammlung wird generiert, wenn `collect (Collectors.toSet ())` zuletzt aufgerufen wird.

Das Synonym für verzögerte Bewertung ist ** regelmäßige Bewertung **. Es ist eine Methode, um an diesem Punkt zu berechnen, auch wenn der Wert nicht erforderlich ist. Dies ist normalerweise häufiger.

Nachteile der Verzögerungsbewertung

Obwohl es Vorteile gibt, gibt es einige unerwartete Fallstricke, wenn Sie sie nicht mit Vorsicht verwenden. Das Folgende ist ein Beispiel (obwohl es nicht sehr vorzuziehen ist), dass die Verzögerungsbewertung eine Diskrepanz zwischen dem Erscheinungsbild und der Erkennung des tatsächlichen Ausführungsergebnisses verursachen kann.

//Definition der Eingabe- / Ausgabedaten
List<String> input = Arrays.asList("foo", "bar", "hoge", "foo", "fuga");
List<String> copy1 = new ArrayList<>();
List<String> copy2 = new ArrayList<>();

//Starten Sie den Erfassungsvorgang.Filter ausführen
Stream<String> stream = input.stream()
    .filter(s -> {
        copy1.add(s);
        return s.startsWith("f");
    });
System.out.println(copy1.size()); //Zu diesem Zeitpunkt wird die Filteroperation nicht tatsächlich ausgewertet, sodass copy1 leer bleibt und 0 ausgegeben wird.
System.out.println(copy2.size()); //Natürlich bleibt copy2 leer, also wird 0 ausgegeben

//Führen Sie dann die Karte der Erfassungsoperation aus
stream = stream
    .map(s -> {
        copy2.add(s);
        return s.toUpperCase();
    });
System.out.println(copy1.size());  //Zu diesem Zeitpunkt wurde die Filteroperation noch nicht ausgewertet, daher wird 0 ausgegeben.
System.out.println(copy2.size()); //In ähnlicher Weise wird die Kartenoperation nicht ausgewertet, so dass 0 ausgegeben wird.

stream.collect(Collectors.toList());
System.out.println(copy1.size()); // stream.5 wird ausgegeben, weil der Filter schließlich durch Sammeln ausgewertet wird
System.out.println(copy2.size()); //In ähnlicher Weise wird die Kartenoperation ausgewertet, so dass 3 ausgegeben wird.

Auf den ersten Blick scheint der obige Code die Größe von "copy1", "copy2" zu erhöhen, wenn "filter", "map", " Tatsächlich nimmt die Größe von "copy1" und "copy2" zu, wenn "stream.collect" aufgerufen wird. Auf diese Weise besteht bei einer Diskrepanz zwischen dem Erscheinungsbild und dem tatsächlichen Bewertungszeitpunkt das Risiko, dass das Debuggen schwierig ist, wenn etwas schief geht, und es schwierig ist, die Ursache zu identifizieren.

Wie man balanciert

Bei einer verzögerten Bewertung besteht die Gefahr, dass bei Missbrauch komplexe Fehler eingebettet werden. Wenn Sie jedoch überhaupt keine Verzögerungsbewertung verwenden, besteht die Gefahr, dass Sammlungen verschwendet werden und die Leistung beeinträchtigt wird.

Im Fall von Java möchten wir das letztere Risiko vermeiden, da normalerweise die Möglichkeit in Betracht gezogen wird, eine große Datenmenge im Back-End zu verarbeiten. Daher müssen wir eine verzögerte Auswertung einführen. Darüber hinaus wäre eine verzögerte Bewertung vorzuziehen, die natürlich (?) Ist, damit die reguläre Bewertung nicht unbeabsichtigt verwendet wird.

Es ist jedoch riskant, Verzögerungsbewertungen auf eine Vielzahl von Java-Standardfunktionen anzuwenden. Daher halte ich es für sinnvoll, die Verzögerungsbewertung auf einen bestimmten Typ zu beschränken, damit die Verzögerungsbewertung bei anderen Typen nicht verwendet wird.

Aussehen von "Stream"

[Stream](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%A0_(%E3%) 83% 97% E3% 83% AD% E3% 82% B0% E3% 83% A9% E3% 83% 9F% E3% 83% B3% E3% 82% B0))

Ein Stream ist ein abstrakter Datentyp, der Daten als "fließendes Ding" betrachtet, die eingehenden Daten eingibt und die auslaufenden Daten als Ausgabe behandelt.

Wie bereits erwähnt, heißt der einzige Typ, der eine Verzögerungsauswertung durchführen kann, "** Stream **". Und der Name der Erfassungsoperations-API lautet "Stream-API". Der Punkt ist, dass Sie, wenn Sie die Erfassungsoperation ausführen möchten, "stream ()" verwenden können, wie der Name schon sagt.

Wir glauben, dass wir damit versucht haben, verzögerte Bewertungen zu erzwingen und das Risiko einer Leistungsverschlechterung aufgrund formeller Bewertungen zu vermeiden.

Objektorientierte Einschränkungen

Ein weiterer Grund über "Stream" ist die objektorientierte Einschränkung. (Streng genommen entspricht es eher der Einschränkung von Handhabungstypen als objektorientiert, aber da Funktionstypen und objektorientiert häufig gegenübergestellt werden, wird hier der Begriff "objektorientiert" verwendet.) Angenommen, Sie haben eine Standardzuordnungsmethode für den Listentyp definiert.

interface List<E> {
    default <R> List<R> map(Function<? super E, ? extends R> mapper) {
        List<R> result = new ArrayList<>();
        for (E elem : this) {
            result.add(mapper.apply(elem));
        }
        return result;
    }
}

In diesem Fall können Sie den Listentyp vorerst wie "list.map (...)" konvertieren. Wenn Sie Methoden wie Filter und andere Sammlungstypen auf dieselbe Weise implementieren, können Sie Sammlungen auf präzise Weise konvertieren, ohne den Stream zu durchlaufen.

Dieses Verfahren weist jedoch schwerwiegende Nachteile auf. Es ist ein anderer Sammlungstyp als die Standardbibliothek.

Angenommen, ein Entwickler erstellt eine MyList, die die List-Schnittstelle implementiert und eine eindeutige Methode, doSomething, hinzufügt. Wenn der MyList-Typ mit der obigen Methode in eine Map konvertiert wird, handelt es sich nach der Konvertierung um einen anderen Listentyp, und doSomething kann nicht aufgerufen werden.

MyList<> mylist = new MyList<>();
//Unterlassung
mylist.doSomething(); //OK
myList.map(x -> new AnotherType(x)).doSomething(); //Kompilierungsfehler

Dies ist eine Herausforderung, wenn funktionale Programmierung in eine objektorientierte Sprache integriert wird. Allerdings sehe ich solche Fälle nicht wirklich, so dass ich mir darüber keine Sorgen machen muss, aber es ist wahrscheinlich aufgrund der Natur der Java-Sprache inakzeptabel.

Für Scala wurde diese Schwierigkeit durch implizite Typauflösung überwunden. Es wird in dem Buch beschrieben, das in Anhang A unten vorgestellt wird. Schauen Sie also bitte vorbei, wenn Sie interessiert sind.

Auftritt von "Collector"

Aus den oben genannten Gründen müssen Sie beim Starten des Konvertierungsvorgangs einer Sammlung etwas anderes als die ursprüngliche Sammlung neu generieren. Es liegt in der Verantwortung des Aufrufers und nicht der Bibliothek, die Sammlung anzugeben. "** Collector **" ist dafür verantwortlich, und Stream.collect gibt an, in welchen Sammlungstyp der Aufrufer konvertieren soll. Der folgende Quellcode ist die Implementierung von Collectors.toList.

public static <T>
Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

Wenn Sie für den von Ihnen selbst erstellten Sammlungstyp MyList eine Methode zum Generieren einer Collector-Instanz auf dieselbe Weise vorbereiten, können Sie von MyList nach MyList konvertieren. Auf diese Weise können Sammlungstypen, die vor der Einführung der Stream-API erstellt wurden, ohne größere Änderungen an der Stream-API verwendet werden.

Übrigens der Grund, warum keine toList-Methode für den Stream-Typ erstellt wurde

Obwohl die Sichtbarkeit verbessert werden kann, indem die Konvertierung der Sammlung in den Collector-Typ zusammengefasst wird, werden der Listentyp usw. im täglichen Leben häufig angezeigt. Zumindest denke ich, dass es in Ordnung ist, so präzise wie "stream.toList ()" anstelle von "stream.collect (Collectors.toList ())" zu schreiben. Ich denke, der Grund dafür ist wahrscheinlich eine Typabhängigkeit. Der Punkt ist, dass es wichtig ist, den Sammlungstyp auf den Stream-Typ zu verweisen, aber ich denke, dass der Grund dafür ist, dass das Verweisen auf den Sammlungstyp vom Stream-Typ als Typentwurf nicht vorzuziehen ist, da es sich um eine gegenseitige Referenz handelt.

Lassen Sie uns die Magie singen und sicher anwenden

Wie oben erwähnt, ist das Ergebnis der Berücksichtigung verschiedener Salden und Konsistenz `list.stream (). Map (/ * * /). Collect (Collectors.toList ())`, eine Erfassungsoperation, die als redundant angesehen werden kann. Ich denke, dass es sich in der Form von niedergelassen hat.

In gewissem Sinne denke ich, dass es eine ** sehr Java-ähnliche Schlussfolgerung ** ist.

Es scheint, dass es ein mysteriöses Projekt auf der Welt gibt, bei dem Sie die Stream-API nicht verwenden sollten, da sie bei der Verwendung von Java gefährlich ist. Da sie jedoch aus Sicherheitsgründen erstellt wurde, ist es schwierig, sie normal zu verwenden. Du musst nicht gehen. Wenn Sie "stream", "collect" gemäß dem Standard singen, tritt kein Problem auf, es sei denn, etwas geht schief.

abschließend

Mit Ausnahme derjenigen, die sich speziell mit rein funktionalen Sprachen befassen, denke ich, dass redundante Beschreibungen gut vertragen werden. Wenn Sie die funktionale Programmierung gut nutzen können, können Sie gut lesbaren Code effizient schreiben. Wenn Sie es noch nicht benutzt haben, probieren Sie es bitte aus. (Referenz: Einführung in die Java Stream-API)

Ergänzung A. Andere Sprachen als Java

Ich bin nicht sehr vertraut damit, aber ich werde beschreiben, was andere Sprachen als Referenz anbieten.

C# LINQ in C # ist eine faule Bewertung wie Java. Im Gegensatz zu Java müssen Sie stream () nicht aufrufen, um es zu starten, und Sie müssen häufig nur ToList aufrufen, beispielsweise beim Sammeln, sodass es viel prägnanter und bequemer als Java ist. (Referenz: "[Verschiedene Hinweise] LINQ- und Verzögerungsbewertung") (Referenz: "C # er weiß natürlich !? Vor- und Nachteile der LINQ-Verzögerungsbewertung")

Scala Scala ist auch eine funktionale Sprache, und es ist möglich, die Verzögerungsbewertung und die regelmäßige Bewertung ordnungsgemäß zu verwenden. Zum Beispiel kann `list.map (...)` verwendet werden, um durch regelmäßige Auswertung in eine andere Sammlung zu konvertieren. Es ist auch möglich, durch verzögerte Auswertung in Form einer Ansicht, erzwingen wie `` `list.view.map (...) .filter (...) .force```, in eine andere Sammlung zu konvertieren. (Referenz: "Generieren Sie einen Generator mit Scala und bewerten Sie die Verzögerung")

Darüber hinaus scheint es eine Zeit gegeben zu haben, in der es schwierig war, zwischen regelmäßiger und verzögerter Bewertung zu unterscheiden, was zu Verwirrung führte, aber zu einem Zeitpunkt wurde die Grenze geklärt.

Es scheint, dass nur diese beiden Typen als Ziele für die Verzögerungsbewertung aussortiert wurden. Was Scala betrifft, enthält das Buch "Scala Scalable Programming" verschiedene erschreckende Details. Wenn Sie also interessiert sind, zögern Sie bitte nicht, uns zu kontaktieren. Bitte schauen Sie sich das an.

JavaScript Im JavaScript-Standard gibt es keine Verzögerungsauswertung. Array.prototype verfügt über Standard-APIs für Erfassungsvorgänge wie Map und Filter, die jedoch alle bewertet sind. Möglicherweise basiert dies auf der Annahme, dass auf der Clientseite verwendetes JavaScript keine großen Datenmengen verarbeitet, sodass eine Verzögerungsauswertung als Standardausrüstung nicht erforderlich ist.

Haskell Haskell ist auch eine rein funktionale Sprache, und es scheint, dass sich die Haarfarbe von den oben aufgeführten unterscheidet. Während normale Sprachen auf einer regulären Bewertung basieren, basiert Haskell auf einer verzögerten Bewertung. Daher scheint es nichts mit dem Gleichgewicht zwischen der regelmäßigen Bewertung und der verzögerten Bewertung zu tun zu haben, das uns in diesem Artikel wichtig ist. (Referenz: "Regelmäßige Bewertung und Verzögerungsbewertung (Details)")

Andere als die oben genannten (PHP, Ruby, Python, etc ...)

Ich werde es bald untersuchen.

Ergänzung B. Option zur Verwendung einer anderen Bibliothek

Zusätzlich zum Java-Standard gibt es eine Bibliothek zur Manipulation von Sammlungen mit dem Namen Eclipse-Sammlungen. Auf diese Weise können Sie genau beschreiben, was mit der Stream-API redundant ist. (Referenz: "Ich habe Eclipse-Sammlungen berührt") (Referenz: "Eclipse Collections Cheet Sheet")

Darüber hinaus ist ImmutableList mit einer unveränderlichen List-Schnittstelle eine Bibliothek mit einer tieferen Farbe der Funktionsmethode. Wenn Sie mehr funktionale Erfassungsvorgänge als die Stream-API ausführen möchten, ist die Implementierung meiner Meinung nach eine Option.

Wenn Sie jedoch die Stream-API vollständig durch Eclipse-Sammlungen ersetzen möchten, müssen Sie viel Arbeit leisten. Bei der Einführung die Geschichte der Site, auf der die Einführung tatsächlich durchgeführt wurde "[Framework-Unterstützung für die Installation von Eclipse-Sammlungen im Feld](https://speakerdeck.com/jflute/how-unext-took-in-eclipse- collection-in-fw) “wird hilfreich sein.

Recommended Posts

Betrachten wir die Bedeutung von "Stream" und "Collect" in der Stream-API von Java.
Das Designkonzept der Datums- und Uhrzeit-API von Java ist interessant
Versuchen Sie es mit der Stream-API in Java
Hinweise zur Stream-API und zu SQL von Java
Zum Verständnis von Karte und Flatmap in Stream (1)
Zusammenfassung der gegenseitigen Konvertierung zwischen Groovys Standard-Groovy-Methoden und Javas Stream-API
[Für Anfänger] DI ~ Die Grundlagen von DI und DI im Frühjahr ~
Verwenden Sie Java-Lambda-Ausdrücke außerhalb der Stream-API
Ich war seltsamerweise süchtig danach, Javas Stream-API mit Scala zu verwenden
Erstellen Sie weitere Registerkarten und Fragmente im Fragment von BottomNavigationView
Java Stream API in 5 Minuten
Memo der JSUG-Studiengruppe 2018 Teil 2 - Bemühungen um Arbeitsspezifikationen im Frühjahr und in der API-Ära
Implementieren wir die Bedingung, dass der Umfang und das Innere der Ougi-Form in Java enthalten sind [Teil 2]
Implementieren wir die Bedingung, dass der Umfang und das Innere der Ougi-Form in Java enthalten sind [Teil 1]
Lassen Sie uns etwas tiefer in die Stream-API eintauchen, von der ich verstehe, dass sie neu geschrieben wurde.
[Java] Ordnen Sie die Daten des vergangenen Montags und Sonntags der Reihe nach an
Die Frühjahrsvalidierung war in der Reihenfolge von Form und BindingResult wichtig
Lassen Sie uns eine TODO-App in Java 5 erstellen. Schalten Sie die Anzeige von TODO um
Detailliertes Verhalten der Stream API Stateful-Zwischenoperation und der Kurzschlussbeendigungsoperation
Nutzen Sie entweder für die individuelle Ausnahmebehandlung in der Java Stream-API
Erste Schritte mit Doma-using Logical Operators wie AND und OR in der WHERE-Klausel der Criteria-API
Dies und das von JDK
Analysieren der COTOHA-API-Syntaxanalyse in Java
Reihenfolge der Verarbeitung im Programm
[Rails] Unterschied im Verhalten zwischen Delegat und has_many-through bei Eins-zu-Eins-zu-Viele
Die Geschichte, zu vergessen, eine Datei in Java zu schließen und zu scheitern
Stellen Sie die Anzahl der Sekunden für den schnellen Vor- und Rücklauf in ExoPlayer ein
Empfangen Sie die API-Antwort im XML-Format und rufen Sie das DOM des angegebenen Tags ab
Bestätigung und Umgestaltung des Flusses von der Anfrage zum Controller in [httpclient]
[Einführung in Ruby] Über die Rolle von true und break in der while-Anweisung
So konvertieren Sie ein Array von Strings mit der Stream-API in ein Array von Objekten
(In 1 Minute bestimmen) Wie verwende ich leer ?, Leer? Und präsent?
Dies und das der Implementierung der zeitlichen Beurteilung von Daten in Java
So ändern Sie die maximale und maximale Anzahl von POST-Daten in Spark
Berechnen Sie den Prozentsatz von "gut", "normal" und "schlecht" im Fragebogen mit SQL
Finden Sie das Maximum und Minimum der fünf in Java eingegebenen Zahlen
Ihnen, denen im Feld "Keine Stream-API verwenden" mitgeteilt wurde
Zeigen Sie die dreidimensionale Struktur von DNA und Protein in Ruby-im Fall von GR.rb an