[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이러한 예측을 막는 기본 제한이없는 동안 무한 스트림 소스의 크기를 결합 할 수는 없습니다.

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

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

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

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


답변

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

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;

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


답변

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

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

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

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

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

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


답변

위의 대부분의 답변에 동의하지만, 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());


답변

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


답변