Java Language
Strumienie
Szukaj…
Wprowadzenie
Stream
reprezentuje sekwencję elementów i obsługuje różnego rodzaju operacje do wykonywania obliczeń na tych elementach. W Javie 8 interfejs Collection
ma dwie metody generowania Stream
: stream()
i parallelStream()
. Operacje Stream
mają charakter pośredni lub terminalowy. Operacje pośrednie zwracają Stream
dzięki czemu można połączyć wiele operacji pośrednich przed zamknięciem Stream
. Operacje terminalowe są nieważne lub zwracają wynik inny niż strumień.
Składnia
- collection.stream ()
- Arrays.stream (tablica)
- Stream.iterate (firstValue, currentValue -> nextValue)
- Stream.generate (() -> wartość)
- Stream.of (elementOfT [, elementOfT, ...])
- Stream.empty ()
- StreamSupport.stream (iterable.spliterator (), false)
Korzystanie ze strumieni
Stream
jest sekwencją elementów, na których można wykonywać sekwencyjne i równoległe operacje agregacji. Każdy Stream
może potencjalnie mieć nieograniczoną ilość danych przepływających przez niego. W rezultacie dane otrzymane ze Stream
są przetwarzane indywidualnie w momencie ich otrzymania, w przeciwieństwie do wykonywania przetwarzania wsadowego danych. W połączeniu z wyrażeniami lambda zapewniają zwięzły sposób wykonywania operacji na sekwencjach danych przy użyciu podejścia funkcjonalnego.
Przykład: ( zobacz, jak działa na 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);
Wynik:
JABŁKO
BANAN
POMARAŃCZOWY
GRUSZKA
Operacje wykonywane przez powyższy kod można podsumować w następujący sposób:
Utwórz
Stream<String>
zawierający sekwencyjnie uporządkowanyStream
owocówString
elementów przy użyciu statycznej metody fabrycznejStream.of(values)
.Operacja
filter()
zachowuje tylko elementy, które pasują do danego predykatu (elementy, które po przetestowaniu przez predykat zwracają wartość true). W takim przypadku zachowuje elementy zawierające"a"
. Predykat podano jako wyrażenie lambda .Operacja
map()
przekształca każdy element za pomocą danej funkcji, zwanej maperem. W tym przypadku każdyString
owocowy jest odwzorowywany na wersjęString
wielkimi literami przy użyciu odwołania do metodyString::toUppercase
.Zauważ, że operacja
map()
zwróci strumień o innym typie ogólnym, jeśli funkcja mapowania zwróci typ inny niż parametr wejściowy. Na przykład wStream<String>
wywołanie.map(String::isEmpty)
zwracaStream<Boolean>
Operacja
sorted()
sortuje elementyStream
zgodnie z ich naturalną kolejnością (leksykograficznie, w przypadku ciąguString
).Na koniec
forEach(action)
wykonuje akcję, która działa na każdy elementStream
, przekazując ją do konsumenta . W tym przykładzie każdy element jest po prostu drukowany na konsoli. Ta operacja jest operacją terminalową, co uniemożliwia ponowną operację na niej.Należy pamiętać, że operacje zdefiniowane w
Stream
są wykonywane z powodu operacji terminalu. Bez operacji terminalowej strumień nie jest przetwarzany. Strumieni nie można ponownie użyć. Po wywołaniu operacji terminalowej obiektStream
staje się bezużyteczny.
Operacje (jak pokazano powyżej) są powiązane ze sobą, tworząc coś, co można postrzegać jako zapytanie dotyczące danych.
Zamykanie strumieni
Pamiętaj, że
Stream
zasadniczo nie musi być zamknięty. Wymagane jest jedynie zamknięcie strumieni działających na kanałach IO. Większość typówStream
nie działa na zasobach i dlatego nie wymaga zamykania.
Interfejs Stream
rozszerza AutoCloseable
. Strumienie można zamknąć, wywołując metodę close
lub używając instrukcji try-with-resource.
Przykładem przypadku, w którym Stream
powinien zostać zamknięty, jest utworzenie Stream
linii z pliku:
try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
lines.forEach(System.out::println);
}
Interfejs Stream
deklaruje również metodę Stream.onClose()
, która pozwala zarejestrować procedury obsługi Runnable
które zostaną wywołane, gdy strumień zostanie zamknięty. Przykładem użycia jest przypadek, w którym kod, który tworzy strumień, musi wiedzieć, kiedy zostanie wykorzystany do wykonania czyszczenia.
public Stream<String>streamAndDelete(Path path) throws IOException {
return Files.lines(path).onClose(() -> someClass.deletePath(path));
}
Program obsługi uruchomi się tylko wtedy, gdy zostanie wywołana metoda close()
, jawnie lub pośrednio przez instrukcję try-with-resources.
Przetwarzanie zamówienia
Przetwarzanie obiektu Stream
może być sekwencyjne lub równoległe .
W trybie sekwencyjnym elementy przetwarzane są w kolejności źródła źródła Stream
. Jeśli Stream
jest uporządkowany (np. Implementacja SortedMap
lub List
), przetwarzanie gwarantuje zgodność z kolejnością źródła. W innych przypadkach, jednak należy zachować ostrożność, aby nie zależy od zamawiającego (patrz: jest Java HashMap
keySet()
iteracja zamówić spójne? ).
Przykład:
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
Tryb równoległy umożliwia stosowanie wielu wątków na wielu rdzeniach, ale nie ma gwarancji kolejności przetwarzania elementów.
Jeśli w sekwencyjnym Stream
wywoływanych jest wiele metod, nie należy wywoływać każdej metody. Na przykład, jeśli Stream
jest filtrowany, a liczba elementów jest zmniejszona do jednego, kolejne wywołanie metody takiej jak sort
nie nastąpi. Może to zwiększyć wydajność sekwencyjnego Stream
- optymalizacja, która nie jest możliwa w przypadku równoległego Stream
.
Przykład:
// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
.filter(e -> (e % 2) == 1)
.count();
System.out.println(howManyOddNumbersParallel); // Output: 2
Różnice w stosunku do pojemników (lub kolekcji )
Chociaż niektóre działania można wykonywać zarówno na kontenerach, jak i strumieniach, ostatecznie służą one innym celom i wspierają różne operacje. Kontenery są bardziej skoncentrowane na tym, jak elementy są przechowywane i jak efektywnie można uzyskać do nich dostęp. Z drugiej strony Stream
nie zapewnia bezpośredniego dostępu i manipulacji jego elementami; jest bardziej poświęcony grupie obiektów jako podmiotowi zbiorowemu i wykonując operacje na tym obiekcie jako całości. Stream
i Collection
są oddzielnymi abstrakcjami wysokiego poziomu dla tych różnych celów.
Zbierz elementy strumienia do kolekcji
Zbieraj za pomocą toList()
i toSet()
Elementy ze Stream
można łatwo zebrać do kontenera za pomocą operacji Stream.collect
:
System.out.println(Arrays
.asList("apple", "banana", "pear", "kiwi", "orange")
.stream()
.filter(s -> s.contains("a"))
.collect(Collectors.toList())
);
// prints: [apple, banana, pear, orange]
Inne instancje kolekcji, takie jak Set
, można wykonać przy użyciu innych wbudowanych metod Collectors
. Na przykład Collectors.toSet()
zbiera elementy Stream
do Set
.
Jawna kontrola nad implementacją List
lub Set
Zgodnie z dokumentacją Collectors#toList()
i Collectors#toSet()
, nie ma gwarancji co do typu, zmienności, możliwości serializacji lub bezpieczeństwa wątków zwracanej List
lub Set
.
Aby uzyskać wyraźną kontrolę nad implementacją, która ma zostać zwrócona, można zamiast tego użyć Collectors#toCollection(Supplier)
, gdzie dany dostawca zwraca nową i pustą kolekcję.
// 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<>()))
);
Zbieranie elementów za pomocą toMap
Kolektor gromadzi elementy na mapie, gdzie kluczem jest identyfikator studenta, a wartość jest wartością studenta.
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);
Wynik :
{1=test1, 2=test2, 3=test3}
Collectors.toMap ma inną implementację Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
.Funkcja merge jest najczęściej używana do wyboru nowej wartości lub zachowania starej wartości, jeśli klucz jest powtarzany podczas dodawania nowego elementu do mapy z listy.
MergeFunction często wygląda następująco: (s1, s2) -> s1
aby zachować wartość odpowiadającą powtórzonemu kluczowi, lub (s1, s2) -> s2
aby wprowadzić nową wartość dla powtarzanego klucza.
Zbieranie elementów do mapy kolekcji
Przykład: z ArrayList na Map <String, List <>>
Często wymaga utworzenia mapy listy z listy podstawowej. Przykład: od studenta listy musimy stworzyć mapę listy tematów dla każdego ucznia.
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);
Wynik:
{ Robert=[LITERATURE],
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE],
Davis=[MATH, SCIENCE, GEOGRAPHY] }
Przykład: od ArrayList do 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);
Wynik:
{ 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]} }
Ściągawka
Cel | Kod |
---|---|
Zbierz na List | Collectors.toList() |
Zbieraj do ArrayList o wstępnie przydzielonym rozmiarze | Collectors.toCollection(() -> new ArrayList<>(size)) |
Zbierz do Set | Collectors.toSet() |
Zbieraj do Set z lepszą wydajnością iteracji | Collectors.toCollection(() -> new LinkedHashSet<>()) |
Zbieraj do rozróżniania wielkości liter Set<String> | Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)) |
EnumSet<AnEnum> do EnumSet<AnEnum> (najlepsza wydajność dla wyliczeń) | Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class)) |
Zbieraj na Map<K,V> z unikalnymi kluczami | Collectors.toMap(keyFunc,valFunc) |
Odwzoruj MyObject.getter () na unikalny MyObject | Collectors.toMap(MyObject::getter, Function.identity()) |
Odwzoruj MyObject.getter () na wiele MyObjects | Collectors.groupingBy(MyObject::getter) |
Nieskończone strumienie
Możliwe jest wygenerowanie Stream
, który się nie kończy. Wywołanie metody terminalowej w nieskończonym Stream
powoduje, że Stream
wchodzi w nieskończoną pętlę. Metodę limit
Stream
można wykorzystać do ograniczenia liczby terminów Stream
przetwarzanych przez Javę.
Ten przykład generuje Stream
wszystkich liczb naturalnych, zaczynając od cyfry 1. Każdy kolejny termin Stream
jest o jeden wyższy od poprzedniego. Wywołując metodę limitu tego Stream
, brane są pod uwagę i drukowane tylko pięć pierwszych warunków Stream
.
// 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);
Wynik:
1
2)
3)
4
5
Innym sposobem generowania nieskończonego strumienia jest użycie metody Stream.generate . Ta metoda przyjmuje lambda typu Dostawca .
// 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);
Zużycie strumieni
Stream
będzie przemierzany tylko wtedy, gdy istnieje operacja terminalowa , taka jak count()
, collect()
lub forEach()
. W przeciwnym razie nie zostanie wykonana żadna operacja na Stream
.
W poniższym przykładzie żadna operacja terminalowa nie została dodana do Stream
, więc operacja filter()
nie zostanie wywołana i nie zostanie wygenerowane żadne wyjście, ponieważ peek()
NIE jest operacją terminalową .
IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);
Jest to sekwencja Stream
z prawidłową operacją terminalową , w ten sposób generowane jest wyjście.
Można również użyć forEach
zamiast peek
:
IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println);
Wynik:
2)
4
6
8
Po wykonaniu operacji terminalu Stream
jest zużywany i nie można go ponownie użyć.
Chociaż danego obiektu strumienia nie można ponownie użyć, łatwo jest utworzyć iterowalny Iterable
wielokrotnego użytku, który deleguje do potoku strumienia. Może to być przydatne do zwracania zmodyfikowanego widoku zestawu danych na żywo bez konieczności gromadzenia wyników w tymczasowej strukturze.
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);
}
Wynik:
bla
bar
bla
bar
Działa to, ponieważ Iterable
deklaruje pojedynczą abstrakcyjną metodę Iterator<T> iterator()
. To sprawia, że jest to skutecznie funkcjonalny interfejs, zaimplementowany przez lambda, który tworzy nowy strumień dla każdego połączenia.
Ogólnie rzecz biorąc, Stream
działa w sposób pokazany na poniższym obrazie:
UWAGA : Sprawdzanie argumentów jest zawsze wykonywane, nawet bez operacji terminalu :
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()");
}
Wynik:
Otrzymaliśmy wyjątek NullPointerException, ponieważ wartość null została przekazana jako argument funkcji filter ()
Tworzenie mapy częstotliwości
Kolektor groupingBy(classifier, downstream)
pozwala na zbieranie elementów Stream
na Map
poprzez klasyfikację każdego elementu w grupie i wykonanie operacji downstream na elementach sklasyfikowanych w tej samej grupie.
Klasycznym przykładem tej zasady jest użycie Map
do zliczenia występowania elementów w Stream
. W tym przykładzie klasyfikator jest po prostu funkcją tożsamości, która zwraca element takim, jaki jest. Poniższa operacja liczy liczbę równych elementów za pomocą counting()
.
Stream.of("apple", "orange", "banana", "apple")
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.forEach(System.out::println);
Operacja downstream sama jest kolektorem ( Collectors.counting()
), który działa na elementach typu String i daje wynik typu Long
. Wynikiem wywołania metody collect
jest Map<String, Long>
.
To dałoby następujące wyniki:
banan = 1
pomarańczowy = 1
jabłko = 2
Strumień równoległy
Uwaga: Przed podjęciem decyzji, którego Stream
użyć, zapoznaj się z zachowaniem ParallelStream vs. Sequential Stream .
Jeśli chcesz wykonywać operacje Stream
jednocześnie, możesz użyć jednego z tych sposobów.
List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();
Lub:
Stream<String> aParallelStream = data.parallelStream();
Aby wykonać operacje zdefiniowane dla strumienia równoległego, zadzwoń do operatora terminalu:
aParallelStream.forEach(System.out::println);
(Możliwe) wyjście ze Stream
równoległego:
Trzy
Cztery
Jeden
Dwa
Pięć
Kolejność może ulec zmianie, ponieważ wszystkie elementy są przetwarzane równolegle (co może przyspieszyć). Użyj parallelStream
strumienia, gdy zamówienie nie ma znaczenia.
Wpływ na wydajność
W przypadku pracy w sieci Stream
równoległy może obniżyć ogólną wydajność aplikacji, ponieważ wszystkie Stream
równoległe korzystają ze wspólnej puli wątków łączących rozwidlenie dla sieci.
Z drugiej strony Stream
równoległy może znacznie poprawić wydajność w wielu innych przypadkach, w zależności od liczby dostępnych rdzeni w uruchomionym procesorze w danym momencie.
Konwertowanie strumienia opcjonalnego na strumień wartości
Może być konieczne przekonwertowanie Stream
emitującego Optional
na Stream
wartości, emitującego tylko wartości z istniejącego Optional
. (tj .: bez wartości null
i bez czynienia z 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]
Tworzenie strumienia
Wszystkie java Collection<E>
mają metody stream()
i parallelStream()
, z których można zbudować Stream<E>
:
Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();
Stream<E>
można utworzyć z tablicy przy użyciu jednej z dwóch metod:
String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);
Różnica między Arrays.stream()
a Stream.of()
polega na tym, że Stream.of()
ma parametr varargs, więc można go używać w następujący sposób:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Istnieją również prymitywne Stream
, których możesz użyć. Na przykład:
IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
Te prymitywne strumienie można również konstruować za pomocą metody Arrays.stream()
:
IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });
Możliwe jest utworzenie Stream
z tablicy o określonym zakresie.
int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);
Zauważ, że każdy prymitywny strumień można przekonwertować na strumień typu pudełkowego za pomocą metody boxed
:
Stream<Integer> integerStream = intStream.boxed();
Może to być przydatne w niektórych przypadkach, jeśli chcesz gromadzić dane, ponieważ w pierwotnym strumieniu nie ma żadnej metody collect
, która przyjmuje argument Collector
jako argument.
Ponowne użycie operacji pośrednich łańcucha strumienia
Strumień jest zamykany po każdym wywołaniu operacji terminalowej. Ponowne użycie strumienia operacji pośrednich, gdy zmienia się tylko operacja terminalowa. moglibyśmy stworzyć dostawcę strumienia, aby zbudować nowy strumień z już skonfigurowanymi wszystkimi operacjami pośrednimi.
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
Tablice int[]
można konwertować na List<Integer>
przy użyciu strumieni
int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());
Znajdowanie statystyk dotyczących strumieni numerycznych
Java 8 udostępnia klasy o IntSummaryStatistics
, DoubleSummaryStatistics
i LongSummaryStatistics
które dają obiekt stanu do zbierania statystyk, takich jak count
, min
, max
, sum
i 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);
Co spowoduje:
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}
Zdobądź kawałek strumienia
Przykład: Uzyskaj Stream
30 elementów zawierający od 21 do 50 (włącznie) element kolekcji.
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);
Uwagi:
-
IllegalArgumentException
jestIllegalArgumentException
jeślin
jest ujemne lubmaxSize
jest ujemne - zarówno
skip(long)
ilimit(long)
są operacjami pośrednimi - jeśli strumień zawiera mniej niż
n
elementów,skip(n)
zwraca pusty strumień - zarówno
skip(long)
ilimit(long)
to tanie operacje na potokach sekwencyjnych, ale mogą być dość drogie na zamówionych równoległych potokach
Połącz strumienie
Zmienna deklaracja dla przykładów:
Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");
Przykład 1 - Połącz dwa Stream
final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());
concat1.forEach(System.out::print);
// prints: abc123
Przykład 2 - Połącz więcej niż dwa 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
Alternatywnie, aby uprościć składnię zagnieżdżoną concat()
Stream
s można również połączyć z flatMap()
:
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
Zachowaj ostrożność podczas konstruowania Stream
z powtarzalnej konkatenacji, ponieważ dostęp do elementu głęboko konkatenowanego Stream
może spowodować głębokie łańcuchy wywołań lub nawet StackOverflowException
.
IntStream to String
Java nie ma strumienia Char , więc podczas pracy z String
i konstruowania Stream
of Character
s, opcją jest uzyskanie IntStream
punktów kodu za pomocą metody String.codePoints()
. Tak więc IntStream
można uzyskać jak poniżej:
public IntStream stringToIntStream(String in) {
return in.codePoints();
}
Konwersja jest nieco bardziej zaangażowana, np. IntStreamToString. Można to zrobić w następujący sposób:
public String intStreamToString(IntStream intStream) {
return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}
Sortuj za pomocą strumienia
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);
Wynik:
[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]
Możliwe jest również użycie innego mechanizmu porównywania, ponieważ istnieje przeciążona sorted
wersja, która przyjmuje argument jako argument.
Ponadto do sortowania można użyć wyrażenia lambda:
List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());
To dałoby wynik [Sydney, New York, Mumbai, London, California, Amsterdam]
Możesz użyć Comparator.reverseOrder()
aby mieć komparator, który narzuca reverse
naturalnego uporządkowania.
List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
Strumienie prymitywów
Java zapewnia specjalistyczne Stream
dla trzech rodzajów prymitywów IntStream
(dla int
), LongStream
(dla long
s) i DoubleStream
(dla double
s). Poza tym, że są zoptymalizowanymi implementacjami dla odpowiednich prymitywów, udostępniają także kilka specyficznych metod terminalowych, zwykle dla operacji matematycznych. Na przykład:
IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0
Zbieraj wyniki strumienia do tablicy
Analogowe, aby uzyskać kolekcję dla Stream
przez collect()
, tablicę można uzyskać za pomocą metody Stream.toArray()
:
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
to specjalny rodzaj odwołania do metody: odwołanie do konstruktora.
Znalezienie pierwszego elementu, który pasuje do predykatu
Można znaleźć pierwszy element Stream
który pasuje do warunku.
W tym przykładzie znajdziemy pierwszą Integer
której kwadrat wynosi ponad 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
To wyrażenie zwróci OptionalInt
z wynikiem.
Zauważ, że w przypadku nieskończonego Stream
Java będzie sprawdzać każdy element, aż znajdzie wynik. W przypadku skończonego Stream
, jeśli Java zabraknie elementów, ale nadal nie może znaleźć wyniku, zwraca pusty OptionalInt
.
Używanie IntStream do iteracji po indeksach
Stream
elementów zwykle nie pozwalają na dostęp do wartości indeksu bieżącego elementu. Aby wykonać iterację po tablicy lub ArrayList
, mając dostęp do indeksów, użyj 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);
Metoda range(start, endExclusive)
zwraca kolejny ÌntStream
a mapToObj(mapper)
zwraca strumień String
.
Wynik:
# 1 Jon
# 2 Darin
# 3 Bauke
# 4 Hans
# 5 Marc
Jest to bardzo podobne do używania normalnej pętli for
z licznikiem, ale z korzyścią dla potokowania i równoległości:
for (int i = 0; i < names.length; i++) {
String newName = String.format("#%d %s", i + 1, names[i]);
System.out.println(newName);
}
Spłaszcz strumienie za pomocą flatMap ()
Stream
elementów, które z kolei można przesyłać strumieniowo, można spłaszczyć w jeden ciągły Stream
:
Tablicę listy elementów można przekształcić w jedną listę.
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]
Mapa zawierająca listę przedmiotów, ponieważ wartości mogą być spłaszczone do listy łączonej
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
Map
może być spłaszczona do jednego ciągłego 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]
Utwórz mapę na podstawie strumienia
Prosta obudowa bez duplikatów kluczy
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}
Żeby było bardziej deklaratywny, możemy użyć metody statycznej w Function
interfejsu - Function.identity()
. Możemy zastąpić ten element -> element
lambda element -> element
Function.identity()
.
Przypadek, w którym mogą istnieć duplikaty kluczy
W javadoc dla Collectors.toMap
stwierdza się:
Jeśli zmapowane klucze zawierają duplikaty (zgodnie z
Object.equals(Object)
),Object.equals(Object)
IllegalStateException
jestObject.equals(Object)
podczas wykonywania operacji zbierania. Jeśli zmapowane klucze mogą mieć duplikaty, użyjtoMap(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}
BinaryOperator
przekazany do Collectors.toMap(...)
generuje wartość do zapisania w przypadku kolizji. To może:
- zwraca starą wartość, aby pierwsza wartość w strumieniu miała pierwszeństwo,
- zwraca nową wartość, aby ostatnia wartość w strumieniu miała pierwszeństwo, lub
- połączyć stare i nowe wartości
Grupowanie według wartości
Możesz użyć Collectors.groupingBy
jeśli chcesz wykonać równoważność operacji kaskadowej bazy danych „grupuj”. Aby to zilustrować, poniższe tworzy mapę, w której nazwiska osób są mapowane na nazwiska:
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]}
Generowanie losowych ciągów za pomocą strumieni
Czasami przydatne jest tworzenie losowych Strings
, na przykład jako identyfikator sesji dla usługi internetowej lub hasło początkowe po rejestracji w aplikacji. Można to łatwo osiągnąć za pomocą Stream
.
Najpierw musimy zainicjować generator liczb losowych. Aby zwiększyć bezpieczeństwo wygenerowany String
s, to jest dobry pomysł, aby użyć SecureRandom
.
Uwaga : Utworzenie SecureRandom
jest dość kosztowne, dlatego najlepszą praktyką jest zrobienie tego tylko raz i wywołanie jednej z jego metod setSeed()
od czasu do czasu, aby go zresetować.
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
Podczas tworzenia losowych String
znaków zwykle chcemy, aby używali tylko niektórych znaków (np. Tylko liter i cyfr). Dlatego możemy stworzyć metodę zwracającą wartość boolean
którą można później wykorzystać do filtrowania 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);
}
Następnie możemy wykorzystać RNG do wygenerowania losowego ciągu o określonej długości, zawierającego useThisCharacter
znaków, który przechodzi naszą kontrolę useThisCharacter
.
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;
}
Używanie strumieni do implementacji funkcji matematycznych
Stream
, a zwłaszcza IntStream
, to elegancki sposób implementacji warunków sumowania (∑). Zakresy Stream
mogą być wykorzystane jako granice sumowania.
Np. Przybliżenie Pi przez Madhavę podaje wzór (źródło: wikipedia ):
Można to obliczyć z dowolną precyzją. Np. Dla 101 terminów:
double pi = Math.sqrt(12) *
IntStream.rangeClosed(0, 100)
.mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
.sum();
Uwaga: Z double
precyzją wybranie górnej granicy 29 jest wystarczające, aby uzyskać wynik, którego nie można odróżnić od Math.Pi
Używanie strumieni i odniesień do metod do pisania procesów samodokumentujących
Odnośniki do metod tworzą doskonały kod samokontrujący, a użycie odniesień do metod w Stream
sprawia, że skomplikowane procesy są łatwe do odczytania i zrozumienia. Rozważ następujący kod:
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;
}
Ta ostatnia metoda przepisana przy użyciu Stream
i odniesień do metod jest o wiele bardziej czytelna, a każdy etap procesu jest szybko i łatwo zrozumiały - nie tylko krótszy, ale także pokazuje na pierwszy rzut oka, które interfejsy i klasy są odpowiedzialne za kod na każdym etapie:
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());
}
Korzystanie ze strumieni map. Wejdź, aby zachować początkowe wartości po mapowaniu
Jeśli masz Stream
który musisz zmapować, ale chcesz również zachować wartości początkowe, możesz zmapować Stream
na Mapę. Map.Entry<K,V>
przy użyciu metody narzędzia takiej jak:
public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}
Następnie możesz użyć konwertera do przetworzenia Stream
posiadającego dostęp zarówno do oryginalnych, jak i odwzorowanych wartości:
Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
.map(entryMapper(transformer));
Następnie możesz kontynuować przetwarzanie tego Stream
w normalny sposób. Pozwala to uniknąć nakładów związanych z tworzeniem kolekcji pośredniej.
Kategorie operacji strumieniowych
Operacje strumieniowe dzielą się na dwie główne kategorie, operacje pośrednie i końcowe oraz dwie podkategorie, bezpaństwowe i stanowe.
Operacje pośrednie:
Operacja pośrednia jest zawsze leniwa , na przykład prosta Stream.map
. Nie jest wywoływany, dopóki strumień nie zostanie faktycznie wykorzystany. Można to łatwo zweryfikować:
Arrays.asList(1, 2 ,3).stream().map(i -> {
throw new RuntimeException("not gonna happen");
return i;
});
Operacje pośrednie są typowymi elementami składowymi strumienia, połączonymi łańcuchem po źródle i zwykle po nich następuje operacja terminalowa uruchamiająca łańcuch strumienia.
Operacje terminalowe
Operacje terminalowe są tym, co wyzwala zużycie strumienia. Niektóre z bardziej powszechne są Stream.forEach
lub Stream.collect
. Zazwyczaj są one umieszczane po szeregu operacji pośrednich i prawie zawsze są chętni .
Operacje bezstanowe
Bezpaństwowość oznacza, że każdy element jest przetwarzany bez kontekstu innych elementów. Operacje bezstanowe umożliwiają wydajne przetwarzanie pamięci strumieni. Operacje takie jak Stream.map
i Stream.filter
, które nie wymagają informacji o innych elementach strumienia, są uważane za bezstanowe.
Operacje stanowe
Stanowość oznacza, że operacja na każdym elemencie zależy od (niektórych) innych elementów strumienia. Wymaga to zachowania stanu. Operacje stanowe mogą zerwać z długimi lub nieskończonymi strumieniami. Operacje takie jak Stream.sorted
wymagają Stream.sorted
całego strumienia przed wysłaniem dowolnego elementu, co spowoduje przerwanie wystarczająco długiego strumienia elementów. Można to wykazać długim strumieniem ( uruchamianym na własne ryzyko ):
// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);
Spowoduje to brak pamięci z powodu Stream.sorted
:
// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);
Konwertowanie iteratora na strumień
Użyj Spliterators.spliterator()
lub Spliterators.spliteratorUnknownSize()
aby przekonwertować iterator na strumień:
Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);
Redukcja za pomocą strumieni
Redukcja to proces nakładania operatora binarnego na każdy element strumienia w celu uzyskania jednej wartości.
Metoda sum()
IntStream
jest przykładem redukcji; ma zastosowanie do każdego terminu Strumienia, co daje jedną wartość końcową:
Jest to równoważne z (((1+2)+3)+4)
Metoda reduce
strumienia umożliwia utworzenie niestandardowej redukcji. Możliwe jest użycie metody reduce
do implementacji metody sum()
:
IntStream istr;
//Initialize istr
OptionalInt istr.reduce((a,b)->a+b);
Optional
jest wersja Optional
aby można było odpowiednio obsługiwać puste strumienie.
Innym przykładem redukcji jest połączenie Stream<LinkedList<T>>
w pojedynczy 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;
});
Możesz także podać element tożsamości . Na przykład elementem tożsamości dla dodania jest 0, ponieważ x+0==x
. W przypadku mnożenia elementem tożsamości jest 1, ponieważ x*1==x
. W powyższym przypadku elementem tożsamości jest pusta lista LinkedList<T>
, ponieważ jeśli dodasz pustą listę do innej listy, lista, do której „dodajesz”, nie ulegnie zmianie:
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;
});
Zauważ, że gdy podany jest element tożsamości, zwracana wartość nie jest zawijana w Optional
jeśli wywoływany w pustym strumieniu, funkcja replace reduce()
zwróci element tożsamości.
Operator binarny musi być również asocjacyjny , co oznacza, że (a+b)+c==a+(b+c)
. Wynika to z faktu, że elementy można zmniejszać w dowolnej kolejności. Na przykład powyższą redukcję dodawania można wykonać w następujący sposób:
Ta redukcja jest równoważna z pisaniem ((1+2)+(3+4))
. Właściwość asocjatywności umożliwia także Java równoległą redukcję Stream
- część Strumienia może być zmniejszona przez każdy procesor, przy czym redukcja łączy wynik każdego procesora na końcu.
Łączenie strumienia z jednym ciągiem
Przypadek użycia że spotyka się często, tworzy String
ze strumienia, gdzie strumień-elementy są oddzielone od pewnego charakteru. Można do tego użyć metody Collectors.joining()
, jak w poniższym przykładzie:
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);
Wynik:
JABŁKO, BANAN, POMARAŃCZA, GRUSZKA
Metoda Collectors.joining()
może również obsługiwać prefiksy i postfiksy:
String result = fruitStream.filter(s -> s.contains("e"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", ", "Fruits: ", "."));
System.out.println(result);
Wynik:
Owoce: JABŁKO, POMARAŃCZOWY, PERŁOWY.