[JAVA] Punkt 46: Bevorzugen Sie nebenwirkungsfreie Funktionen in Streams

46. Wählen Sie Stream-Verarbeitung ohne Nebenwirkungen

Paradigmenwechsel aufgrund von Streams

Streams sind nicht nur APIs, sie sind Änderungen in einem Paradigma, das in der funktionalen Programmierung verwurzelt ist, und wir müssen uns an dieses Paradigma anpassen. Der wichtigste Teil des Stream-Paradigmas besteht darin, die Operation als Ergebnis einer Reihe von Transformationen durch reine Funktionen zu strukturieren. Eine reine Funktion ist eine Funktion, deren Ergebnis nur von ihrer Eingabe abhängt, nicht von veränderlichen Zuständen abhängt und andere Zustände nicht ändert. Um dieses Paradigma zu erreichen, müssen Zwischen- und Beendigungsoperationen im Stream frei von Nebenwirkungen sein.

Im Folgenden sehen wir uns ein Programm an, das die Häufigkeit der in einer Datei enthaltenen Wörter berechnet.

// Uses the streams API but not the paradigm--Don't do this!
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
    words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
    });
}

Es werden Streams, Lambda-Ausdrücke und Methodenreferenzen verwendet. Die Ergebnisse sind korrekt, die Stream-API wird jedoch nicht genutzt. Das Problem ist, dass wir den externen Status (Freq-Variable) in forEach ändern. Im Allgemeinen ist der Code, der das Ergebnis im Stream forEach nicht anzeigt, der Code, der den Status ändert, sodass es sich möglicherweise um fehlerhaften Code handelt.

Das Folgende ist, was es sein sollte.

// Proper use of streams to initialize a frequency table
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
    freq = words
        .collect(groupingBy(String::toLowerCase, counting()));
}

** forEach sollte verwendet werden, um das Ergebnis der Operation eines Streams anzuzeigen, nicht die Operation selbst. ** ** **

Verwendung des Sammlers

Der oben verbesserte Code verwendet einen Kollektor und ist für die Verwendung von Streams unerlässlich. Die Collectors-API verfügt über 39 Methoden und kann bis zu 5 Argumente aufnehmen, was beängstigend aussieht. Sie können diese API jedoch verwenden, ohne tief zu gehen. Ignorieren Sie zunächst die Collector-Oberfläche und überlegen Sie, eine Reduzierung durchzuführen (indem Sie die Elemente des Streams zu einem Objekt kombinieren).

Es gibt `toList ()`, toSet (), toCollection () `als Methoden, um die Elemente der Stream-Sammlung zu erstellen. Sie geben Liste, Satz und jede Sammlung zurück. Mit diesen werden die Top 10 der Frequenztabelle extrahiert.

// Pipeline to get a top-ten list of words from a frequency table
List<String> topTen = freq.keySet().stream()
    .sorted(comparing(freq::get).reversed())
    .limit(10)
    .collect(toList());

Angenommen, im obigen Code sollten Collectors-Mitglieder statisch importiert werden, um die Lesbarkeit der Stream-Pipeline zu gewährleisten.

toMap-Methode

Mit Ausnahme der oben genannten drei Methoden dienen die verbleibenden 36 Methoden hauptsächlich zur Zuordnung von Streams. Am einfachsten ist `toMap (keyMapper, valueMapper)`, das eine Funktion übernimmt, die den Stream zu einem Schlüssel macht, und eine Funktion, die den Stream zu einem Wert macht. Ein Beispiel ist wie folgt.

// Using a toMap collector to make a map from string to enum
private static final Map<String, Operation> stringToEnum =
    Stream.of(values()).collect(
        toMap(Object::toString, e -> e));

Der obige Code löst eine "IllegalStateException" aus, wenn mehrere identische Schlüssel vorhanden sind. Eine Möglichkeit, solche Konflikte zu verhindern, besteht darin, eine Zusammenführungsfunktion (`BinaryOperator <V>` wobei V der Kartenwerttyp ist) im Argument zu haben. Im folgenden Beispiel wird aus einem Stream von Albumobjekten eine Karte des meistverkauften Albums für jeden Künstler erstellt.

// Collector to generate a map from key to chosen element for key
Map<Artist, Album> topHits = albums.collect(
   toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));

Eine andere Verwendung für die `` `toMap``` -Methode, die drei Argumente akzeptiert, besteht darin, das letzte geschriebene Positiv zu machen, wenn ein Schlüsselkonflikt auftritt. Das Codebeispiel zu diesem Zeitpunkt lautet wie folgt.

// Collector to impose last-write-wins policy
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)

Es gibt auch eine `` `toMap``` Methode, die vier Argumente akzeptiert, und das vierte Argument gibt die Map an, die den Rückgabewert implementiert.

groupingBy-Methode

Zusätzlich zur `` toMap``` -Methode verfügt die Collectors-API auch über eine `groupingBy-Methode.groupingby```Die Methode erstellt eine Karte, die die Elemente basierend auf der Klassifikatorfunktion kategorisiert. Die Klassifikatorfunktion ist eine Funktion, die ein Element empfängt und die Kategorie (Map-Taste) dieses Elements zurückgibt. Dies ist dasjenige, das in dem in Punkt 45 dargestellten Anagrammprogramm verwendet wird.

words.collect(groupingBy(word -> alphabetize(word)))

Um einen Kollektor zurückzugeben, der eine Map generiert, deren Wert in der groupingBy-Methode nicht List ist, muss zusätzlich zur Klassifiziererfunktion der nachgeschaltete Kollektor angegeben werden. Wenn Sie im einfachsten Beispiel anSet an diesen Parameter übergeben, wird der Wert von Map anstelle von List festgelegt. Ein weiteres einfaches Beispiel für die Verwendung von zwei Argumenten für die groupingBy-Methode ist die Übergabe von count () an den nachgeschalteten Kollektor. count () kann die Anzahl der Elemente in jeder Kategorie aggregieren. Ein Beispiel hierfür ist die Häufigkeitstabelle am Anfang dieses Kapitels.

Map<String, Long> freq = words
        .collect(groupingBy(String::toLowerCase, counting()));

Mit der groupingBy-Methode, die drei Argumente akzeptiert, können Sie den Typ der zu generierenden Map angeben. (Die Map-Factory steht jedoch im zweiten Argument und der nachgeschaltete Kollektor im dritten.)

Andere Methoden

Die Zählmethode ist auf die Verwendung als Downstream-Kollektor spezialisiert, und ähnliche Funktionen können direkt von Stream bezogen werden. Daher sollten Aufrufe wie "collect (counting ())" nicht ausgeführt werden. In Collectors gibt es 15 weitere Methoden mit solchen Merkmalen, von denen 9 Methodennamen sind, die mit Summieren, Mitteln und Zusammenfassen beginnen. Darüber hinaus gibt es Methoden zum Reduzieren, Filtern, Zuordnen, FlatMapping und Sammeln von AndThen, die denen von Stream ähnlich sind.

Es gibt drei Collectors-Methoden, die ich noch nicht erwähnt habe, aber sie haben wenig mit Collectors zu tun. Die ersten beiden sind die Methoden `minBy``` und` maxBy```. Sie nehmen einen Komparator als Argument und geben die kleinsten und größten Elemente aus den Elementen des Streams zurück.

Die letzte Collectors-Methode ist "Joining", die nur den Stream der "CharSequence" -Instanz (z. B. "String") manipuliert. Beim Verbinden ohne Argumente wird ein Kollektor zurückgegeben, der nur die Elemente verbindet. Wenn Sie sich mit einem Argument verbinden, wird das Trennzeichen als Argument verwendet und ein Kollektor zurückgegeben, der das Trennzeichen zwischen den Elementen einfügt. Beim Verbinden mit 3 Argumenten werden zusätzlich zum Trennzeichen Präfix und Suffix als Argumente verwendet. Wenn das Trennzeichen ein Komma ist und das Präfix [und das Suffix] ist,

[came, saw, conquered].

so werden.

Recommended Posts

Punkt 46: Bevorzugen Sie nebenwirkungsfreie Funktionen in Streams
Punkt 80: Ziehen Sie Executoren, Aufgaben und Streams Threads vor
Azure funktioniert in Java
Punkt 45: Verwenden Sie Streams mit Bedacht
[Ruby] Ausnahmebehandlung in Funktionen
Erstellen Sie Azure-Funktionen in Java
Punkt 65: Schnittstellen der Reflexion vorziehen