Java 스트림에서 실제로는 디버깅을 위해서만 엿볼 수 있습니까?
Java 스트림에 대해 읽고 새로운 진행 사항을 발견하고 있습니다. 내가 찾은 새로운 것 중 하나는 peek()
기능이었습니다. peek에서 읽은 거의 모든 것이 스트림을 디버깅하는 데 사용해야한다고 말합니다.
각 계정에 사용자 이름, 비밀번호 필드 및 login () 및 logsIn () 메소드가있는 스트림이있는 경우 어떻게해야합니까?
나도
Consumer<Account> login = account -> account.login();
과
Predicate<Account> loggedIn = account -> account.loggedIn();
왜 이것이 그렇게 나쁠까요?
List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount =
accounts.stream()
.peek(login)
.filter(loggedIn)
.collect(Collectors.toList());
이제 내가 알 수있는 한 정확히 의도 된 작업을 수행합니다. 그것;
- 계정 목록을 가져옵니다
- 각 계정에 로그인을 시도합니다
- 로그인하지 않은 계정을 걸러냅니다
- 로그인 한 계정을 새로운 목록으로 수집
이런 식의 단점은 무엇입니까? 진행하지 말아야 할 이유가 있습니까? 마지막 으로이 솔루션이 아니라면 무엇입니까?
이것의 원래 버전은 다음과 같이 .filter () 메소드를 사용했습니다.
.filter(account -> {
account.login();
return account.loggedIn();
})
이것에서 중요한 것은 :
API가 즉각적인 목표를 달성하더라도 의도하지 않은 방식으로 API를 사용하지 마십시오. 이러한 접근 방식은 미래에 중단 될 수 있으며 미래의 관리자에게도 분명하지 않습니다.
별개의 작업이므로 여러 작업으로 구분할 때 해가 없습니다. 가 이다 이 특정 행동이 자바의 향후 버전에서 수정 될 경우 파급 효과를 가질 수있는 명확하고 의도하지 않은 방법으로 API 사용에 해를 끼치.
forEach
이 작업을 사용하면의 각 요소에 의도 된 부작용 accounts
이 있으며이를 조작 할 수있는 작업을 수행하고 있음을 관리자에게 명확하게 알 수 있습니다.
또한 peek
터미널 작업이 실행될 때까지 전체 컬렉션에서 작동하지 않지만 forEach
실제로 터미널 작업 인 중간 작업 이라는 점에서 더 일반적입니다 . 이런 식으로, 당신은 이 문맥에서 peek
와 똑같이 행동 할 것인지에 대한 질문을하는 대신 행동과 코드의 흐름에 대해 강력한 논쟁을 할 수 있습니다 forEach
.
accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
.filter(Account::loggedIn)
.collect(Collectors.toList());
이해해야 할 중요한 점은 스트림이 터미널 작업 에 의해 구동된다는 것 입니다. 터미널 작업은 모든 요소를 처리해야하는지 아니면 전혀 처리해야하는지 결정합니다. 그래서 collect
반면, 각 항목을 처리하는 동작이다 findAny
그것이 매칭 소자가 발생하면 처리 항목을 중지 할 수는.
그리고 count()
항목을 처리하지 않고 스트림의 크기를 결정할 수 있으면 요소를 전혀 처리하지 않을 수 있습니다. 이것은 Java 8에서는 최적화되지 않았지만 Java 9에서는 최적화이기 때문에 Java 9로 전환하고 count()
모든 항목 처리에 의존하는 코드가있을 경우 놀라 울 수 있습니다 . 이것은 다른 구현에 의존하는 세부 사항, 예를 들어 Java 9에서도 연결되어 있으며, 참조 구현은 limit
이러한 예측을 막는 기본 제한이없는 동안 무한 스트림 소스의 크기를 결합하여 예측할 수 없습니다.
Since peek
allows “performing the provided action on each element as elements are consumed from the resulting stream”, it does not mandate processing of elements but will perform the action dependent on what the terminal operation needs. This implies that you have to use it with great care if you need a particular processing, e.g. want to apply an action on all elements. It works if the terminal operation is guaranteed to process all items, but even then, you must be sure that not the next developer changes the terminal operation (or you forget that subtle aspect).
Further, while streams guarantee maintaining the encounter order for certain combination of operations even for parallel streams, these guarantees do not apply to peek
. When collecting into a list, the resulting list will have the right order for ordered parallel streams, but the peek
action may get invoked in an arbitrary order and concurrently.
So the most useful thing you can do with peek
is to find out whether a stream element has been processed which is exactly what the API documentation says:
This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline
Perhaps a rule of thumb should be that if you do use peek outside the "debug" scenario, you should only do so if you're sure of what the terminating and intermediate filtering conditions are. For example:
return list.stream().map(foo->foo.getBar())
.peek(bar->bar.publish("HELLO"))
.collect(Collectors.toList());
seems to be a valid case where you want, in one operation to transform all Foos to Bars and tell them all hello.
Seems more efficient and elegant than something like:
List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;
and you don't end up iterating a collection twice.
I would say that peek
provides the ability to decentralize code that can mutate stream objects, or modify global state (based on them), instead of stuffing everything into a simple or composed function passed to a terminal method.
Now the question might be: should we mutate stream objects or change global state from within functions in functional style java programming?
If the answer to any of the the above 2 questions is yes (or: in some cases yes) then peek()
is definitely not only for debugging purposes, for the same reason that forEach()
isn't only for debugging purposes.
For me when choosing between forEach()
and peek()
, is choosing the following: Do I want pieces of code that mutate stream objects to be attached to a composable, or do I want them to attach directly to stream?
I think peek()
will better pair with java9 methods. e.g. takeWhile()
may need to decide when to stop iteration based on an already mutated object, so paring it with forEach()
would not have the same effect.
P.S. I have not referenced map()
anywhere because in case we want to mutate objects (or global state), rather than generating new objects, it works exactly like peek()
.
Although I agree with most answers above, I have one case in which using peek actually seems like the cleanest way to go.
Similar to your use case, suppose you want to filter only on active accounts and then perform a login on these accounts.
accounts.stream()
.filter(Account::isActive)
.peek(login)
.collect(Collectors.toList());
Peek is helpful to avoid the redundant call while not having to iterate the collection twice:
accounts.stream()
.filter(Account::isActive)
.map(account -> {
account.login();
return account;
})
.collect(Collectors.toList());
The functional solution is to make account object immutable. So account.login() must return a new account object. This will mean that the map operation can be used for login instead of peek.
참고URL : https://stackoverflow.com/questions/33635717/in-java-streams-is-peek-really-only-for-debugging
'program story' 카테고리의 다른 글
C #의 동적 익명 형식에 속성이 있는지 어떻게 확인합니까? (0) | 2020.08.04 |
---|---|
Chrome net :: ERR_INCOMPLETE_CHUNKED_ENCODING 오류 (0) | 2020.08.04 |
XML, HTML 및 XHTML 문서에 유효한 컨텐츠 유형 (0) | 2020.08.04 |
Django : 양식을 사용하여 하나의 템플릿에 여러 모델 (0) | 2020.08.04 |
컴파일 코드와 실행 코드의 차이점은 무엇입니까? (0) | 2020.08.04 |