Java 8-목록을 변환하는 가장 좋은 방법 : map 또는 foreach?


188

나는 목록이 myListToParse내가 요소를 필터링하고 각 요소에 대해 방법을 적용하고, 다른 목록에 결과를 추가 할 myFinalList.

Java 8에서는 두 가지 방법으로 할 수 있음을 알았습니다. 나는 그들 사이의보다 효율적인 방법을 알고 싶습니다. 왜 한 가지 방법이 다른 방법보다 낫습니다.

세 번째 방법에 대한 제안이 열려 있습니다.

방법 1 :

myFinalList = new ArrayList<>();
myListToParse.stream()
        .filter(elt -> elt != null)
        .forEach(elt -> myFinalList.add(doSomething(elt)));

방법 2 :

myFinalList = myListToParse.stream()
        .filter(elt -> elt != null)
        .map(elt -> doSomething(elt))
        .collect(Collectors.toList()); 

55
두 번째입니다. 적절한 기능에는 부작용이 없어야합니다. 첫 번째 구현에서 외부 세계를 수정합니다.
ThanksForAllTheFish

37
단지 스타일의 문제이지만 elt -> elt != null대체 될 수 있습니다Objects::nonNull
the8472

2
@ the8472 컬렉션에 null 값이 없는지 확인하고 대신와 Optional<T>함께 사용하는 것이 좋습니다 flatMap.
herman

2
@SzymonRoziewski, 별로는 아닙니다. 이처럼 사소한 일이라면, 후드 아래에서 병렬 스트림을 설정하는 데 필요한 작업은이 구성 음소거를 사용합니다.
MK

2
비 정적 방법 .map(this::doSomething)이라고 가정하여 작성할 수 있습니다 doSomething. 정적 인 경우 this클래스 이름으로 바꿀 수 있습니다 .
herman

답변:


153

성능 차이에 대해 걱정하지 마십시오.이 경우 정상적으로 최소화됩니다.

방법 2가 바람직하기 때문에

  1. 람다 식 외부에 존재하는 컬렉션을 변경하지 않아도됩니다.

  2. 수집 파이프 라인에서 수행되는 여러 단계가 순차적으로 작성되기 때문에 더 읽기 쉽습니다. 먼저 필터 작업, 맵 작업, 결과 수집 (수집 파이프 라인의 이점에 대한 자세한 내용은 Martin Fowler의 우수한 기사 참조 )

  3. Collector사용 된 값을 바꾸면 값 수집 방법을 쉽게 변경할 수 있습니다 . 경우에 따라서는 직접 작성해야 할 수도 Collector있지만, 쉽게 재사용 할 수 있다는 이점이 있습니다.


43

나는 두 번째 형식이 부작용이없고 병렬화하기가 더 쉽기 때문에 (병렬 스트림을 사용하면) 기존 답변에 동의합니다.

성능 측면에서는 병렬 스트림을 사용하기 시작할 때까지 동일한 것으로 보입니다. 이 경우 지도의 성능이 훨씬 향상됩니다. 마이크로 벤치 마크 결과 는 아래를 참조하십시오 .

Benchmark                         Mode  Samples    Score   Error  Units
SO28319064.forEach                avgt      100  187.310 ± 1.768  ms/op
SO28319064.map                    avgt      100  189.180 ± 1.692  ms/op
SO28319064.mapWithParallelStream  avgt      100   55,577 ± 0,782  ms/op

forEach 는 터미널 메서드이므로 void를 반환하므로 상태 저장 람다를 사용해야 하기 때문에 첫 번째 예제를 같은 방식으로 향상시킬 수 없습니다 . 그러나 병렬 스트림을 사용하는 경우에는 실제로 나쁜 생각 입니다.

마지막으로 두 번째 스 니펫은 메소드 참조 및 정적 가져 오기를 사용하여 훨씬 간결하게 작성 될 수 있습니다.

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 

1
성능에 관해서는, 병렬 스트림을 사용하는 경우에는 "map"이 실제로 "forEach"보다 승리합니다. 밀리 초 단위 벤치 마크 : SO28319064.for 각 : 187,310 ± 1,768ms / op-SO28319064.map : 189,180 ± 1,692ms / op --SO28319064.mapParallelStream : 55,577 ± 0,782ms / op
Giuseppe Bertone

2
@GiuseppeBertone, 그것은 assylias에 달려 있지만, 내 의견으로는 편집이 원래 저자의 의도와 모순됩니다. 자신의 답변을 추가하려면 기존 답변을 너무 많이 편집하는 대신 추가하는 것이 좋습니다. 또한 마이크로 벤치 마크에 대한 링크는 결과와 관련이 없습니다.
Tagir Valeev 님이

5

스트림 사용의 주요 이점 중 하나는 선언적인 방식으로, 즉 기능적인 프로그래밍 스타일을 사용하여 데이터를 처리 할 수 ​​있다는 것입니다. 또한 멀티 스레딩 기능을 무료로 제공하므로 스트림을 동시에 만들기 위해 추가 멀티 스레딩 코드를 작성할 필요가 없습니다.

이 스타일의 프로그래밍을 탐색하는 이유를 가정하면 이러한 이점을 활용하기 위해 첫 번째 코드 샘플이 작동하지 않을 수 있습니다.이 foreach방법은 터미널로 분류되므로 부작용이 발생할 수 있습니다.

두 번째 방법은 맵 함수가 상태 비 저장 람다 함수를 수용 할 수 있기 때문에 함수형 프로그래밍 관점에서 선호됩니다. 보다 명확하게, map 함수에 전달 된 람다는

  1. 비 간섭 : 함수가 비 동시 (예 :) 스트림의 소스를 변경해서는 안됨을 의미합니다 ArrayList.
  2. 병렬 처리를 수행 할 때 예기치 않은 결과를 피하기 위해 상태 비 저장 (스레드 스케줄링 차이로 인해 발생)

두 번째 접근 방식의 또 다른 이점은 스트림이 병렬이고 수집기가 동시적이고 정렬되지 않은 경우 이러한 특성은 수집을 동시에 수행하기 위해 축소 작업에 유용한 힌트를 제공 할 수 있다는 것입니다.


4

당신이 사용하는 경우 이클립스 컬렉션을 당신이 사용할 수있는 collectIf()방법을.

MutableList<Integer> source =
    Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5);

MutableList<String> result = source.collectIf(Objects::nonNull, String::valueOf);

Assert.assertEquals(Lists.immutable.with("1", "2", "3", "4", "5"), result);

그것은 열심히 평가하고 스트림을 사용하는 것보다 약간 빠릅니다.

참고 : 저는 Eclipse Collections의 커미터입니다.


1

나는 두 번째 방법을 선호합니다.

첫 번째 방법을 사용할 때 성능을 향상시키기 위해 병렬 스트림을 사용하기로 결정하면에 의해 요소가 출력 목록에 추가되는 순서를 제어 할 수 없습니다 forEach.

당신이 사용하는 경우 toList, 스트림 API는 병렬 스트림을 사용하는 경우에도 순서를 유지합니다.


이것이 올바른 조언인지 확실하지 않습니다 . 병렬 스트림을 사용하고 싶지만 순서를 유지하려는 경우 forEachOrdered대신 사용할 수 있습니다 forEach. 그러나 forEach국가에 대한 문서로서 만남 순서를 유지하면 병렬 처리의 이점이 희생됩니다. 그때도 그런 것 같아요 toList.
herman

0

스트림에 toList 메소드가없는 이유에 대한 세 번째 옵션 -usingstream().toArray() -see 주석 이 있습니다 . forEach () 또는 collect ()보다 느리고 표현력이 떨어집니다. 나중에 JDK 빌드에서 최적화 될 수 있으므로 만일을 위해 여기에 추가하십시오.

가정 List<String>

    myFinalList = Arrays.asList(
            myListToParse.stream()
                    .filter(Objects::nonNull)
                    .map(this::doSomething)
                    .toArray(String[]::new)
    );

마이크로 마이크로 벤치 마크, 1M 엔트리, 20 % 널 및 doSomething ()의 간단한 변환

private LongSummaryStatistics benchmark(final String testName, final Runnable methodToTest, int samples) {
    long[] timing = new long[samples];
    for (int i = 0; i < samples; i++) {
        long start = System.currentTimeMillis();
        methodToTest.run();
        timing[i] = System.currentTimeMillis() - start;
    }
    final LongSummaryStatistics stats = Arrays.stream(timing).summaryStatistics();
    System.out.println(testName + ": " + stats);
    return stats;
}

결과는

평행:

toArray: LongSummaryStatistics{count=10, sum=3721, min=321, average=372,100000, max=535}
forEach: LongSummaryStatistics{count=10, sum=3502, min=249, average=350,200000, max=389}
collect: LongSummaryStatistics{count=10, sum=3325, min=265, average=332,500000, max=368}

잇달아 일어나는:

toArray: LongSummaryStatistics{count=10, sum=5493, min=517, average=549,300000, max=569}
forEach: LongSummaryStatistics{count=10, sum=5316, min=427, average=531,600000, max=571}
collect: LongSummaryStatistics{count=10, sum=5380, min=444, average=538,000000, max=557}

null과 필터가없는 병렬 (스트림은 SIZED) : toArrays는 이러한 경우에 최고의 성능을 가지며, .forEach()받는 ArrayList에서 "indexOutOfBounds"로 실패하고.forEachOrdered()

toArray: LongSummaryStatistics{count=100, sum=75566, min=707, average=755,660000, max=1107}
forEach: LongSummaryStatistics{count=100, sum=115802, min=992, average=1158,020000, max=1254}
collect: LongSummaryStatistics{count=100, sum=88415, min=732, average=884,150000, max=1014}

0

방법 3 일 수 있습니다.

나는 항상 논리를 분리하는 것을 선호합니다.

Predicate<Long> greaterThan100 = new Predicate<Long>() {
            @Override
            public boolean test(Long currentParameter) {
                return currentParameter > 100;
            }
        };

        List<Long> sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L);
        List<Long> resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList());

0

3rd Pary Libaries를 사용해도 괜찮습니다. cyclops-react 는이 기능이 내장 된 Lazy 확장 컬렉션을 정의합니다. 예를 들어 간단히

ListX myListToParse;

ListX myFinalList = myListToParse.filter (elt-> elt! = null) .map (elt-> doSomething (elt));

myFinalList는 처음 액세스 할 때까지 (및 구체화 된 목록이 캐시되어 재사용 된 후에) 평가되지 않습니다.

[공개 나는 사이클롭스 반응의 주요 개발자입니다]

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