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ądkowanyStreamowocówStringelementó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żdyStringowocowy jest odwzorowywany na wersjęStringwielkimi 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 elementyStreamzgodnie 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
Streamsą wykonywane z powodu operacji terminalu. Bez operacji terminalowej strumień nie jest przetwarzany. Strumieni nie można ponownie użyć. Po wywołaniu operacji terminalowej obiektStreamstaje 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
Streamzasadniczo nie musi być zamknięty. Wymagane jest jedynie zamknięcie strumieni działających na kanałach IO. Większość typówStreamnie 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:
-
IllegalArgumentExceptionjestIllegalArgumentExceptionjeślinjest ujemne lubmaxSizejest ujemne - zarówno
skip(long)ilimit(long)są operacjami pośrednimi - jeśli strumień zawiera mniej niż
nelementó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)IllegalStateExceptionjestObject.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.


