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:

  1. Utwórz Stream<String> zawierający sekwencyjnie uporządkowany Stream owoców String elementów przy użyciu statycznej metody fabrycznej Stream.of(values) .

  2. 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 .

  3. Operacja map() przekształca każdy element za pomocą danej funkcji, zwanej maperem. W tym przypadku każdy String owocowy jest odwzorowywany na wersję String wielkimi literami przy użyciu odwołania do metody String::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 w Stream<String> wywołanie .map(String::isEmpty) zwraca Stream<Boolean>

  4. Operacja sorted() sortuje elementy Stream zgodnie z ich naturalną kolejnością (leksykograficznie, w przypadku ciągu String ).

  5. Na koniec forEach(action) wykonuje akcję, która działa na każdy element Stream , 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 obiekt Stream staje się bezużyteczny.

Połączone operacje

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ów Stream 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

Żyj na Ideone

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

Żyj na Ideone


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);

Żyj na Ideone

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); 

Żyj na Ideone

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:

Operacja przesyłania strumieniowego

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()");
}

Żyj na Ideone

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 .

Java SE 8
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:

Java SE 8
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 jest IllegalArgumentException jeśli n jest ujemne lub maxSize jest ujemne
  • zarówno skip(long) i limit(long) są operacjami pośrednimi
  • jeśli strumień zawiera mniej niż n elementów, skip(n) zwraca pusty strumień
  • zarówno skip(long) i limit(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 jest Object.equals(Object) podczas wykonywania operacji zbierania. Jeśli zmapowane klucze mogą mieć duplikaty, użyj toMap(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]}

Żyj na Ideone

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 ): Przybliżenie Madhavy

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ą: Zmniejszenie sumy

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:

Inne zmniejszenie sumy

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.

Żyj na Ideone



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow