Pośrednie operacje strumieniowe nie są obliczane na podstawie liczby

33

Wygląda na to, że mam problem ze zrozumieniem, w jaki sposób Java komponuje operacje strumieniowe do potoku strumieniowego.

Podczas wykonywania następującego kodu

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

Konsola drukuje tylko 4. StringBuilderObiekt nadal ma wartość "".

Po dodaniu operacji filtrowania: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

Wyjście zmienia się na:

4
1234

W jaki sposób ta pozornie zbędna operacja filtrowania zmienia zachowanie złożonego potoku strumienia?

atalantus
źródło
2
Ciekawy !!!
uneq95
3
Wyobrażam sobie, że jest to zachowanie specyficzne dla implementacji; być może dlatego, że pierwszy strumień ma znany rozmiar, ale drugi nie, a rozmiar określa, czy operacje pośrednie są wykonywane.
Andy Turner
Co się stanie, jeśli odwrócisz filtr i mapę?
Andy Turner
Po zaprogramowaniu trochę w Haskell pachnie trochę leniwą oceną. Wyszukiwarka google wróciła, że ​​strumienie rzeczywiście mają pewne lenistwo. Czy tak może być? I bez filtra, jeśli Java jest wystarczająco sprytna, nie ma potrzeby wykonywania mapowania.
Frederik
@AndyTurner Daje ten sam wynik, nawet przy cofnięciu
uneq95

Odpowiedzi:

39

Operacja count()terminalu w mojej wersji JDK kończy się na wykonaniu następującego kodu:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

Jeśli filter()w potoku operacji znajduje się operacja, wielkość strumienia, który jest znany na początku, nie może być już znana (ponieważ filtermoże odrzucić niektóre elementy strumienia). Tak więc ifblok nie jest wykonywany, operacje pośrednie są wykonywane, a StringBuilder jest modyfikowany.

Z drugiej strony, jeśli masz tylko map()w potoku, liczba elementów w strumieniu jest na pewno taka sama jak początkowa liczba elementów. Tak więc blok if jest wykonywany, a rozmiar jest zwracany bezpośrednio, bez oceny operacji pośrednich.

Zwróć uwagę, że przekazana lambda map()narusza umowę zdefiniowaną w dokumentacji: ma to być bezpaństwowa operacja bezstanowa, ale nie jest bezpaństwowa. Tak więc posiadanie innego wyniku w obu przypadkach nie może być uważane za błąd.

JB Nizet
źródło
Ponieważ flatMap()może być w stanie zmienić liczbę elementów, czy to był powód, dla którego początkowo był chętny (teraz leniwy)? Tak więc alternatywą byłoby użycie forEach()i liczenie osobno, jeśli map()w obecnej formie narusza umowę, tak sądzę.
Frederik
3
Jeśli chodzi o flatMap, nie sądzę. Tak było, AFAIK, ponieważ początkowo łatwiej było sprawić, by był chętny. Tak, użycie strumienia z mapą () do wywołania efektów ubocznych jest złym pomysłem.
JB Nizet
Czy masz sugestię, jak osiągnąć pełną wydajność 4 1234bez korzystania z dodatkowego filtra lub wywoływania efektów ubocznych w operacji map ()?
atalantus
1
int count = array.length; String result = String.join("", array);
JB Nizet
1
lub możesz użyć forEach, jeśli naprawdę chcesz użyć StringBuilder, lub możesz użyćCollectors.joining("")
njzk2
19

W jdk-9 było to wyraźnie udokumentowane w dokumentach Java

Unikanie skutków ubocznych może być również zaskakujące. Z wyjątkiem operacji terminalowych forEach i forEachOrDER, skutki uboczne parametrów behawioralnych nie zawsze mogą być wykonywane, gdy implementacja strumienia może zoptymalizować wykonanie parametrów behawioralnych bez wpływu na wynik obliczeń. (W celu uzyskania konkretnego przykładu zobacz notatkę API udokumentowaną w operacji liczenia ).

Uwaga API:

Implementacja może nie wykonywać potoku strumienia (sekwencyjnie lub równolegle), jeśli jest w stanie obliczyć liczbę bezpośrednio ze źródła strumienia. W takich przypadkach żaden element źródłowy nie zostanie przemierzony i nie zostaną ocenione żadne operacje pośrednie. Może to mieć wpływ na parametry behawioralne z efektami ubocznymi, które są zdecydowanie odradzane, z wyjątkiem nieszkodliwych przypadków, takich jak debugowanie. Weźmy na przykład następujący strumień:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

Liczba elementów objętych źródłem strumienia, Listą, jest znana, a operacja pośrednia, peek, nie wstrzykuje ani nie usuwa elementów ze strumienia (jak może być w przypadku operacji flatMap lub filtrów). Zatem liczba jest wielkością listy i nie ma potrzeby wykonywania potoku i, jako efekt uboczny, wydrukuj elementy listy.

Deadpool
źródło
0

Nie do tego służy .map. Ma to służyć do przekształcania strumienia „Coś” w strumień „Coś innego”. W tym przypadku używasz mapy, aby dołączyć ciąg do zewnętrznego Stringbuilder, po czym masz strumień „Stringbuilder”, z których każdy został utworzony przez operację mapowania, dodając jeden numer do oryginalnego Stringbuilder.

Strumień faktycznie nie robi nic z odwzorowanymi wynikami w strumieniu, więc całkowicie uzasadnione jest założenie, że procesor może pominąć ten krok. Liczysz się na efekty uboczne, które psują funkcjonalny model mapy. Lepiej skorzystasz z usługi forEach. Wykonaj liczenie jako osobny strumień w całości lub umieść licznik za pomocą AtomicInt w forEach.

Filtr zmusza go do uruchomienia zawartości strumienia, ponieważ musi on teraz zrobić coś sensownie znaczącego z każdym elementem strumienia.

DaveB
źródło