스트림은 언제 사용해야합니까?


99

a List와 그 stream()방법을 사용할 때 방금 질문을 받았습니다. 내가 알고 있지만 방법 을 사용하여, 나는 확신에 대한 아니에요 사용할 수 있습니다.

예를 들어, 다른 위치에 대한 다양한 경로가 포함 된 목록이 있습니다. 이제 주어진 단일 경로에 목록에 지정된 경로가 포함되어 있는지 확인하고 싶습니다. boolean조건이 충족되었는지 여부에 따라 를 반환하고 싶습니다 .

물론 이것은 어려운 작업이 아닙니다. 하지만 스트림을 사용해야하는지, 아니면 for (-each) 루프를 사용해야하는지 궁금합니다.

목록

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

예-스트림

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

예-For-Each 루프

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

참고 것을 path매개 변수는 항상 소문자 .

내 첫 번째 추측은 for-each 접근 방식이 더 빠르다는 것입니다. 조건이 충족되면 루프가 즉시 반환되기 때문입니다. 필터링을 완료하기 위해 스트림은 모든 목록 항목을 계속 반복합니다.

내 가정이 맞습니까? 그렇다면 (또는 오히려 언제 ) 사용 stream()합니까?


11
스트림은 기존 for 루프보다 표현력이 뛰어나고 읽기 쉽습니다. 나중에 if-then 및 조건 등의 내장 함수에 대해주의해야합니다. 스트림 표현식은 매우 명확합니다. 파일 이름을 소문자로 변환 한 다음 무언가로 필터링 한 다음 계산, 수집 등 결과 : 매우 반복적입니다. 계산 흐름의 표현.
장 - 밥 티스트 Yunès

12
new String[]{…}여기는 필요 없습니다 . 그냥 사용Arrays.asList("my/path/one", "my/path/two")
Holger

4
소스가이면을 ( String[]를) 호출 할 필요가 없습니다 Arrays.asList. 을 사용하여 배열을 통해 스트리밍 할 수 있습니다 Arrays.stream(array). 덧붙여서 isExcluded시험 의 목적을 모두 이해하는 데 어려움이 있습니다. 의 요소 EXCLUDE_PATHS가 문자 그대로 경로 내 어딘가에 포함되어 있는지 정말 흥미로운가요 ? Ie isExcluded("my/path/one/foo/bar/baz")true뿐만 아니라 isExcluded("foo/bar/baz/my/path/one/")...
Holger

3
Arrays.stream좋습니다. 방법을 몰랐 습니다. 지적 해 주셔서 감사합니다. 사실, 제가 게시 한 예제는 저 외에 다른 사람에게는 쓸모가 없어 보입니다. 나는 그 isExcluded방법 의 동작을 알고 있지만, 그것은 정말로 나 자신에게 필요한 것이므로 귀하의 질문에 대답하기 위해 : , 범위에 맞지 않기 때문에 언급하고 싶지 않은 이유로 흥미 롭습니다. 원래 질문의.
mcuenez

1
toLowerCase이미 소문자 인 상수에이 적용되는 이유는 무엇 입니까? path논쟁에 적용되어야하지 않습니까?
Sebastian Redl

답변:


78

당신의 가정이 맞습니다. 스트림 구현이 for 루프보다 느립니다.

이 스트림 사용은 for 루프만큼 빠릅니다.

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

이것은 항목을 반복 String::toLowerCase하고 항목에 하나씩 적용 하고 필터를 적용 하고 첫 번째 항목에서 종료합니다. 일치 .

collect()& 둘 다 anyMatch()터미널 작업입니다. anyMatch()그러나 collect()모든 항목을 처리해야하는 동안 처음 발견 된 항목에서 종료 됩니다.


2
최고 약 몰랐 findFirst()와 함께 filter(). 분명히, 나는 할 수 없습니다 내가 생각뿐만 아니라 같은 스트림을 사용하는 방법을 알고있다.
mcuenez

4
스트림 API 성능에 관한 정말 흥미로운 블로그 기사와 프레젠테이션이 웹에 있는데,이 내용이 내부적으로 어떻게 작동하는지 이해하는 데 매우 도움이된다는 것을 알았습니다. 관심이 있으시다면 조금만 조사해 보는 것이 좋습니다.
Stefan Pries

편집 후 다른 답변의 의견에서 내 질문에 답변 했으므로 귀하의 답변이 수락되어야한다고 생각합니다. 그래도 @ rvit34에 코드 게시에 대한 크레딧을주고 싶습니다. :-)
mcuenez

34

Streams 사용 여부는 성능 고려가 아니라 가독성에 따라 결정해야합니다. 실제로 성능과 관련하여 다른 고려 사항이 있습니다.

당신으로 .filter(path::contains).collect(Collectors.toList()).size() > 0접근, 당신은 모든 요소를 처리하고 임시로 수집List 하고 크기를 비교하기 전에 하지만 두 요소로 구성된 스트림에는 거의 문제가되지 않습니다.

.map(String::toLowerCase).anyMatch(path::contains)요소 수가 상당히 많은 경우을 사용 하면 CPU주기와 메모리를 절약 할 수 있습니다. 그래도 String일치 항목이 발견 될 때까지 각각 을 소문자 표현으로 변환합니다 . 분명히, 사용에 요점이 있습니다

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

대신. 따라서를 호출 할 때마다 소문자로의 변환을 반복 할 필요가 없습니다 isExcluded. EXCLUDE_PATHS문자열 의 요소 수 또는 길이가 정말 커지면 다음을 사용하는 것이 좋습니다.

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

LITERAL플래그를 사용 하여 문자열을 정규식 패턴으로 컴파일하면 일반 문자열 작업처럼 동작하지만 엔진이 예를 들어 Boyer Moore 알고리즘을 사용하여 준비하는 데 약간의 시간을 소비하여 실제 비교와 관련하여 더 효율적입니다.

물론 이것은 준비에 소요되는 시간을 보상 할 수있는 충분한 후속 테스트가있는 경우에만 효과가 있습니다. 이 작업이 성능에 매우 중요한지 여부를 묻는 첫 번째 질문 외에 실제 성능 고려 사항 중 하나입니다. Streams를 사용할지 아니면for 루프 .

그건 그렇고, 위의 코드 예제는 원래 코드의 논리를 유지하므로 나에게는 의심스러워 보입니다. 귀하의 isExcluded방법을 반환 true, 그것은 반환하도록 지정된 경로가,리스트 내의 요소 중 하나를 포함하는 경우 true에 대한 /some/prefix/to/my/path/one뿐만 아니라,로 my/path/one/and/some/suffix또는 /some/prefix/to/my/path/one/and/some/suffix.

심지어 dummy/path/onerouscontains문자열 로 기준을 충족하는 것으로 간주됩니다 my/path/one.


가능한 성능 최적화에 대한 좋은 통찰력, 감사합니다. 귀하의 답변의 마지막 부분과 관련하여 귀하의 의견에 대한 내 답변이 만족스럽지 않은 경우 내 예제 코드를 실제 코드가 아닌 다른 사람들이 내가 요청하는 것을 이해하는 데 도움이되는 단순한 도우미로 간주하십시오. 또한 더 나은 예를 염두에두고 있다면 언제든지 질문을 편집 할 수 있습니다.
mcuenez

3
이 작업은 정말 원하는 것이므로 변경할 필요가 없습니다. 나는 미래의 독자들을 위해 마지막 섹션을 유지할 것입니다. 그래서 그들은 이것이 일반적인 작업이 아니라는 것을 알고 있습니다. 또한 이미 논의되었으며 추가 의견이 필요하지 않습니다.
Holger

실제로 스트림은 작업 메모리의 양이 서버 제한에 위반되는 경우 메모리 최적화를 위해 사용하는 완벽한
ColacX

21

네. 당신이 옳습니다. 스트림 접근 방식에는 약간의 오버 헤드가 있습니다. 그러나 다음과 같은 구성을 사용할 수 있습니다.

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

스트림을 사용하는 주된 이유는 코드를 더 간단하고 읽기 쉽게 만들기 때문입니다.


3
anyMatch대한 바로 가기 filter(...).findFirst().isPresent()입니까?
mcuenez

6
네, 그렇습니다! 그것은 내 첫 제안보다 훨씬 낫습니다.
Stefan Pries

8

Java 스트림의 목표는 병렬 코드 작성의 복잡성을 단순화하는 것입니다. 함수형 프로그래밍에서 영감을 얻었습니다. 직렬 스트림은 코드를 더 깔끔하게 만드는 것입니다.

성능을 원한다면, 설계된 parallelStream을 사용해야합니다. 일반적으로 시리얼은 더 느립니다.

좋은에 대해 읽을 수있는 글이 , 실적 ForLoopStreamParallelStream .

코드에서 종료 방법을 사용하여 첫 번째 일치에서 검색을 중지 할 수 있습니다. (anyMatch ...)


5
소규모 스트림 및 일부 다른 경우에는 시작 비용으로 인해 병렬 스트림이 느려질 수 있습니다. 그리고 순서가 지정되지 않은 병렬 처리가 아닌 순서가 지정된 터미널 작업이있는 경우 마지막에 재 동기화합니다.
CAD97

0

다른 사람들이 많은 좋은 점을 언급했듯이 스트림 평가에서 지연 평가 를 언급하고 싶습니다 . map()소문자 경로의 스트림을 생성 할 때 전체 스트림을 즉시 생성하지 않고 대신 스트림이 느리게 구성 되므로 성능이 기존 for 루프와 동일해야합니다. 그것은 전체 검사를 수행하지 않는, map()그리고 anyMatch()같은 시간에 실행됩니다. anyMatch()true를 반환 하면 단락됩니다.

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