자바 스트림에서 실제로는 디버깅을 위해서만 엿볼 수 있습니까?


137

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

38
여러 줄 람다가 필요할 때마다 줄을 개인 메서드로 옮기고 람다 대신 메서드 참조를 전달합니다.
VGR

1
의도가 무엇 - 당신은 모두 계정 로그인하려고 하고 그들이가 로그인하는 경우에 따라 필터를 (사소하게 사실 수있는)? 또는 로그인 한 다음 로그인했는지 여부에 따라 필터링 하시겠습니까? 이 forEach작업은와 반대로 원하는 작업이 될 수 있으므로이 순서대로 요청 합니다 peek. API에 있다고해서 (예 :와 같은 Optional.of) 악용으로 공개되지 않았다는 의미는 아닙니다 .
Makoto

8
또한 코드는 단지 .peek(Account::login)and 일 수 있습니다 .filter(Account::loggedIn). 이와 같은 다른 메소드를 호출하는 Consumer and Predicate를 작성할 이유가 없습니다.
Joshua Taylor

2
또한 스트림 API 는 행동 매개 변수의 부작용명시 적으로 권장하지 않습니다 .
Didier L

6
유용한 소비자는 항상 부작용이 있으므로 당연히 권장하지 않습니다. 이것은 실제로 동일한 부분에서 설명한 " 스트림 연산 같은 소수 forEach()peek()만 부작용을 통해 동작 할 수있다; 이들은주의해서 사용해야합니다. ”. 내 말은 것을 상기시켜 더이었다 peek(디버깅 목적을 위해 설계) 작업이 같은 다른 작업 내에서 같은 일을하고로 대체되어서는 안 map()filter().
Didier L

답변:


77

이것에서 중요한 것은 :

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

3
전처리 단계에서 로그인을 수행하는 경우 스트림이 전혀 필요하지 않습니다. forEach소스 콜렉션에서 바로 수행 할 수 있습니다 .accounts.forEach(a -> a.login());
Holger

1
@Holger : 훌륭한 지적입니다. 나는 그것을 대답에 포함시켰다.
Makoto

2
@ Adam.J : 내 대답은 제목에 포함 된 일반적인 질문에 더 집중했습니다. 즉,이 방법은 해당 방법의 측면을 설명함으로써 실제로 디버깅 만합니다. 이 답변은 실제 사용 사례와 그 방법에 더 융합되어 있습니다. 그래서 그들은 함께 전체 그림을 제공한다고 말할 수 있습니다. 첫째, 이것이 의도 된 용도가 아닌 이유, 두 번째 결론은 의도하지 않은 용도에 충실하지 않고 대신해야 할 일입니다. 후자는 더 실용적입니다.
Holger

2
물론 login()메소드 boolean가 성공 상태를 나타내는 값을 반환 하면 훨씬 쉬워졌습니다 .
Holger

3
그것이 내가 목표로 한 것입니다. 를 login()반환 boolean하면 가장 깨끗한 솔루션 인 술어로 사용할 수 있습니다. 여전히 부작용이 있지만 간섭하지 않는 한 괜찮습니다. 즉, login하나의 Account프로세스는 다른 프로세스의 로그인 프로세스에 영향을 미치지 않습니다 Account.
Holger

111

알아야 할 중요한 점은 스트림이 터미널 작업 에 의해 구동된다는 것 입니다. 터미널 작업은 모든 요소를 ​​처리해야하는지 아니면 전혀 처리해야하는지 결정합니다. 그래서 collect반면, 각 항목을 처리하는 동작이다 findAny그것이 매칭 소자가 발생하면 처리 항목을 중지 할 수는.

그리고 count()항목을 처리하지 않고 스트림의 크기를 결정할 수 있으면 요소를 전혀 처리하지 않을 수 있습니다. 이것은 Java 8에서는 최적화되지 않았지만 Java 9에서는 최적화이기 때문에 Java 9로 전환하고 count()모든 항목 처리에 의존하는 코드가있을 경우 놀라 울 수 있습니다 . 이것은 다른 구현-의존적 세부 사항, 예를 들어 Java 9에서도 연결되어 있으며, 참조 구현은 limit이러한 예측을 막는 기본 제한이없는 동안 무한 스트림 소스의 크기를 결합 할 수는 없습니다.

peek" 요소가 결과 스트림에서 소비 될 때 각 요소에 대해 제공된 조치를 수행"할 수 있으므로 요소 처리를 요구하지는 않지만 터미널 조작에 필요한 조치에 따라 조치를 수행합니다. 즉, 특정 처리가 필요한 경우 (예 : 모든 요소에 작업을 적용하려는 경우)주의해서 사용해야합니다. 터미널 작업이 모든 항목을 처리하도록 보장되면 작동하지만 다음 개발자가 터미널 작업을 변경하지 않아야합니다 (또는 그 미묘한 측면을 잊어 버려야 함).

또한 스트림은 병렬 스트림의 경우에도 특정 조합의 연산에 대해 발생 순서를 유지하도록 보장하지만 이러한 보장은 적용되지 않습니다 peek. 목록으로 수집 할 때 결과 목록은 정렬 된 병렬 스트림에 대해 올바른 순서를 갖지만 peek작업은 임의의 순서로 동시에 호출 될 수 있습니다.

따라서 당신이 할 수있는 가장 유용한 것은 peekAPI 문서가 말하는 스트림 요소가 처리되었는지 여부를 찾는 것입니다.

이 방법은 주로 디버깅을 지원하기 위해 존재하며, 파이프 라인의 특정 지점을지나 가면서 요소를 확인하려는 경우


OP의 유스 케이스에 미래 또는 현재 문제가 있습니까? 그의 코드는 항상 원하는 것을 수행합니까?
ZhongYu

9
@ bayou.io : 내가 볼 수있는 한, 이 정확한 형태 에는 문제가 없습니다 . 그러나 설명하려고했지만이 방법을 사용하면 1-2 년 후 코드로 되돌아와«기능 요청 9876»을 코드에 통합하더라도이 측면을 기억해야합니다.
Holger

1
"피킹 작업은 임의의 순서로 동시에 호출 될 수 있습니다." 이 말은 "요소가 소비 될 때"와 같이 엿보기의 작동 방식에 대한 규칙에 위배되지 않습니까?
호세 마르티네즈

5
@Jose Martinez : " 결과 스트림에서 요소가 소비 됨"으로 표시됩니다. 이는 최종 조치가 아니라 처리이지만 최종 조치가 일관성이 없으면 최종 조치가 요소를 순서대로 사용할 수 있습니다. 그러나 API 노트의 문구 인“ 파이프 라인의 특정 지점을지나 가면서 요소를 볼 수 있습니다 ”는 그것을 설명하는 데 더 효과적이라고 생각합니다.
Holger

23

아마도 "디버그"시나리오 외부에서 엿보기를 사용하는 경우 종료 및 중간 필터링 조건이 무엇인지 확실하게 확인해야합니다. 예를 들면 다음과 같습니다.

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

한 번의 작업으로 모든 Foos를 Bars로 변환하고 모두에게 인사하는 올바른 경우 인 것 같습니다.

다음과 같은 것보다 더 효율적이고 우아해 보입니다.

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

컬렉션을 두 번 반복하지 않습니다.


4

나는 그 말을 peek할 수있는 기능 제공 스트림 객체를 돌연변이, 또는 글로벌 상태를 수정할 수 있습니다 분권화 코드 대신에 모든 먹거리의, (그들에 기반을) 단순 또는 구성 기능 터미널 메서드에 전달합니다.

이제 문제는 다음과 같습니다. 함수형 자바 프로그래밍의 함수 내에서 스트림 객체를 변경하거나 전역 상태를 변경해야 합니까?

하여 2 위의 질문들에 대한 답이있는 경우 예 (또는 : 어떤 경우 예에서) 다음 peek()이다 확실히뿐만 아니라 디버깅 목적으로 , 같은 이유로 forEach()디버깅 목적뿐만 아니라 .

나를 위해 사이에 선택할 때 forEach()peek()다음을 선택한다 :해야합니까 돌연변이 스트림이 작성 가능에 부착 할 객체를 그 코드의 조각을 원하거나 내가 그들을 스트림에 직접 연결 하시겠습니까?

peek()java9 메소드와 더 잘 어울릴 것이라고 생각 합니다. 예를 들어 takeWhile()이미 변형 된 객체를 기반으로 반복을 중지 할 시점을 결정해야하므로이를 파싱해도 forEach()같은 효과가 없습니다.

추신 : 나는 map()새로운 객체를 생성하는 대신 객체 (또는 전역 상태)를 변경하려는 경우 정확히 똑같이 작동하기 때문에 어디에서나 참조하지 않았습니다 peek().


3

위의 대부분의 답변에 동의하지만, peek을 사용하는 것이 실제로 가장 깨끗한 방법처럼 보이는 경우가 있습니다.

사용 사례와 유사하게 활성 계정에서만 필터링 한 다음 해당 계정에서 로그인을 수행한다고 가정합니다.

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peek는 컬렉션을 두 번 반복 할 필요없이 중복 호출을 피하는 데 도움이됩니다.

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

3
로그인 방법을 올바르게 설정하기 만하면됩니다. 나는 어떻게 엿봄이 가장 깨끗한 방법인지 알지 못한다. 코드를 읽는 사람이 실제로 API를 잘못 사용한다는 것을 어떻게 알 수 있습니까? 훌륭하고 깨끗한 코드는 독자가 코드에 대한 가정을 강요하지 않습니다.
kaba713

1

기능적 솔루션은 계정 개체를 변경할 수 없게 만드는 것입니다. 따라서 account.login ()은 새 계정 객체를 반환해야합니다. 이는 맵 조작을 엿보기 대신 로그인에 사용할 수 있음을 의미합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.