Java Language
Streams
Suche…
Einführung
Ein Stream
repräsentiert eine Folge von Elementen und unterstützt verschiedene Arten von Operationen, um Berechnungen an diesen Elementen durchzuführen. Mit Java 8 verfügt die Collection
Schnittstelle über zwei Methoden zum Generieren eines Stream
: stream()
und parallelStream()
. Stream
Operationen sind entweder Zwischen- oder Endgeräte. Zwischenoperationen geben einen Stream
sodass mehrere Zwischenoperationen verkettet werden können, bevor der Stream
geschlossen wird. Terminalvorgänge sind entweder ungültig oder liefern ein Nicht-Stream-Ergebnis.
Syntax
- collection.stream ()
- Arrays.stream (Array)
- Stream.iterate (ersterWert, aktuellerWert -> nächsterWert)
- Stream.generate (() -> Wert)
- Stream.of (elementOfT [, elementOfT, ...])
- Stream.empty ()
- StreamSupport.stream (iterable.spliterator (), false)
Streams verwenden
Ein Stream
ist eine Folge von Elementen, auf die sequentielle und parallele Aggregatoperationen ausgeführt werden können. Jeder Stream
kann möglicherweise eine unbegrenzte Datenmenge durchfließen. Infolgedessen werden Daten, die von einem Stream
empfangen werden, bei ihrer Ankunft einzeln verarbeitet, im Gegensatz zur gesamten Stapelverarbeitung der Daten. In Kombination mit Lambda-Ausdrücken bieten sie eine präzise Möglichkeit, Operationen an Datensequenzen mithilfe eines funktionalen Ansatzes auszuführen.
Beispiel: ( siehe Arbeit auf Ideone )
Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");
fruitStream.filter(s -> s.contains("a"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
Ausgabe:
APFEL
BANANE
ORANGE
BIRNE
Die durch den obigen Code ausgeführten Operationen können wie folgt zusammengefasst werden:
Erstellen Sie einen
Stream<String>
-Stream
String
Stream.of(values)
Stream<String>
enthält eine sequenziert geordnetenStream
von ObstString
- Elemente unter Verwendung der statischen Factory - MethodeStream.of(values)
.Die
filter()
Operation behält nur Elemente bei, die mit einem bestimmten Prädikat übereinstimmen (die Elemente, die beim Prädikat getestet werden, geben true zurück). In diesem Fall bleiben die Elemente erhalten, die ein"a"
. Das Prädikat wird als Lambda-Ausdruck angegeben .Die
map()
Operation transformiert jedes Element mithilfe einer angegebenen Funktion, die als Mapper bezeichnet wird. In diesem Fall wird jeder Obst-String
mithilfe der MethodenreferenzString::toUppercase
seinerString
Version in GroßbuchstabenString::toUppercase
.Beachten Sie, dass die Operation
map()
einen Stream mit einem anderen generischen Typ zurückgibt, wenn die Zuordnungsfunktion einen anderen Typ als den Eingabeparameter zurückgibt. Zum Beispiel gibt ein Aufruf von.map(String::isEmpty)
in einemStream<Boolean>
Stream<String>
einenStream<Boolean>
Die
sorted()
Operation sortiert die Elemente desStream
entsprechend ihrer natürlichen Reihenfolge (lexikographisch im Fall vonString
).Schließlich führt die Operation
forEach(action)
eine Aktion aus, die auf jedes Element desStream
einwirkt und an einen Consumer weitergibt . In diesem Beispiel wird jedes Element einfach auf die Konsole gedruckt. Diese Operation ist eine Terminal-Operation, die es unmöglich macht, sie erneut zu bearbeiten.Beachten Sie, dass auf dem
Stream
definierte Operationen aufgrund der Terminaloperation ausgeführt werden. Ohne eine Terminaloperation wird der Stream nicht verarbeitet. Streams können nicht wiederverwendet werden. Sobald eine Terminaloperation aufgerufen wird, wird dasStream
Objekt unbrauchbar.
Operationen (wie oben gezeigt) werden miteinander verkettet und bilden so eine Abfrage der Daten.
Streams schließen
Beachten Sie, dass ein
Stream
Allgemeinen nicht geschlossen werden muss. Es müssen nur Streams geschlossen werden, die auf E / A-Kanälen arbeiten. Die meistenStream
Typen arbeiten nicht mit Ressourcen und müssen daher nicht geschlossen werden.
Die Stream
Schnittstelle erweitert AutoCloseable
. Streams können durch Aufrufen der close
Methode oder mithilfe von try-with-resource-Anweisungen geschlossen werden.
Ein Beispiel für den Fall, dass ein Stream
geschlossen werden soll, ist das Erstellen eines Stream
aus Zeilen aus einer Datei:
try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
lines.forEach(System.out::println);
}
Das Stream
Interface deklariert auch die Stream.onClose()
-Methode, mit der Sie Runnable
Handler registrieren können, die beim Runnable
des Streams aufgerufen werden. Ein Beispiel für einen Anwendungsfall ist, wo Code, der einen Stream erzeugt, wissen muss, wann er zur Bereinigung verbraucht wird.
public Stream<String>streamAndDelete(Path path) throws IOException {
return Files.lines(path).onClose(() -> someClass.deletePath(path));
}
Der run-Handler wird nur ausgeführt, wenn die close()
-Methode explizit oder implizit durch eine try-with-resources-Anweisung aufgerufen wird.
Bearbeitungsauftrag
Die Verarbeitung eines Stream
Objekts kann sequentiell oder parallel sein .
In einem sequentiellen Modus werden die Elemente in der Reihenfolge der Quelle des Stream
. Wenn der Stream
bestellt wird (z. B. eine SortedMap
Implementierung oder eine List
), SortedMap
die Verarbeitung garantiert mit der Reihenfolge der Quelle überein. In anderen Fällen sollte jedoch darauf geachtet werden, nicht von der Reihenfolge abzuhängen (siehe: keySet()
die Java HashMap
keySet()
Iterationsreihenfolge konsistent? ).
Beispiel:
List<Integer> integerList = Arrays.asList(0, 1, 2, 3, 42);
// sequential
long howManyOddNumbers = integerList.stream()
.filter(e -> (e % 2) == 1)
.count();
System.out.println(howManyOddNumbers); // Output: 2
Der parallele Modus ermöglicht die Verwendung mehrerer Threads auf mehreren Kernen, es gibt jedoch keine Garantie für die Reihenfolge, in der Elemente verarbeitet werden.
Wenn mehrere Methoden für einen sequentiellen Stream
aufgerufen werden, muss nicht jede Methode aufgerufen werden. Wenn beispielsweise ein Stream
gefiltert wird und die Anzahl der Elemente auf eins reduziert wird, erfolgt kein nachfolgender Aufruf einer Methode wie sort
. Dies kann die Leistung eines sequentiellen Stream
erhöhen - eine Optimierung, die mit einem parallelen Stream
nicht möglich ist.
Beispiel:
// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
.filter(e -> (e % 2) == 1)
.count();
System.out.println(howManyOddNumbersParallel); // Output: 2
Unterschiede zu Containern (oder Sammlungen )
Während einige Aktionen sowohl für Container als auch für Streams ausgeführt werden können, dienen sie letztendlich unterschiedlichen Zwecken und unterstützen unterschiedliche Operationen. Container konzentrieren sich mehr darauf, wie die Elemente gespeichert werden und wie auf diese Elemente effizient zugegriffen werden kann. Auf der anderen Seite bietet ein Stream
keinen direkten Zugriff und keine Manipulation auf seine Elemente. Es ist mehr der Gruppe von Objekten als einer kollektiven Entität gewidmet und führt Operationen an dieser Entität als Ganzes durch. Stream
und Collection
sind separate Abstraktionen auf hoher Ebene für diese unterschiedlichen Zwecke.
Sammeln Sie Elemente eines Streams in eine Sammlung
Sammeln mit toList()
und toSet()
Elemente aus einem Stream
können mit der Stream.collect
Operation leicht in einem Container Stream.collect
werden:
System.out.println(Arrays
.asList("apple", "banana", "pear", "kiwi", "orange")
.stream()
.filter(s -> s.contains("a"))
.collect(Collectors.toList())
);
// prints: [apple, banana, pear, orange]
Andere Sammlungsinstanzen, wie z. B. ein Set
, können mit anderen integrierten Collectors
-Methoden erstellt werden. Beispielsweise sammelt Collectors.toSet()
die Elemente eines Stream
in einem Set
.
Explizite Kontrolle über die Implementierung von List
oder Set
Gemäß der Dokumentation von Collectors#toList()
und Collectors#toSet()
gibt es keine Gewähr für Typ, Veränderlichkeit, Serialisierbarkeit oder Collectors#toSet()
der zurückgegebenen List
oder des Set
.
Für die explizite Steuerung der zurückzugebenden Implementierung kann stattdessen Collectors#toCollection(Supplier)
verwendet werden, bei der der angegebene Lieferant eine neue und leere Sammlung zurückgibt.
// syntax with method reference
System.out.println(strings
.stream()
.filter(s -> s != null && s.length() <= 3)
.collect(Collectors.toCollection(ArrayList::new))
);
// syntax with lambda
System.out.println(strings
.stream()
.filter(s -> s != null && s.length() <= 3)
.collect(Collectors.toCollection(() -> new LinkedHashSet<>()))
);
Elemente mit toMap sammeln
Der Collector sammelt Elemente in einer Karte, wobei der Schlüssel die Schüler-ID und der Wert den Schülerwert ist.
List<Student> students = new ArrayList<Student>();
students.add(new Student(1,"test1"));
students.add(new Student(2,"test2"));
students.add(new Student(3,"test3"));
Map<Integer, String> IdToName = students.stream()
.collect(Collectors.toMap(Student::getId, Student::getName));
System.out.println(IdToName);
Ausgabe :
{1=test1, 2=test2, 3=test3}
Die Collectors.toMap hat eine andere Implementierung: Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
Die mergeFunction wird hauptsächlich verwendet, um entweder einen neuen Wert auszuwählen oder den alten Wert beizubehalten, wenn der Schlüssel wiederholt wird, wenn ein neues Mitglied in der Map aus einer Liste hinzugefügt wird.
Die mergeFunction sieht häufig wie folgt aus: (s1, s2) -> s1
, um den Wert zu erhalten, der dem wiederholten Schlüssel entspricht, oder (s1, s2) -> s2
, um einen neuen Wert für den wiederholten Schlüssel zu setzen.
Elemente zur Karte der Sammlungen sammeln
Beispiel: von ArrayList nach Map <String, List <>>
Häufig muss eine Liste mit einer Liste aus einer Primärliste erstellt werden. Beispiel: Von einem Schüler der Liste müssen wir eine Liste der Themen für jeden Schüler erstellen.
List<Student> list = new ArrayList<>();
list.add(new Student("Davis", SUBJECT.MATH, 35.0));
list.add(new Student("Davis", SUBJECT.SCIENCE, 12.9));
list.add(new Student("Davis", SUBJECT.GEOGRAPHY, 37.0));
list.add(new Student("Sascha", SUBJECT.ENGLISH, 85.0));
list.add(new Student("Sascha", SUBJECT.MATH, 80.0));
list.add(new Student("Sascha", SUBJECT.SCIENCE, 12.0));
list.add(new Student("Sascha", SUBJECT.LITERATURE, 50.0));
list.add(new Student("Robert", SUBJECT.LITERATURE, 12.0));
Map<String, List<SUBJECT>> map = new HashMap<>();
list.stream().forEach(s -> {
map.computeIfAbsent(s.getName(), x -> new ArrayList<>()).add(s.getSubject());
});
System.out.println(map);
Ausgabe:
{ Robert=[LITERATURE],
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE],
Davis=[MATH, SCIENCE, GEOGRAPHY] }
Beispiel: von ArrayList nach Map <String, Map <>>
List<Student> list = new ArrayList<>();
list.add(new Student("Davis", SUBJECT.MATH, 1, 35.0));
list.add(new Student("Davis", SUBJECT.SCIENCE, 2, 12.9));
list.add(new Student("Davis", SUBJECT.MATH, 3, 37.0));
list.add(new Student("Davis", SUBJECT.SCIENCE, 4, 37.0));
list.add(new Student("Sascha", SUBJECT.ENGLISH, 5, 85.0));
list.add(new Student("Sascha", SUBJECT.MATH, 1, 80.0));
list.add(new Student("Sascha", SUBJECT.ENGLISH, 6, 12.0));
list.add(new Student("Sascha", SUBJECT.MATH, 3, 50.0));
list.add(new Student("Robert", SUBJECT.ENGLISH, 5, 12.0));
Map<String, Map<SUBJECT, List<Double>>> map = new HashMap<>();
list.stream().forEach(student -> {
map.computeIfAbsent(student.getName(), s -> new HashMap<>())
.computeIfAbsent(student.getSubject(), s -> new ArrayList<>())
.add(student.getMarks());
});
System.out.println(map);
Ausgabe:
{ Robert={ENGLISH=[12.0]},
Sascha={MATH=[80.0, 50.0], ENGLISH=[85.0, 12.0]},
Davis={MATH=[35.0, 37.0], SCIENCE=[12.9, 37.0]} }
Spickzettel
Tor | Code |
---|---|
Sammle zu einer List | Collectors.toList() |
In einer ArrayList mit vorab zugewiesener Größe sammeln | Collectors.toCollection(() -> new ArrayList<>(size)) |
Sammeln Sie zu einem Set | Collectors.toSet() |
Sammeln Sie zu einem Set mit besserer Iterationsleistung | Collectors.toCollection(() -> new LinkedHashSet<>()) |
Sammeln Sie zu einem Set<String> Groß- und Kleinschreibung nicht Set<String> | Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)) |
In einem EnumSet<AnEnum> (beste Leistung für Enums) | Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class)) |
Sammeln Sie auf einer Map<K,V> mit eindeutigen Schlüsseln | Collectors.toMap(keyFunc,valFunc) |
Ordnen Sie MyObject.getter () einem eindeutigen MyObject zu | Collectors.toMap(MyObject::getter, Function.identity()) |
Ordnen Sie MyObject.getter () mehreren MyObjects zu | Collectors.groupingBy(MyObject::getter) |
Unendlich Streams
Es ist möglich, einen Stream
zu generieren, der nicht beendet wird. Wenn Sie eine Terminal-Methode für einen infinite Stream
aufrufen, tritt der Stream
in eine Endlosschleife ein. Die limit
Methode eines Stream
kann verwendet werden, um die Anzahl der Terme des Stream
zu begrenzen, die Java verarbeitet.
In diesem Beispiel wird ein Stream
aller natürlichen Zahlen generiert, beginnend mit der Zahl 1. Jeder nachfolgende Begriff des Stream
ist um eins höher als der vorherige. Beim Aufruf der Limit-Methode dieses Stream
werden nur die ersten fünf Terme des Stream
berücksichtigt und gedruckt.
// Generate infinite stream - 1, 2, 3, 4, 5, 6, 7, ...
IntStream naturalNumbers = IntStream.iterate(1, x -> x + 1);
// Print out only the first 5 terms
naturalNumbers.limit(5).forEach(System.out::println);
Ausgabe:
1
2
3
4
5
Eine andere Methode zum Erzeugen eines unendlichen Streams besteht in der Verwendung der Stream.generate- Methode. Diese Methode benötigt ein Lambda vom Typ Supplier .
// Generate an infinite stream of random numbers
Stream<Double> infiniteRandomNumbers = Stream.generate(Math::random);
// Print out only the first 10 random numbers
infiniteRandomNumbers.limit(10).forEach(System.out::println);
Verbrauchende Streams
Ein Stream
wird nur dann durchlaufen, wenn eine Terminaloperation wie count()
, collect()
oder forEach()
. Andernfalls wird keine Operation für den Stream
ausgeführt.
Im folgenden Beispiel wird dem Stream
keine Terminaloperation hinzugefügt, sodass die filter()
Operation nicht aufgerufen wird und keine Ausgabe erzeugt wird, da peek()
keine Terminaloperation ist .
IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);
Dies ist eine Stream
Sequenz mit einem gültigen Terminalbetrieb , daher wird eine Ausgabe erzeugt.
Sie können auch forEach
anstelle von peek
:
IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println);
Ausgabe:
2
4
6
8
Nachdem der Terminalbetrieb ausgeführt wurde, wird der Stream
verbraucht und kann nicht wiederverwendet werden.
Obwohl ein bestimmtes Stream-Objekt nicht wiederverwendet werden kann, ist es einfach, eine wiederverwendbare Iterable
zu erstellen, die an eine Stream-Pipeline delegiert. Dies kann nützlich sein, um eine modifizierte Ansicht eines Live-Datensatzes zurückzugeben, ohne Ergebnisse in einer temporären Struktur sammeln zu müssen.
List<String> list = Arrays.asList("FOO", "BAR");
Iterable<String> iterable = () -> list.stream().map(String::toLowerCase).iterator();
for (String str : iterable) {
System.out.println(str);
}
for (String str : iterable) {
System.out.println(str);
}
Ausgabe:
foo
Bar
foo
Bar
Dies funktioniert, weil Iterable
eine einzige abstrakte Methode Iterator<T> iterator()
deklariert. Das macht es zu einer funktionellen Schnittstelle, die von einem Lambda implementiert wird, das bei jedem Aufruf einen neuen Stream erstellt.
Im Allgemeinen funktioniert ein Stream
wie in der folgenden Abbildung dargestellt:
HINWEIS : Argumentprüfungen werden immer ausgeführt, auch ohne Terminaloperation :
try {
IntStream.range(1, 10).filter(null);
} catch (NullPointerException e) {
System.out.println("We got a NullPointerException as null was passed as an argument to filter()");
}
Ausgabe:
Wir haben eine NullPointerException erhalten, da null als Argument an filter () übergeben wurde.
Frequenzkarte erstellen
Der groupingBy(classifier, downstream)
-Kollektor ermöglicht das Sammeln von Stream
Elementen in einer Map
indem jedes Element in einer Gruppe klassifiziert und ein Downstream-Vorgang für die Elemente ausgeführt wird, die in derselben Gruppe klassifiziert sind.
Ein klassisches Beispiel für dieses Prinzip ist die Verwendung einer Map
um die Vorkommen von Elementen in einem Stream
. In diesem Beispiel ist der Klassifizierer einfach die Identitätsfunktion, die das Element unverändert zurückgibt. Der Downstream-Vorgang zählt die Anzahl der gleichen Elemente mithilfe von counting()
.
Stream.of("apple", "orange", "banana", "apple")
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.forEach(System.out::println);
Die nachgelagerte Operation ist selbst ein Collector ( Collectors.counting()
), der Elemente des Typs String verarbeitet und ein Ergebnis des Typs Long
. Das Ergebnis des Aufrufs der collect
Methode ist Map<String, Long>
.
Dies würde die folgende Ausgabe erzeugen:
Banane = 1
orange = 1
Apfel = 2
Paralleler Stream
Hinweis: Bevor Sie sich für einen Stream
, sollten Sie sich das Verhalten von ParallelStream vs. Sequential Stream anschauen.
Wenn Sie Stream
Vorgänge gleichzeitig ausführen möchten, können Sie eine dieser Methoden verwenden.
List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();
Oder:
Stream<String> aParallelStream = data.parallelStream();
Rufen Sie einen Terminaloperator auf, um die für den parallelen Stream definierten Vorgänge auszuführen:
aParallelStream.forEach(System.out::println);
(Eine mögliche) Ausgabe vom parallelen Stream
:
Drei
Vier
Ein
Zwei
Fünf
Die Reihenfolge kann sich ändern, da alle Elemente parallel verarbeitet werden (was sie möglicherweise schneller macht). Verwenden Sie parallelStream
wenn die Bestellung keine Rolle spielt.
Auswirkungen auf die Leistung
Wenn es sich um Netzwerke handelt, können parallele Stream
die Gesamtleistung einer Anwendung beeinträchtigen, da alle parallelen Stream
einen gemeinsamen Fork-Join-Thread-Pool für das Netzwerk verwenden.
Auf der anderen Seite können parallele Stream
die Leistung in vielen anderen Fällen erheblich verbessern, abhängig von der Anzahl der verfügbaren Kerne in der laufenden CPU.
Konvertieren eines optionalen Streams in einen Wertestrom
Möglicherweise müssen Sie einen Stream
, der Optional
in einen Stream
von Werten konvertieren und nur Werte aus dem vorhandenen Optional
. (dh ohne null
und nicht mit Optional.empty()
).
Optional<String> op1 = Optional.empty();
Optional<String> op2 = Optional.of("Hello World");
List<String> result = Stream.of(op1, op2)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
System.out.println(result); //[Hello World]
Stream erstellen
Alle java Collection<E>
verfügen über die Methoden stream()
und parallelStream()
, aus denen ein Stream<E>
erstellt werden kann:
Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();
Ein Stream<E>
kann mit einem von zwei Verfahren aus einem Array erstellt werden:
String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);
Der Unterschied zwischen Arrays.stream()
und Stream.of()
besteht darin, dass Stream.of()
einen varargs -Parameter besitzt, sodass er folgendermaßen verwendet werden kann:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Es gibt auch primitive Stream
, die Sie verwenden können. Zum Beispiel:
IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
Diese primitiven Streams können auch mit der Arrays.stream()
-Methode erstellt werden:
IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });
Es ist möglich, einen Stream
aus einem Array mit einem angegebenen Bereich zu erstellen.
int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);
Beachten Sie, dass jeder primitive Stream mit der boxed
Methode in einen Boxed-Type-Stream konvertiert werden kann:
Stream<Integer> integerStream = intStream.boxed();
Dies kann in einigen Fällen hilfreich sein, wenn Sie die Daten erfassen möchten, da der primitive Stream keine collect
, für die ein Collector
als Argument verwendet wird.
Wiederverwenden von Zwischenvorgängen einer Stromkette
Der Stream wird geschlossen, wenn der Terminalbetrieb aufgerufen wird. Wiederverwenden des Stroms von Zwischenvorgängen, wenn nur der Terminalbetrieb nur variiert. Wir könnten einen Stream-Anbieter erstellen, um einen neuen Stream mit allen bereits eingerichteten Zwischenvorgängen aufzubauen.
Supplier<Stream<String>> streamSupplier = () -> Stream.of("apple", "banana","orange", "grapes", "melon","blueberry","blackberry")
.map(String::toUpperCase).sorted();
streamSupplier.get().filter(s -> s.startsWith("A")).forEach(System.out::println);
// APPLE
streamSupplier.get().filter(s -> s.startsWith("B")).forEach(System.out::println);
// BANANA
// BLACKBERRY
// BLUEBERRY
int[]
Arrays können mit Streams in List<Integer>
konvertiert werden
int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());
Suchen von Statistiken zu numerischen Streams
Java 8 stellt Klassen genannt IntSummaryStatistics
, DoubleSummaryStatistics
und LongSummaryStatistics
, die ein Statusobjekt für die Statistik wie geben sammeln count
, min
, max
, sum
und average
.
List<Integer> naturalNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
IntSummaryStatistics stats = naturalNumbers.stream()
.mapToInt((x) -> x)
.summaryStatistics();
System.out.println(stats);
Was führt zu:
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}
Holen Sie sich ein Stück Strom
Beispiel: Holen Sie sich einen Stream
mit 30 Elementen, der das 21. bis einschließlich 50. Element einer Sammlung enthält.
final long n = 20L; // the number of elements to skip
final long maxSize = 30L; // the number of elements the stream should be limited to
final Stream<T> slice = collection.stream().skip(n).limit(maxSize);
Anmerkungen:
-
IllegalArgumentException
wird ausgelöst, wennn
negativ ist odermaxSize
negativ ist - Sowohl
skip(long)
als auchlimit(long)
sind Zwischenoperationen - Wenn ein Stream weniger als
n
Elemente enthält, gibtskip(n)
einen leeren Stream zurück - Sowohl
skip(long)
als auchlimit(long)
sind billige Vorgänge bei sequentiellen Stream-Pipelines, können jedoch bei geordneten parallelen Pipelines recht teuer sein
Streams verketten
Variablendeklaration für Beispiele:
Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");
Beispiel 1 - Verketten Sie zwei Stream
final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());
concat1.forEach(System.out::print);
// prints: abc123
Beispiel 2 - Verketten Sie mehr als zwei Stream
final Stream<String> concat2 = Stream.concat(
Stream.concat(abc.stream(), digits.stream()),
greekAbc.stream());
System.out.println(concat2.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma
Alternativ zur Vereinfachung der verschachtelten concat()
die Stream
auch mit flatMap()
verkettet werden:
final Stream<String> concat3 = Stream.of(
abc.stream(), digits.stream(), greekAbc.stream())
.flatMap(s -> s);
// or `.flatMap(Function.identity());` (java.util.function.Function)
System.out.println(concat3.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma
Seien Sie beim Stream
von Stream
aus wiederholten Verkettungen vorsichtig, da der Zugriff auf ein Element eines tief verketteten Stream
zu tiefen Aufrufketten oder sogar zu einer StackOverflowException
.
IntStream to String
Java verfügt nicht über einen Char-Stream . Wenn Sie also mit String
s arbeiten und einen Stream
von Character
IntStream
, besteht die Option, einen IntStream
von Codepunkten mithilfe der String.codePoints()
-Methode String.codePoints()
. So ist IntStream
wie IntStream
:
public IntStream stringToIntStream(String in) {
return in.codePoints();
}
Es ist etwas komplizierter, die Konvertierung anders durchzuführen, z. B. IntStreamToString. Das kann wie folgt gemacht werden:
public String intStreamToString(IntStream intStream) {
return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}
Sortieren mit Stream
List<String> data = new ArrayList<>();
data.add("Sydney");
data.add("London");
data.add("New York");
data.add("Amsterdam");
data.add("Mumbai");
data.add("California");
System.out.println(data);
List<String> sortedData = data.stream().sorted().collect(Collectors.toList());
System.out.println(sortedData);
Ausgabe:
[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]
Es ist auch möglich, einen anderen Vergleichsmechanismus zu verwenden, da es eine überladene sorted
Version gibt, die einen Vergleicher als Argument verwendet.
Sie können auch einen Lambda-Ausdruck zum Sortieren verwenden:
List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());
Dies würde [Sydney, New York, Mumbai, London, California, Amsterdam]
ausgeben.
Sie können Comparator.reverseOrder()
, um einen Komparator zu haben, der die reverse
der natürlichen Reihenfolge durchführt.
List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
Ströme von Primitiven
Java bietet spezialisierte Stream
für drei Arten von IntStream
: IntStream
(für int
), LongStream
(für long
s) und DoubleStream
(für double
s). Sie sind nicht nur optimierte Implementierungen für ihre jeweiligen Grundelemente, sondern bieten auch mehrere spezifische Terminalmethoden, typischerweise für mathematische Operationen. Z.B:
IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0
Sammeln Sie die Ergebnisse eines Streams in einem Array
Analog, um eine Sammlung für einen Stream
durch collect()
zu erhalten, kann ein Array mit der Stream.toArray()
-Methode erhalten werden:
List<String> fruits = Arrays.asList("apple", "banana", "pear", "kiwi", "orange");
String[] filteredFruits = fruits.stream()
.filter(s -> s.contains("a"))
.toArray(String[]::new);
// prints: [apple, banana, pear, orange]
System.out.println(Arrays.toString(filteredFruits));
String[]::new
ist eine spezielle Art von Methodenreferenz: eine Konstruktorreferenz.
Das erste Element finden, das einem Prädikat entspricht
Es ist möglich, das erste Element eines Stream
zu finden, das einer Bedingung entspricht.
In diesem Beispiel finden wir die erste Integer
deren Quadrat über 50000
.
IntStream.iterate(1, i -> i + 1) // Generate an infinite stream 1,2,3,4...
.filter(i -> (i*i) > 50000) // Filter to find elements where the square is >50000
.findFirst(); // Find the first filtered element
Dieser Ausdruck gibt ein OptionalInt
mit dem Ergebnis zurück.
Beachten Sie, dass Java bei einem unendlichen Stream
jedes Element so lange überprüft, bis es ein Ergebnis findet. Bei einem endlichen Stream
gibt Java ein leeres OptionalInt
zurück, wenn ihm die Elemente ausgehen, das Ergebnis jedoch nicht gefunden wird.
Verwenden von IntStream zum Durchlaufen von Indizes
Stream
von Elementen erlauben normalerweise keinen Zugriff auf den Indexwert des aktuellen Elements. Verwenden Sie IntStream.range(start, endExclusive)
um über ein Array oder eine ArrayList
zu iterieren, während Sie auf Indizes IntStream.range(start, endExclusive)
.
String[] names = { "Jon", "Darin", "Bauke", "Hans", "Marc" };
IntStream.range(0, names.length)
.mapToObj(i -> String.format("#%d %s", i + 1, names[i]))
.forEach(System.out::println);
Die range(start, endExclusive)
-Methode gibt einen anderen ÌntStream
und der mapToObj(mapper)
gibt einen Stream von String
.
Ausgabe:
# 1 Jon
# 2 Darin
# 3 Bauke
# 4 Hans
# 5 Marc
Dies ist der Verwendung einer normalen for
Schleife mit einem Zähler sehr ähnlich, jedoch mit dem Vorteil des Pipelining und der Parallelisierung:
for (int i = 0; i < names.length; i++) {
String newName = String.format("#%d %s", i + 1, names[i]);
System.out.println(newName);
}
Flachen von Streams mit flatMap ()
Ein Stream
von Elementen, die wiederum streamfähig sind, kann zu einem einzigen kontinuierlichen Stream
:
Das Array mit der Liste der Elemente kann in eine einzige Liste konvertiert werden.
List<String> list1 = Arrays.asList("one", "two");
List<String> list2 = Arrays.asList("three","four","five");
List<String> list3 = Arrays.asList("six");
List<String> finalList = Stream.of(list1, list2, list3).flatMap(Collection::stream).collect(Collectors.toList());
System.out.println(finalList);
// [one, two, three, four, five, six]
Map mit der Liste der Elemente als Werte kann auf eine kombinierte Liste reduziert werden
Map<String, List<Integer>> map = new LinkedHashMap<>();
map.put("a", Arrays.asList(1, 2, 3));
map.put("b", Arrays.asList(4, 5, 6));
List<Integer> allValues = map.values() // Collection<List<Integer>>
.stream() // Stream<List<Integer>>
.flatMap(List::stream) // Stream<Integer>
.collect(Collectors.toList());
System.out.println(allValues);
// [1, 2, 3, 4, 5, 6]
List
der Map
kann in einen einzigen kontinuierlichen Stream
List<Map<String, String>> list = new ArrayList<>();
Map<String,String> map1 = new HashMap();
map1.put("1", "one");
map1.put("2", "two");
Map<String,String> map2 = new HashMap();
map2.put("3", "three");
map2.put("4", "four");
list.add(map1);
list.add(map2);
Set<String> output= list.stream() // Stream<Map<String, String>>
.map(Map::values) // Stream<List<String>>
.flatMap(Collection::stream) // Stream<String>
.collect(Collectors.toSet()); //Set<String>
// [one, two, three,four]
Erstellen Sie eine Karte basierend auf einem Stream
Einfacher Fall ohne doppelte Schlüssel
Stream<String> characters = Stream.of("A", "B", "C");
Map<Integer, String> map = characters
.collect(Collectors.toMap(element -> element.hashCode(), element -> element));
// map = {65=A, 66=B, 67=C}
Um die Dinge deklarativer zu gestalten, können wir in Function
interface - Function.identity()
statische Methode verwenden. Wir können dieses Lambda- element -> element
durch Function.identity()
ersetzen.
Fall, in dem möglicherweise doppelte Schlüssel vorhanden sind
Der Javadoc für Collectors.toMap
gibt Folgendes an:
Wenn die zugeordneten Schlüssel Duplikate enthalten (gemäß
Object.equals(Object)
), wird eineIllegalStateException
ausgelöst, wenn der Auflistungsvorgang ausgeführt wird. Wenn die zugeordneten Tasten Duplikate enthalten können, verwendentoMap(Function, Function, BinaryOperator)
stattdessentoMap(Function, Function, BinaryOperator)
.
Stream<String> characters = Stream.of("A", "B", "B", "C");
Map<Integer, String> map = characters
.collect(Collectors.toMap(
element -> element.hashCode(),
element -> element,
(existingVal, newVal) -> (existingVal + newVal)));
// map = {65=A, 66=BB, 67=C}
Der an Collectors.toMap(...)
BinaryOperator
generiert den Wert, der bei einer Kollision gespeichert werden soll. Es kann:
- den alten Wert zurückgeben, sodass der erste Wert im Stream Vorrang hat,
- den neuen Wert zurückgeben, damit der letzte Wert im Stream Vorrang hat, oder
- kombinieren Sie die alten und neuen Werte
Gruppierung nach Wert
Sie können Collectors.groupingBy
wenn Sie das Äquivalent einer kaskadierten "group by" -Operation der Datenbank ausführen müssen. Zur Veranschaulichung wird im Folgenden eine Karte erstellt, in der die Namen der Personen den Nachnamen zugeordnet werden:
List<Person> people = Arrays.asList(
new Person("Sam", "Rossi"),
new Person("Sam", "Verdi"),
new Person("John", "Bianchi"),
new Person("John", "Rossi"),
new Person("John", "Verdi")
);
Map<String, List<String>> map = people.stream()
.collect(
// function mapping input elements to keys
Collectors.groupingBy(Person::getName,
// function mapping input elements to values,
// how to store values
Collectors.mapping(Person::getSurname, Collectors.toList()))
);
// map = {John=[Bianchi, Rossi, Verdi], Sam=[Rossi, Verdi]}
Zufällige Strings mit Streams erzeugen
Es ist manchmal nützlich, zufällige Strings
zu erstellen, z. B. als Sitzungs-ID für einen Web-Service oder als Initialkennwort nach der Registrierung für eine Anwendung. Dies kann leicht mit Stream
s erreicht werden.
Zuerst müssen wir einen Zufallszahlengenerator initialisieren. Zur Erhöhung der Sicherheit für den erzeugten String
s, ist es eine gute Idee zu verwenden SecureRandom
.
Hinweis : Das Erstellen eines SecureRandom
ist ziemlich teuer. SecureRandom
es sich, dies nur einmal zu tun und setSeed()
eine seiner setSeed()
Methoden aufzurufen, um es neu zu bestimmen.
private static final SecureRandom rng = new SecureRandom(SecureRandom.generateSeed(20));
//20 Bytes as a seed is rather arbitrary, it is the number used in the JavaDoc example
Bei der Erstellung von zufälligen String
s möchten wir normalerweise, dass sie nur bestimmte Zeichen (z. B. nur Buchstaben und Ziffern) verwenden. Daher können wir eine Methode erstellen, die einen boolean
der später zum Filtern des Stream
.
//returns true for all chars in 0-9, a-z and A-Z
boolean useThisCharacter(char c){
//check for range to avoid using all unicode Letter (e.g. some chinese symbols)
return c >= '0' && c <= 'z' && Character.isLetterOrDigit(c);
}
Als Nächstes können wir das RNG verwenden, um einen zufälligen String mit einer bestimmten Länge zu generieren, der den Zeichensatz enthält, der unsere useThisCharacter
Prüfung besteht.
public String generateRandomString(long length){
//Since there is no native CharStream, we use an IntStream instead
//and convert it to a Stream<Character> using mapToObj.
//We need to specify the boundaries for the int values to ensure they can safely be cast to char
Stream<Character> randomCharStream = rng.ints(Character.MIN_CODE_POINT, Character.MAX_CODE_POINT).mapToObj(i -> (char)i).filter(c -> this::useThisCharacter).limit(length);
//now we can use this Stream to build a String utilizing the collect method.
String randomString = randomCharStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
return randomString;
}
Verwenden von Streams zum Implementieren mathematischer Funktionen
Stream
und vor allem IntStream
sind eine elegante Möglichkeit, Summationsterme (∑) zu implementieren. Die Bereiche des Stream
können als Grenzen der Summation verwendet werden.
Madhavas Annäherung an Pi ist beispielsweise durch die Formel gegeben (Quelle: wikipedia ):
Dies kann mit einer beliebigen Genauigkeit berechnet werden. ZB für 101 Begriffe:
double pi = Math.sqrt(12) *
IntStream.rangeClosed(0, 100)
.mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
.sum();
Hinweis: Mit der double
Genauigkeit genügt die Auswahl einer oberen Grenze von 29, um ein Ergebnis zu Math.Pi
, das sich von Math.Pi
.
Verwenden von Streams und Methodenreferenzen zum Schreiben selbstdokumentierender Prozesse
Methodenreferenzen machen ausgezeichneten selbstdokumentierenden Code, und die Verwendung von Methodenreferenzen mit Stream
s macht komplizierte Prozesse leicht lesbar und verständlich. Betrachten Sie den folgenden Code:
public interface Ordered {
default int getOrder(){
return 0;
}
}
public interface Valued<V extends Ordered> {
boolean hasPropertyTwo();
V getValue();
}
public interface Thing<V extends Ordered> {
boolean hasPropertyOne();
Valued<V> getValuedProperty();
}
public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
List<V> results = new ArrayList<V>();
for (Thing<V> thing : things) {
if (thing.hasPropertyOne()) {
Valued<V> valued = thing.getValuedProperty();
if (valued != null && valued.hasPropertyTwo()){
V value = valued.getValue();
if (value != null){
results.add(value);
}
}
}
}
results.sort((a, b)->{
return Integer.compare(a.getOrder(), b.getOrder());
});
return results;
}
Diese letzte mit Stream
und Methodenreferenzen neu geschriebene Methode ist viel lesbarer und jeder Schritt des Prozesses ist schnell und einfach zu verstehen. Sie ist nicht nur kürzer, sondern zeigt auf einen Blick, welche Schnittstellen und Klassen in jedem Schritt für den Code verantwortlich sind:
public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
return things.stream()
.filter(Thing::hasPropertyOne)
.map(Thing::getValuedProperty)
.filter(Objects::nonNull)
.filter(Valued::hasPropertyTwo)
.map(Valued::getValue)
.filter(Objects::nonNull)
.sorted(Comparator.comparing(Ordered::getOrder))
.collect(Collectors.toList());
}
Verwenden von Streams of Map.Entry zum Erhalten der Anfangswerte nach dem Mapping
Wenn Sie einen Stream
haben, den Sie Map.Entry<K,V>
müssen, die ursprünglichen Werte aber auch beibehalten möchten, können Sie den Stream
einer Map.Entry<K,V>
public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}
Dann können Sie Ihren Konverter verwenden, um Stream
zu verarbeiten, die sowohl auf die ursprünglichen als auch auf die zugeordneten Werte zugreifen können:
Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
.map(entryMapper(transformer));
Sie können diesen Stream
wie gewohnt weiterverarbeiten. Dies vermeidet den Aufwand für das Erstellen einer Zwischensammlung.
Stream-Betriebskategorien
Streamoperationen lassen sich in zwei Hauptkategorien, Zwischen- und Terminaloperationen, und zwei Unterkategorien, zustandslos und zustandsbehaftet, unterteilen.
Zwischenoperationen:
Eine Zwischenoperation ist immer faul , wie eine einfache Stream.map
. Sie wird erst aufgerufen, wenn der Stream tatsächlich verbraucht ist. Dies kann leicht überprüft werden:
Arrays.asList(1, 2 ,3).stream().map(i -> {
throw new RuntimeException("not gonna happen");
return i;
});
Zwischenoperationen sind die allgemeinen Bausteine eines Streams, die nach der Quelle verkettet sind, und normalerweise folgt eine Terminaloperation, die die Streamkette auslöst.
Terminalbetrieb
Terminalvorgänge lösen den Verbrauch eines Streams aus. Einige der häufigsten sind Stream.forEach
oder Stream.collect
. Sie werden normalerweise nach einer Kette von Zwischenoperationen angeordnet und sind fast immer eifrig .
Zustandslose Operationen
Zustandslosigkeit bedeutet, dass jeder Artikel ohne den Kontext anderer Artikel verarbeitet wird. Zustandslose Operationen ermöglichen eine speichereffiziente Verarbeitung von Streams. Vorgänge wie Stream.map
und Stream.filter
, für die keine Informationen zu anderen Elementen des Streams erforderlich sind, werden als zustandslos betrachtet.
Stateful Operationen
Statefulness bedeutet, dass die Operation für jedes Element von (einigen) anderen Elementen des Streams abhängt. Dies erfordert, dass ein Zustand erhalten bleibt. Statefulness-Operationen können mit langen oder unendlichen Streams brechen. Vorgänge wie Stream.sorted
erfordern die Stream.sorted
des gesamten Streams, bevor ein Element Stream.sorted
wird, das einen ausreichend langen Stream von Elementen Stream.sorted
. Dies kann durch einen langen Stream ( auf eigenes Risiko ) gezeigt werden:
// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);
Dies führt aufgrund von Stream.sorted
von Stream.sorted
zu einem Out-of-Memory.
// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);
Konvertierung eines Iterators in einen Stream
Verwenden Sie Spliterators.spliterator()
oder Spliterators.spliteratorUnknownSize()
, um einen Iterator in einen Stream zu konvertieren:
Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);
Reduktion mit Streams
Reduktion ist der Vorgang, bei dem ein binärer Operator auf jedes Element eines Streams angewendet wird, um einen Wert zu erhalten.
Die sum()
Methode eines IntStream
ist ein Beispiel für eine Reduzierung. es wird zusätzlich auf jeden Begriff des Streams angewendet, was zu einem endgültigen Wert führt:
Dies ist äquivalent zu (((1+2)+3)+4)
Die reduce
eines Streams ermöglicht das Erstellen einer benutzerdefinierten Reduzierung. Es ist möglich, die reduce
Methode zum Implementieren der sum()
-Methode zu verwenden:
IntStream istr;
//Initialize istr
OptionalInt istr.reduce((a,b)->a+b);
Die Optional
Version wird zurückgegeben, damit leere Streams entsprechend behandelt werden können.
Ein weiteres Beispiel für die Reduktion ist die Kombination einer Stream<LinkedList<T>>
in einer einzelnen LinkedList<T>
:
Stream<LinkedList<T>> listStream;
//Create a Stream<LinkedList<T>>
Optional<LinkedList<T>> bigList = listStream.reduce((LinkedList<T> list1, LinkedList<T> list2)->{
LinkedList<T> retList = new LinkedList<T>();
retList.addAll(list1);
retList.addAll(list2);
return retList;
});
Sie können auch ein Identitätselement angeben . Beispielsweise ist das Identitätselement für die Addition 0, da x+0==x
. Für die Multiplikation ist das Identitätselement 1, da x*1==x
. Im obigen Fall handelt es sich bei dem Identitätselement um eine leere LinkedList<T>
. Wenn Sie einer anderen Liste eine leere Liste hinzufügen, ändert sich die Liste, zu der Sie "hinzufügen", nicht:
Stream<LinkedList<T>> listStream;
//Create a Stream<LinkedList<T>>
LinkedList<T> bigList = listStream.reduce(new LinkedList<T>(), (LinkedList<T> list1, LinkedList<T> list2)->{
LinkedList<T> retList = new LinkedList<T>();
retList.addAll(list1);
retList.addAll(list2);
return retList;
});
Wenn ein Identitätselement bereitgestellt wird, wird der Rückgabewert nicht in ein Optional
Element eingeschlossen. Wenn dieses Element in einem leeren Stream aufgerufen wird, wird das Identitätselement von reduz reduce()
zurückgegeben.
Der binäre Operator muss auch assoziativ sein , dh (a+b)+c==a+(b+c)
. Dies liegt daran, dass die Elemente in beliebiger Reihenfolge reduziert werden können. Die obige Additionsreduktion könnte beispielsweise folgendermaßen durchgeführt werden:
Diese Reduktion entspricht dem Schreiben ((1+2)+(3+4))
. Die Eigenschaft der Assoziativität ermöglicht es auch, dass Java den Stream
parallel reduziert. Ein Teil des Streams kann von jedem Prozessor reduziert werden, wobei das Ergebnis jedes Prozessors am Ende kombiniert wird.
Verknüpfen eines Streams mit einem einzelnen String
Ein häufig auftretender Anwendungsfall ist das Erstellen eines String
aus einem Stream, bei dem die Stream-Elemente durch ein bestimmtes Zeichen getrennt werden. Collectors.joining()
kann die Collectors.joining()
-Methode verwendet werden, wie im folgenden Beispiel:
Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");
String result = fruitStream.filter(s -> s.contains("a"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", "));
System.out.println(result);
Ausgabe:
Apfel, Banane, Orange, Birne
Die Collectors.joining()
-Methode kann auch für Prä- und Postfixes sorgen:
String result = fruitStream.filter(s -> s.contains("e"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", ", "Fruits: ", "."));
System.out.println(result);
Ausgabe:
Früchte: APPLE, ORANGE, BIRN.