Das Java 8 Stream API-Tutorial

Das Java 8 Stream API Tutorial

1. Überblick

In diesem ausführlichen Tutorial werden wir die praktische Verwendung von Java 8-Streams von der Erstellung bis zur parallelen Ausführung erläutern.

Um dieses Material zu verstehen, müssen die Leser über Grundkenntnisse in Java 8 (Lambda-Ausdrücke,Optional,-Methodenreferenzen) und in der Stream-API verfügen. Wenn Sie mit diesen Themen nicht vertraut sind, lesen Sie bitte unsere vorherigen Artikel -New Features in Java 8 undIntroduction to Java 8 Streams.

Weitere Lektüre:

Lambda-Ausdrücke und funktionale Schnittstellen: Tipps und bewährte Methoden

Tipps und bewährte Methoden zur Verwendung von Java 8-Lambdas und funktionalen Schnittstellen.

Read more

Leitfaden für Java 8-Collectors

Der Artikel behandelt Java 8-Collectors und zeigt Beispiele für integrierte Collectors sowie das Erstellen benutzerdefinierter Collectors.

Read more

2. Stream-Erstellung

Es gibt viele Möglichkeiten, eine Stream-Instanz verschiedener Quellen zu erstellen. Nach der Erstellung ermöglicht die Instanzwill not modify its source,die Erstellung mehrerer Instanzen aus einer einzigen Quelle.

2.1. Leerer Stream

Die Methodeempty() sollte verwendet werden, wenn ein leerer Stream erstellt wird:

Stream streamEmpty = Stream.empty();

Es ist häufig der Fall, dass die Methodeempty() bei der Erstellung verwendet wird, um zu vermeiden, dassnull für Streams ohne Element zurückgegeben werden:

public Stream streamOf(List list) {
    return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}

2.2. Strom vonCollection

Stream kann auch von jedem Typ vonCollection (Collection, List, Set) erstellt werden:

Collection collection = Arrays.asList("a", "b", "c");
Stream streamOfCollection = collection.stream();

2.3. Array-Stream

Array kann auch eine Quelle eines Streams sein:

Stream streamOfArray = Stream.of("a", "b", "c");

Sie können auch aus einem vorhandenen Array oder einem Teil eines Arrays erstellt werden:

String[] arr = new String[]{"a", "b", "c"};
Stream streamOfArrayFull = Arrays.stream(arr);
Stream streamOfArrayPart = Arrays.stream(arr, 1, 3);

2.4. Stream.builder()

When builder is usedthe desired type should be additionally specified in the right part of the statement, andernfalls erstellt die Methodebuild() eine Instanz vonStream<Object>:

Stream streamBuilder =
  Stream.builder().add("a").add("b").add("c").build();

2.5. Stream.generate()

Die Methodegenerate() akzeptiertSupplier<T> für die Elementgenerierung. Da der resultierende Stream unendlich ist, sollte der Entwickler die gewünschte Größe angeben, da sonst diegenerate()-Methode funktioniert, bis das Speicherlimit erreicht ist:

Stream streamGenerated =
  Stream.generate(() -> "element").limit(10);

Der obige Code erstellt eine Folge von zehn Zeichenfolgen mit dem Wert -“element”.

2.6. Stream.iterate()

Eine andere Möglichkeit, einen unendlichen Stream zu erstellen, ist die Verwendung deriterate()-Methode:

Stream streamIterated = Stream.iterate(40, n -> n + 2).limit(20);

Das erste Element des resultierenden Streams ist ein erster Parameter deriterate()-Methode. Für die Erstellung jedes folgenden Elements wird die angegebene Funktion auf das vorherige Element angewendet. Im obigen Beispiel ist das zweite Element 42.

2.7. Strom von Primitiven

Java 8 bietet die Möglichkeit, Streams aus drei Grundtypen zu erstellen:int, long unddouble. DaStream<T> eine generische Schnittstelle ist und es keine Möglichkeit gibt, Grundelemente als Typparameter für Generika zu verwenden. Es wurden drei neue spezielle Schnittstellen erstellt:IntStream, LongStream, DoubleStream.

Durch die Verwendung der neuen Schnittstellen wird unnötiges Auto-Boxing vermieden und die Produktivität gesteigert:

IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);

Die Methoderange(int startInclusive, int endExclusive) erstellt einen geordneten Stream vom ersten zum zweiten Parameter. Es erhöht den Wert nachfolgender Elemente um den Wert 1. Das Ergebnis enthält nicht den letzten Parameter, sondern nur eine Obergrenze der Sequenz.

DierangeClosed(int startInclusive, int endInclusive)-Methode macht dasselbe mit nur einem Unterschied - das zweite Element ist enthalten. Diese beiden Methoden können verwendet werden, um einen der drei Arten von Strömen von Grundelementen zu erzeugen.

Seit Java 8 bietet die KlasseRandomeine breite Palette von Methoden zur Generierung von Primitivströmen. Der folgende Code erstellt beispielsweise einDoubleStream, mit drei Elementen:

Random random = new Random();
DoubleStream doubleStream = random.doubles(3);

2.8. Strom vonString

String kann auch als Quelle zum Erstellen eines Streams verwendet werden.

Mit Hilfe derchars()-Methode derString-Klasse. Da es in JDK keine SchnittstelleCharStream gibt, wirdIntStream verwendet, um stattdessen einen Zeichenstrom darzustellen.

IntStream streamOfChars = "abc".chars();

Im folgenden Beispiel wird einString gemäß den angegebenenRegEx in Unterzeichenfolgen unterteilt:

Stream streamOfString =
  Pattern.compile(", ").splitAsStream("a, b, c");

2.9. Datenstrom

Mit der Java NIO-KlasseFiles könnenStream<String> einer Textdatei mit der Methodelines() generiert werden. Jede Textzeile wird zum Element des Streams:

Path path = Paths.get("C:\\file.txt");
Stream streamOfStrings = Files.lines(path);
Stream streamWithCharset =
  Files.lines(path, Charset.forName("UTF-8"));

DieCharset können als Argument derlines()-Methode angegeben werden.

3. Referenzieren vona Stream

Es ist möglich, einen Stream zu instanziieren und auf ihn zuzugreifen, solange nur Zwischenoperationen aufgerufen wurden. Durch Ausführen einer Terminaloperation kann auf einen Stream. nicht zugegriffen werden

Um dies zu demonstrieren, werden wir für eine Weile vergessen, dass es die beste Praxis ist, die Abfolge der Operationen zu verketten. Neben der unnötigen Ausführlichkeit gilt technisch der folgende Code:

Stream stream =
  Stream.of("a", "b", "c").filter(element -> element.contains("b"));
Optional anyElement = stream.findAny();

Ein Versuch, dieselbe Referenz nach dem Aufrufen der Terminaloperation wiederzuverwenden, löst jedochIllegalStateException: aus

Optional firstElement = stream.findFirst();

DaIllegalStateExceptionRuntimeException ist, signalisiert ein Compiler kein Problem. Es ist daher sehr wichtig, sich daran zu erinnern, dassJava 8streams can’t be reused.

Diese Art von Verhalten ist logisch, da Streams die Möglichkeit bieten, eine endliche Folge von Operationen auf die Quelle von Elementen in einem funktionalen Stil anzuwenden, aber keine Elemente zu speichern.

Damit der vorherige Code ordnungsgemäß funktioniert, sollten einige Änderungen vorgenommen werden:

List elements =
  Stream.of("a", "b", "c").filter(element -> element.contains("b"))
    .collect(Collectors.toList());
Optional anyElement = elements.stream().findAny();
Optional firstElement = elements.stream().findFirst();

4. Stream-Pipeline

Um eine Folge von Operationen über die Elemente der Datenquelle auszuführen und deren Ergebnisse zu aggregieren, werden drei Teile benötigt -source,intermediate operation(s) undterminal operation.

Zwischenoperationen geben einen neuen modifizierten Stream zurück. Um beispielsweise einen neuen Stream des vorhandenen Streams ohne wenige Elemente zu erstellen, sollte die Methodeskip()verwendet werden:

Stream onceModifiedStream =
  Stream.of("abcd", "bbcd", "cbcd").skip(1);

Wenn mehr als eine Änderung erforderlich ist, können Zwischenoperationen verkettet werden. Angenommen, wir müssen auch jedes Element von currentStream<String> durch eine Teilzeichenfolge der ersten Zeichen ersetzen. Dies erfolgt durch Verketten der Methodenskip() undmap():

Stream twiceModifiedStream =
  stream.skip(1).map(element -> element.substring(0, 3));

Wie Sie sehen können, verwendet die Methodemap()einen Lambda-Ausdruck als Parameter. Wenn Sie mehr über Lambdas erfahren möchten, schauen Sie sich unser TutorialLambda Expressions and Functional Interfaces: Tips and Best Practices an.

Ein Stream an sich ist wertlos. Das eigentliche Interesse eines Benutzers ergibt sich aus der Terminaloperation, die ein Wert eines bestimmten Typs oder eine Aktion sein kann, die auf jedes Element des Streams angewendet wird. Only one terminal operation can be used per stream.

Die richtige und bequemste Art, Streams zu verwenden, ist einstream pipeline, which is a chain of stream source, intermediate operations, and a terminal operation.. Beispiel:

List list = Arrays.asList("abc1", "abc2", "abc3");
long size = list.stream().skip(1)
  .map(element -> element.substring(0, 3)).sorted().count();

5. Fauler Anruf

Intermediate operations are lazy. Dies bedeutet, dassthey will be invoked only if it is necessary for the terminal operation execution.

Um dies zu demonstrieren, stellen Sie sich vor, wir haben die MethodewasCalled(),, die einen inneren Zähler jedes Mal erhöht, wenn er aufgerufen wird:

private long counter;

private void wasCalled() {
    counter++;
}

Die Aufrufmethode warCalled() von Operationfilter():

List list = Arrays.asList(“abc1”, “abc2”, “abc3”);
counter = 0;
Stream stream = list.stream().filter(element -> {
    wasCalled();
    return element.contains("2");
});

Da wir eine Quelle mit drei Elementen haben, können wir davon ausgehen, dass die Methodefilter() dreimal aufgerufen wird und der Wert der Variablencounter 3 beträgt. Das Ausführen dieses Codes ändert jedochcounter überhaupt nicht, es ist immer noch Null, sodass diefilter()-Methode nicht einmal aufgerufen wurde. Der Grund warum - fehlt der Terminalbetrieb.

Schreiben wir diesen Code ein wenig um, indem wir einemap()-Operation und eine Terminal-Operation hinzufügen -findFirst().. Wir werden auch die Möglichkeit hinzufügen, eine Reihenfolge von Methodenaufrufen mithilfe der Protokollierung zu verfolgen:

Optional stream = list.stream().filter(element -> {
    log.info("filter() was called");
    return element.contains("2");
}).map(element -> {
    log.info("map() was called");
    return element.toUpperCase();
}).findFirst();

Das resultierende Protokoll zeigt, dass die Methodefilter() zweimal und die Methodemap()nur einmal aufgerufen wurde. Das liegt daran, dass die Pipeline vertikal ausgeführt wird. In unserem Beispiel hat das erste Element des Streams das Prädikat des Filters nicht erfüllt. Anschließend wurde die Methodefilter()für das zweite Element aufgerufen, das den Filter bestanden hat. Ohne diefilter() für das dritte Element aufzurufen, gingen wir durch die Pipeline zurmap()-Methode.

Die OperationfindFirst()erfüllt nur ein Element. In diesem speziellen Beispiel konnten durch den verzögerten Aufruf zwei Methodenaufrufe vermieden werden - einer fürfilter() und einer fürmap().

6. Ausführungsreihenfolge

Aus Sicht der Leistung sindthe right order is one of the most important aspects of chaining operations in the stream pipeline:

long size = list.stream().map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).skip(2).count();

Die Ausführung dieses Codes erhöht den Wert des Zählers um drei. Dies bedeutet, dass diemap()-Methode des Streams dreimal aufgerufen wurde. Aber der Wert vonsize ist eins. Der resultierende Stream hat also nur ein Element und wir haben die teurenmap()-Operationen ohne Grund zweimal von dreimal ausgeführt.

Wenn wir die Reihenfolge derskip()- undmap()-Methoden, ändern, erhöhen sich diecounter nur um eins. Die Methodemap() wird also nur einmal aufgerufen:

long size = list.stream().skip(2).map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).count();

Dies bringt uns zur Regel:intermediate operations which reduce the size of the stream should be placed before operations which are applying to each element. Behalten Sie also Methoden wie skip(), filter(), distinct() oben in Ihrer Stream-Pipeline.

7. Stream-Reduzierung

Die API verfügt über viele Terminaloperationen, die einen Stream zu einem Typ oder zu einem Grundelement zusammenfassen, z. B.count(), max(), min(), sum(),. Diese Operationen funktionieren jedoch gemäß der vordefinierten Implementierung. Und welcheif a developer needs to customize a Stream’s reduction mechanism? Es gibt zwei Methoden, die dies ermöglichen - diereduce()- und diecollect()-Methode.

7.1. Diereduce()-Methode

Es gibt drei Varianten dieser Methode, die sich durch ihre Signaturen und Rückgabetypen unterscheiden. Sie können folgende Parameter haben:

identity –ist der Anfangswert für einen Akkumulator oder ein Standardwert, wenn ein Stream leer ist und nichts zu akkumulieren ist.

accumulator –ist eine Funktion, die eine Logik der Aggregation von Elementen angibt. Da der Akkumulator für jeden Schritt der Reduzierung einen neuen Wert erstellt, entspricht die Anzahl der neuen Werte der Größe des Streams und nur der letzte Wert ist nützlich. Dies ist nicht sehr gut für die Leistung.

combiner –ist eine Funktion, die die Ergebnisse des Akkumulators aggregiert. Combiner wird nur in einem parallelen Modus aufgerufen, um die Ergebnisse von Akkumulatoren aus verschiedenen Threads zu reduzieren.

Schauen wir uns also diese drei Methoden in Aktion an:

OptionalInt reduced =
  IntStream.range(1, 4).reduce((a, b) -> a + b);

reduced = 6 (1 + 2 + 3)

int reducedTwoParams =
  IntStream.range(1, 4).reduce(10, (a, b) -> a + b);

reducedTwoParams = 16 (10 + 1 + 2 + 3)

int reducedParams = Stream.of(1, 2, 3)
  .reduce(10, (a, b) -> a + b, (a, b) -> {
     log.info("combiner was called");
     return a + b;
  });

Das Ergebnis ist das gleiche wie im vorherigen Beispiel (16) und es erfolgt keine Anmeldung, was bedeutet, dass der Kombinierer nicht aufgerufen wurde. Damit ein Combiner funktioniert, sollte ein Stream parallel sein:

int reducedParallel = Arrays.asList(1, 2, 3).parallelStream()
    .reduce(10, (a, b) -> a + b, (a, b) -> {
       log.info("combiner was called");
       return a + b;
    });

Das Ergebnis ist hier anders (36) und der Combiner wurde zweimal aufgerufen. Hier funktioniert die Reduktion nach dem folgenden Algorithmus: Der Akkumulator wurde dreimal ausgeführt, indem jedes Element des Streams zuidentity zu jedem Element des Streams addiert wurde. Diese Aktionen werden parallel ausgeführt. Als Ergebnis haben sie (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;). Jetzt kann Combiner diese drei Ergebnisse zusammenführen. Dafür sind zwei Iterationen erforderlich (12 + 13 = 25; 25 + 11 = 36).

7.2. Diecollect()-Methode

Die Reduzierung eines Streams kann auch durch eine andere Terminaloperation ausgeführt werden - diecollect()-Methode. Es akzeptiert ein Argument vom TypCollector,, das den Reduktionsmechanismus angibt. Für die meisten gängigen Vorgänge sind bereits vordefinierte Kollektoren erstellt worden. Auf sie kann mit Hilfe des TypsCollectorszugegriffen werden.

In diesem Abschnitt verwenden wir die folgendenList als Quelle für alle Streams:

List productList = Arrays.asList(new Product(23, "potatoes"),
  new Product(14, "orange"), new Product(13, "lemon"),
  new Product(23, "bread"), new Product(13, "sugar"));

Konvertieren eines Streams inCollection (Collection, List oderSet):

List collectorCollection =
  productList.stream().map(Product::getName).collect(Collectors.toList());

Reduzieren aufString:

String listToString = productList.stream().map(Product::getName)
  .collect(Collectors.joining(", ", "[", "]"));

Die Methodejoiner()kann einen bis drei Parameter haben (Trennzeichen, Präfix, Suffix). Das Handlichste an der Verwendung vonjoiner() - Entwickler müssen nicht prüfen, ob der Stream sein Ende erreicht, um das Suffix anzuwenden und kein Trennzeichen anzuwenden. Collector wird sich darum kümmern.

Verarbeitung des Durchschnittswerts aller numerischen Elemente des Streams:

double averagePrice = productList.stream()
  .collect(Collectors.averagingInt(Product::getPrice));

Verarbeitung der Summe aller numerischen Elemente des Streams:

int summingPrice = productList.stream()
  .collect(Collectors.summingInt(Product::getPrice));

Die MethodenaveragingXX(), summingXX() undsummarizingXX() können wie bei Grundelementen (int, long, double) wie bei ihren Wrapper-Klassen (Integer, Long, Double) funktionieren. Ein leistungsfähigeres Merkmal dieser Methoden ist die Bereitstellung des Mappings. Daher muss der Entwickler vor dercollect()-Methode keine zusätzlichemap()-Operation verwenden.

Sammeln statistischer Informationen zu Stream-Elementen:

IntSummaryStatistics statistics = productList.stream()
  .collect(Collectors.summarizingInt(Product::getPrice));

Mithilfe der resultierenden Instanz vom TypIntSummaryStatisticskann der Entwickler einen statistischen Bericht erstellen, indem er die MethodetoString()anwendet. Das Ergebnis ist einString, das diesem“IntSummaryStatistics\{count=5, sum=86, min=13, average=17,200000, max=23}”. gemeinsam ist

Es ist auch einfach, separate Werte fürcount, sum, min, average aus diesem Objekt zu extrahieren, indem MethodengetCount(), getSum(), getMin(), getAverage(), getMax(). angewendet werden. Alle diese Werte können aus einer einzelnen Pipeline extrahiert werden.

Gruppierung der Stream-Elemente nach der angegebenen Funktion:

Map> collectorMapOfLists = productList.stream()
  .collect(Collectors.groupingBy(Product::getPrice));

Im obigen Beispiel wurde der Strom aufMap reduziert, wodurch alle Produkte nach ihrem Preis gruppiert werden.

Unterteilen der Stream-Elemente in Gruppen nach einem Prädikat:

Map> mapPartioned = productList.stream()
  .collect(Collectors.partitioningBy(element -> element.getPrice() > 15));

Drücken Sie den Kollektor, um eine zusätzliche Transformation durchzuführen:

Set unmodifiableSet = productList.stream()
  .collect(Collectors.collectingAndThen(Collectors.toSet(),
  Collections::unmodifiableSet));

In diesem speziellen Fall hat der Kollektor einen Stream inSet konvertiert und dann die nicht modifizierbarenSet daraus erstellt.

Benutzerdefinierter Sammler:

Wenn aus irgendeinem Grund ein benutzerdefinierter Kollektor erstellt werden soll, ist es am einfachsten und weniger ausführlich, die Methodeof() vom TypCollector. zu verwenden

Collector> toLinkedList =
  Collector.of(LinkedList::new, LinkedList::add,
    (first, second) -> {
       first.addAll(second);
       return first;
    });

LinkedList linkedListOfPersons =
  productList.stream().collect(toLinkedList);

In diesem Beispiel wurde eine Instanz vonCollector aufLinkedList<Persone>. reduziert

Parallele Streams

Vor Java 8 war die Parallelisierung komplex. Das Auftauchen vonExecutorService undForkJoin hat das Leben der Entwickler ein wenig vereinfacht, aber sie sollten immer noch bedenken, wie man einen bestimmten Executor erstellt, wie man ihn ausführt und so weiter. Mit Java 8 wurde eine Möglichkeit eingeführt, Parallelität in einem funktionalen Stil zu erreichen.

Mit der API können parallele Streams erstellt werden, die Vorgänge im parallelen Modus ausführen. Wenn die Quelle eines Streams einCollection oder einarray ist, kann dies mit Hilfe derparallelStream()-Methode erreicht werden:

Stream streamOfCollection = productList.parallelStream();
boolean isParallel = streamOfCollection.isParallel();
boolean bigPrice = streamOfCollection
  .map(product -> product.getPrice() * 12)
  .anyMatch(price -> price > 200);

Wenn die Quelle des Streams etwas anderes alsCollection oderarray ist, sollte die Methodeparallel() verwendet werden:

IntStream intStreamParallel = IntStream.range(1, 150).parallel();
boolean isParallel = intStreamParallel.isParallel();

Unter der Haube verwendet die Stream-API automatisch dasForkJoin-Framework, um Operationen parallel auszuführen. Standardmäßig wird der allgemeine Thread-Pool verwendet, und es gibt (zumindest im Moment) keine Möglichkeit, ihm einen benutzerdefinierten Thread-Pool zuzuweisen. This can be overcome by using a custom set of parallel collectors.

Vermeiden Sie bei der Verwendung von Streams im parallelen Modus das Blockieren von Vorgängen und verwenden Sie den parallelen Modus, wenn die Ausführung von Aufgaben ähnlich lange dauert (wenn eine Aufgabe länger dauert als die andere, kann dies den Workflow der gesamten App verlangsamen).

Der Stream im Parallelmodus kann mit der Methodesequential() wieder in den sequentiellen Modus konvertiert werden:

IntStream intStreamSequential = intStreamParallel.sequential();
boolean isParallel = intStreamSequential.isParallel();

Schlussfolgerungen

Die Stream-API ist eine leistungsstarke, aber einfach zu verstehende Sammlung von Tools für die Verarbeitung von Elementsequenzen. Dies ermöglicht es uns, eine große Menge an Code zu reduzieren, besser lesbare Programme zu erstellen und die Produktivität der App zu verbessern, wenn sie ordnungsgemäß verwendet wird.

In den meisten in diesem Artikel gezeigten Codebeispielen wurden Streams nicht konsumiert (wir haben weder dieclose()-Methode noch eine Terminaloperation angewendet). In einer realen Appdon’t leave an instantiated streams unconsumed as that will lead to memory leaks.

Die vollständigen Codebeispiele, die dem Artikel beiliegen, sind inover on GitHub. verfügbar