Kiedy należy używać strumieni?

99

Właśnie natknąłem się na pytanie, kiedy używam a Listi jego stream()metody. Chociaż wiem, jak ich używać, nie jestem pewien, kiedy ich używać.

Na przykład mam listę zawierającą różne ścieżki do różnych lokalizacji. Teraz chciałbym sprawdzić, czy pojedyncza podana ścieżka zawiera którąkolwiek ze ścieżek określonych na liście. Chciałbym zwrócićboolean podstawie tego, czy warunek został spełniony.

Oczywiście nie jest to trudne zadanie samo w sobie. Ale zastanawiam się, czy powinienem używać strumieni, czy pętli for (-each).

Lista

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

Przykład - Stream

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

Przykład - pętla For-Each

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

Zwróć uwagę, że pathparametr jest zawsze pisany małymi literami .

Moje pierwsze przypuszczenie jest takie, że podejście for-each jest szybsze, ponieważ pętla powróci natychmiast, jeśli warunek zostanie spełniony. Podczas gdy strumień nadal będzie przechodził przez wszystkie wpisy na liście, aby zakończyć filtrowanie.

Czy moje założenie jest prawidłowe? Jeśli tak, dlaczego (a raczej kiedy ) miałbym stream()wtedy użyć ?

mcuenez
źródło
11
Strumienie są bardziej wyraziste i czytelne niż tradycyjne pętle for. W dalszej części musisz uważać na elementy wewnętrzne warunku jeśli-to, itp. Wyrażenie strumienia jest bardzo jasne: konwertuj nazwy plików na małe litery, następnie filtruj według czegoś, a następnie zliczaj, zbieraj itp. Wynik: bardzo iteracyjny wyrażenie przepływu obliczeń.
Jean-Baptiste Yunès
12
Nie ma takiej potrzeby new String[]{…}. Po prostu użyjArrays.asList("my/path/one", "my/path/two")
Holger
4
Jeśli Twoim źródłem jest a String[], nie ma potrzeby dzwonić Arrays.asList. Możesz po prostu przesyłać strumieniowo przez tablicę za pomocą Arrays.stream(array). Nawiasem mówiąc, mam trudności ze zrozumieniem ogólnego celu isExcludedtestu. Czy to naprawdę interesujące, czy element EXCLUDE_PATHSjest dosłownie zawarty gdzieś na ścieżce? To znaczy isExcluded("my/path/one/foo/bar/baz")wrócę true, a także isExcluded("foo/bar/baz/my/path/one/")
Holger
3
Świetnie, nie byłem świadomy tej Arrays.streammetody, dzięki za wskazanie tego. Rzeczywiście, przykład, który opublikowałem, wydaje się zupełnie bezużyteczny dla nikogo poza mną. Zdaję sobie sprawę z zachowania isExcludedmetody, ale tak naprawdę jest to coś, czego dla siebie potrzebuję, więc odpowiadając na Twoje pytanie: tak , jest ciekawa z powodów, o których nie chciałbym mówić, bo nie mieściłaby się w zakresie pierwotnego pytania.
mcuenez
1
Dlaczego toLowerCasestosuje się do stałej, która jest już mała? Czy nie należy go zastosować do pathargumentu?
Sebastian Redl

Odpowiedzi:

78

Twoje założenie jest poprawne. Twoja implementacja strumienia jest wolniejsza niż pętla for.

To użycie strumienia powinno być jednak tak szybkie jak pętla for:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

Powoduje to iterację przez elementy, stosowanie String::toLowerCasei filtrowanie elementów jeden po drugim i kończenie na pierwszym elemencie pasującym .

Obie collect()& anyMatch()są operacjami terminalowymi. anyMatch()wychodzi jednak przy pierwszym znalezionym elemencie, podczas gdy collect()wymaga przetworzenia wszystkich elementów.

Stefan Pries
źródło
2
Niesamowite, o których nie wiedziałem findFirst()w połączeniu z filter(). Najwyraźniej nie umiem korzystać ze strumieni tak dobrze, jak myślałem.
mcuenez
4
W sieci jest kilka naprawdę interesujących artykułów i prezentacji na blogu na temat wydajności strumieniowego API, które okazały się bardzo pomocne w zrozumieniu, jak te rzeczy działają pod maską. Zdecydowanie mogę polecić trochę zbadania, jeśli jesteś tym zainteresowany.
Stefan Pries
Wydaje mi się, że po Twojej redakcji Twoja odpowiedź powinna zostać zaakceptowana, ponieważ odpowiedziałeś również na moje pytanie w komentarzach do drugiej odpowiedzi. Chociaż, chciałbym przyznać @ rvit34 trochę uznania za wysłanie kodu :-)
mcuenez
34

Decyzja, czy użyć strumieni, czy nie, nie powinna być podyktowana rozważaniami dotyczącymi wydajności, ale raczej czytelnością. Jeśli chodzi o wydajność, należy wziąć pod uwagę inne kwestie.

Swoim .filter(path::contains).collect(Collectors.toList()).size() > 0podejściem przetwarzasz wszystkie elementy i zbierasz je w tymczasoweList , zanim porównasz rozmiar, jednak rzadko ma to znaczenie dla strumienia składającego się z dwóch elementów.

Używanie .map(String::toLowerCase).anyMatch(path::contains)może zaoszczędzić cykle procesora i pamięć, jeśli masz znacznie większą liczbę elementów. Mimo to konwertuje to każdą Stringna reprezentację małych liter, dopóki nie zostanie znalezione dopasowanie. Oczywiście jest sens używania

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

zamiast. Nie musisz więc powtarzać konwersji na małe litery przy każdym wywołaniu isExcluded. Jeśli liczba elementów EXCLUDE_PATHSlub długości ciągów stają się naprawdę duże, możesz rozważyć użycie

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

Skompilowanie łańcucha jako wzorca wyrażenia regularnego z LITERALflagą sprawia, że ​​zachowuje się on jak zwykłe operacje na łańcuchach, ale pozwala silnikowi spędzić trochę czasu na przygotowaniach, np. Przy użyciu algorytmu Boyera Moore'a, aby być bardziej wydajnym, jeśli chodzi o faktyczne porównanie.

Oczywiście opłaca się to tylko wtedy, gdy jest wystarczająco dużo kolejnych testów, aby zrekompensować czas spędzony na przygotowaniach. Ustalenie, czy tak się stanie, jest jednym z faktycznych rozważań dotyczących wydajności, poza pierwszym pytaniem, czy ta operacja kiedykolwiek będzie miała krytyczne znaczenie dla wydajności. Nie chodzi o to, czy używać strumieni, czyfor pętli.

Nawiasem mówiąc, powyższe przykłady kodu zachowują logikę oryginalnego kodu, który wydaje mi się wątpliwy. Twoja isExcludedmetoda zwraca true, jeśli określona ścieżka zawiera którykolwiek z elementów na liście, więc wraca truedla /some/prefix/to/my/path/one, a także my/path/one/and/some/suffixlub nawet /some/prefix/to/my/path/one/and/some/suffix.

Nawet dummy/path/onerousjest uważany za spełniający kryteria, ponieważ jest containsciągiem my/path/one

Holger
źródło
Niezłe spojrzenie na możliwą optymalizację wydajności, dzięki. Odnośnie ostatniej części twojej odpowiedzi: jeśli moja odpowiedź na twój komentarz nie była satysfakcjonująca, potraktuj mój przykładowy kod jako zwykłą pomoc dla innych, aby zrozumieli, o co proszę - a nie jest to rzeczywisty kod. Możesz też zawsze edytować pytanie, jeśli masz na myśli lepszy przykład.
mcuenez
3
Przyjmuję twój komentarz, że ta operacja jest tym, czego naprawdę chcesz, więc nie ma potrzeby jej zmieniać. Po prostu zatrzymam ostatnią sekcję dla przyszłych czytelników, aby byli świadomi, że nie jest to typowa operacja, ale także, że została już omówiona i nie wymaga dalszych komentarzy…
Holger,
W rzeczywistości strumienie są idealne do optymalizacji pamięci, gdy ilość pamięci roboczej
przekracza
21

Tak. Masz rację. Twoje podejście do transmisji będzie miało pewne obciążenie. Ale możesz użyć takiej konstrukcji:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

Głównym powodem korzystania ze strumieni jest to, że sprawiają, że kod jest prostszy i łatwiejszy do odczytania.

rvit34
źródło
3
Czy anyMatchjest skrót filter(...).findFirst().isPresent()?
mcuenez
6
Tak to jest! To nawet lepsze niż moja pierwsza sugestia.
Stefan Pries
8

Celem strumieni w Javie jest uproszczenie pisania kodu równoległego. Inspiruje się programowaniem funkcjonalnym. Strumień szeregowy służy tylko do czyszczenia kodu.

Jeśli zależy nam na wydajności, powinniśmy użyć parallelStream, do którego został zaprojektowany. Szeregowy jest generalnie wolniejszy.

Jest dobry artykuł czytać o , a wydajność ForLoopStreamParallelStream .

W Twoim kodzie możemy użyć metod zakończenia, aby zatrzymać wyszukiwanie na pierwszym dopasowaniu. (anyMatch ...)

Paulo Ricardo Almeida
źródło
5
Zwróć uwagę, że w przypadku małych strumieni oraz w niektórych innych przypadkach strumień równoległy może być wolniejszy ze względu na koszt uruchomienia. A jeśli masz uporządkowaną operację terminalową zamiast nieuporządkowanej operacji równoległej, ponowna synchronizacja na końcu.
CAD 97
0

Jak inni wspominali o wielu dobrych punktach, ale ja chcę tylko wspomnieć o leniwej ocenie w ocenie strumienia. Kiedy map()tworzymy strumień ścieżek z małymi literami, nie tworzymy od razu całego strumienia, zamiast tego strumień jest konstruowany leniwie , dlatego wydajność powinna być równoważna tradycyjnej pętli for. Nie wykonuje pełnego skanowania map()i anyMatch()jest wykonywany w tym samym czasie. Gdy anyMatch()zwróci prawdę, nastąpi zwarcie.

Kaicheng Hu
źródło