Czytam o strumieniach Java i odkrywam nowe rzeczy w trakcie. Jedną z nowych rzeczy, które znalazłem, była peek()
funkcja. Prawie wszystko, co przeczytałem w Peek, mówi, że powinno się go używać do debugowania strumieni.
Co by było, gdybym miał Stream, w którym każde konto ma nazwę użytkownika, pole hasła oraz metodę login () i loggedIn ().
też mam
Consumer<Account> login = account -> account.login();
i
Predicate<Account> loggedIn = account -> account.loggedIn();
Dlaczego to miałoby być takie złe?
List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount =
accounts.stream()
.peek(login)
.filter(loggedIn)
.collect(Collectors.toList());
O ile wiem, robi dokładnie to, co jest przeznaczone. To;
- Pobiera listę kont
- Próbuje zalogować się na każde konto
- Filtruje konta, które nie są zalogowane
- Zbiera zalogowane konta na nową listę
Jakie są wady zrobienia czegoś takiego? Jest jakiś powód, dla którego nie powinienem kontynuować? Wreszcie, jeśli nie to rozwiązanie, to co?
Oryginalna wersja tego używała metody .filter () w następujący sposób;
.filter(account -> {
account.login();
return account.loggedIn();
})
java
java-8
java-stream
peek
Adam.J
źródło
źródło
forEach
może to być operacja, której chcesz, a nie innąpeek
. To, że jest w API, nie oznacza, że nie jest otwarte na nadużycia (npOptional.of
.)..peek(Account::login)
i.filter(Account::loggedIn)
; nie ma powodu, aby pisać konsumenta i predykatu, który po prostu wywołuje inną taką metodę.forEach()
ipeek()
, może działać jedynie poprzez efekty uboczne; należy ich używać ostrożnie. ”. Moja uwaga była raczej przypomnieniem, żepeek
operacja (która jest przeznaczona do debugowania) nie powinna być zastępowana przez robienie tego samego w ramach innej operacji, takiej jakmap()
lubfilter()
.Odpowiedzi:
Kluczowe wnioski z tego:
Nie używaj interfejsu API w niezamierzony sposób, nawet jeśli osiąga to twój bezpośredni cel. Takie podejście może się załamać w przyszłości i nie jest jasne dla przyszłych opiekunów.
Nie ma nic złego w rozbiciu tego na wiele operacji, ponieważ są to odrębne operacje. Nie ma nic złego w użyciu API w sposób niejasny i niezamierzony sposób, który może mieć konsekwencje, jeśli ten konkretny zachowanie jest modyfikowany w wersji przyszłości Java.
Użycie
forEach
tej operacji wyjaśniłoby opiekunowi, że każdy element ma zamierzony efekt ubocznyaccounts
i że wykonujesz jakąś operację, która może go zmienić.Jest to również bardziej konwencjonalne w tym sensie, że
peek
jest operacją pośrednią, która nie działa na całej kolekcji, dopóki nie zostanie uruchomiona operacja terminalowa, ale wforEach
rzeczywistości jest operacją terminalową. W ten sposób możesz tworzyć mocne argumenty dotyczące zachowania i przepływu kodu, zamiast zadawać pytania o to,peek
czy zachowywałby się tak samo, jakforEach
w tym kontekście.źródło
forEach
bezpośrednio w kolekcji źródłowej:accounts.forEach(a -> a.login());
login()
metoda zwróciłaboolean
wartość wskazującą na stan sukcesu…login()
zwraca aboolean
, możesz użyć go jako predykatu, który jest najczystszym rozwiązaniem. Nadal ma efekt uboczny, ale to jest w porządku, o ile nie przeszkadza, tj.login
Proces „jednegoAccount
nie ma wpływu na proces logowania” drugiegoAccount
.Ważną rzeczą, którą musisz zrozumieć, jest to, że strumienie są sterowane przez działanie terminala . Operacja terminala określa, czy wszystkie elementy mają zostać przetworzone, czy w ogóle. Podobnie
collect
jest z operacją, która przetwarza każdy element, podczas gdyfindAny
może przestać przetwarzać elementy, gdy napotka pasujący element.I
count()
może w ogóle nie przetwarzać żadnych elementów, jeśli może określić rozmiar strumienia bez przetwarzania elementów. Ponieważ jest to optymalizacja, która nie została przeprowadzona w Javie 8, ale będzie w Javie 9, mogą wystąpić niespodzianki, gdy przełączysz się na Javę 9 i kod będzie zależał odcount()
przetwarzania wszystkich elementów. Jest to również związane z innymi szczegółami zależnymi od implementacji, np. Nawet w Javie 9 implementacja referencyjna nie będzie w stanie przewidzieć rozmiaru nieskończonego strumienia źródła w połączeniu z,limit
podczas gdy nie ma fundamentalnego ograniczenia uniemożliwiającego takie przewidywanie.Ponieważ
peek
umożliwia „wykonywanie podanej akcji na każdym elemencie, gdy elementy są zużywane z wynikowego strumienia ”, nie nakazuje przetwarzania elementów, ale wykona akcję w zależności od potrzeb operacji terminala. Oznacza to, że musisz używać go z wielką ostrożnością, jeśli potrzebujesz określonego przetwarzania, np. Chcesz zastosować akcję na wszystkich elementach. Działa, jeśli operacja terminala gwarantuje przetworzenie wszystkich elementów, ale nawet wtedy musisz mieć pewność, że następny programista nie zmieni operacji terminalu (lub zapomnisz o tym subtelnym aspekcie).Ponadto, chociaż strumienie gwarantują utrzymanie kolejności napotkania dla określonej kombinacji operacji nawet dla strumieni równoległych, te gwarancje nie mają zastosowania
peek
. Podczas zbierania do listy wynikowa lista będzie miała właściwą kolejność dla uporządkowanych równoległych strumieni, alepeek
akcja może zostać wywołana w dowolnej kolejności i jednocześnie.Więc najbardziej użyteczną rzeczą, jaką możesz zrobić,
peek
jest sprawdzenie, czy element strumienia został przetworzony, co jest dokładnie tym, co mówi dokumentacja API:źródło
Być może ogólną zasadą powinno być to, że jeśli używasz wglądu poza scenariusz „debugowania”, powinieneś to robić tylko wtedy, gdy jesteś pewien, jakie są warunki kończenia i pośredniego filtrowania. Na przykład:
wydaje się być dobrym przypadkiem, w którym chcesz, w jednej operacji przekształcić wszystkie Foos w Bars i powiedzieć im wszystkim cześć.
Wydaje się bardziej wydajne i eleganckie niż coś takiego:
i nie kończy się dwukrotnym iterowaniem kolekcji.
źródło
Powiedziałbym, że
peek
zapewnia to możliwość zdecentralizowania kodu, który może mutować obiekty strumieniowe lub modyfikować stan globalny (na ich podstawie), zamiast upychać wszystko w prostą lub złożoną funkcję przekazaną do metody terminala.Teraz pytanie może brzmieć: czy powinniśmy mutować obiekty strumieniowe, czy zmieniać stan globalny z poziomu funkcji w programowaniu w języku funkcjonalnym w języku Java ?
Jeśli odpowiedź na którekolwiek z powyższych 2 pytań brzmi tak (lub: w niektórych przypadkach tak),
peek()
to zdecydowanie nie jest to tylko dla celów debugowania , z tego samego powodu, któryforEach()
nie jest tylko do celów debugowania .Dla mnie, wybierając między
forEach()
ipeek()
, wybieram następujące: Czy chcę, aby fragmenty kodu, które mutują obiekty strumienia, były dołączane do komponentu, czy też chcę, aby były dołączane bezpośrednio do strumienia?Myślę, że
peek()
będzie lepiej sparować z metodami java9. np.takeWhile()
może być konieczne podjęcie decyzji, kiedy zatrzymać iterację na podstawie już zmutowanego obiektu, więc parowanie go zforEach()
nie miałoby takiego samego efektu.PS
map()
Nigdzie się nie odwoływałem, ponieważ w przypadku, gdy chcemy mutować obiekty (lub stan globalny), zamiast generować nowe obiekty, działa to dokładnie tak, jakpeek()
.źródło
Chociaż zgadzam się z większością powyższych odpowiedzi, mam jeden przypadek, w którym użycie wglądu wydaje się najczystszym sposobem.
Podobnie jak w przypadku użycia, załóżmy, że chcesz filtrować tylko na aktywnych kontach, a następnie zalogować się na te konta.
Wgląd jest pomocny, aby uniknąć zbędnego wywołania bez konieczności dwukrotnego iterowania kolekcji:
źródło
Funkcjonalnym rozwiązaniem jest uczynienie obiektu konta niezmiennym. Zatem account.login () musi zwrócić nowy obiekt konta. Oznacza to, że operacja mapy może służyć do logowania zamiast wglądu.
źródło