[java] Java 8 Iterable.forEach () 대 foreach 루프

다음 중 Java 8에서 더 나은 방법은 무엇입니까?

자바 8 :

joins.forEach(join -> mIrc.join(mSession, join));

자바 7 :

for (String join : joins) {
    mIrc.join(mSession, join);
}

람다로 “단순화”될 수있는 많은 for 루프가 있지만 실제로 사용하면 어떤 이점이 있습니까? 성능과 가독성이 향상됩니까?

편집하다

또한이 질문을 더 긴 방법으로 확장 할 것입니다. 부모 함수를 람다에서 반환하거나 중단 할 수 없으며 이것을 비교할 때 고려해야 할 사항이 있지만 고려해야 할 다른 것이 있습니까?



답변

더 나은 방법은을 사용하는 것 for-each입니다. 신기한 신기한 원칙 인 Keep It Simple을 위반하는 것 외에도 forEach()최소한 다음과 같은 결함이 있습니다.

  • 최종 변수가 아닌 변수를 사용할 수 없습니다 . 따라서 다음과 같은 코드는 forEach 람다로 변환 할 수 없습니다.

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
  • 확인 된 예외를 처리 할 수 ​​없습니다 . 람다는 실제로 확인 된 예외를 던지는 것을 금지하지 않지만 일반적인 기능 인터페이스 Consumer는 선언하지 않습니다. 따라서 확인 된 예외를 발생시키는 모든 코드는 try-catch또는로 감싸 야합니다 Throwables.propagate(). 그러나 그렇게해도, 예외가 어떻게 발생하는지 항상 명확하지는 않습니다. 그것은 내장의 어딘가에 삼킬 수 있습니다forEach()

  • 제한된 흐름 제어 . return람다의 A continue는 for-each의 a와 같지만 a와 동등한 것은 없습니다 break. 반환 값, 단락 또는 설정 플래그 ( 최종 변수가 아닌 변수 규칙을 위반하지 않으면 약간 완화 할 수 있음)와 같은 작업을 수행하는 것도 어렵습니다 . “이것은 최적화 일뿐만 아니라 파일에서 행을 읽는 것과 같은 일부 시퀀스에 부작용이 있거나 무한 시퀀스가있을 수 있다고 생각할 때 중요합니다.”

  • 병렬로 실행될 수 있습니다. 이는 최적화해야 할 코드의 0.1 %를 제외한 모든 사람에게 끔찍하고 끔찍한 일입니다. 모든 병렬 코드는 잠금, 휘발성 및 전통적인 멀티 스레드 실행의 다른 불쾌한 측면을 사용하지 않더라도 고려해야합니다. 모든 버그를 찾기가 어렵습니다.

  • JIT가 일반 루프와 같은 정도로 forEach () + lambda를 최적화 할 수 없으므로 특히 람다는 새롭기 때문에 성능이 저하 될 수 있습니다. “최적화”란 람다 (작은)를 호출하는 오버 헤드가 아니라 현대 JIT 컴파일러가 코드 실행시 수행하는 정교한 분석 및 변환을 의미합니다.

  • 병렬 처리가 필요한 경우 ExecutorService를 사용하는 것이 훨씬 빠르며 훨씬 어렵지 않습니다 . (: 많은 문제에 대해 모르는 읽기) 스트림은 모두 자동적 인은 전문 (읽기 : 일반적인 경우 비효율적)를 사용하는 병렬 전략을 ( 재귀 분해 포크 – 조인 ).

  • 중첩 된 호출 계층 구조 및 병렬 실행으로 인해 디버깅이 더 혼동 됩니다. 디버거에 주변 코드의 변수를 표시하는 데 문제가있을 수 있으며 단계별와 같은 기능이 예상대로 작동하지 않을 수 있습니다.

  • 스트림은 일반적으로 코딩, 읽기 및 디버그가 더 어렵습니다 . 실제로 이것은 일반적으로 복잡한 ” 유창한 “API에 해당됩니다. 복잡한 단일 명령문의 조합, 제네릭의 과도한 사용 및 중간 변수의 부족으로 인해 혼란스러운 오류 메시지가 생성되고 디버깅이 좌절됩니다. “이 방법에는 X 형에 대한 과부하가 없습니다”대신 “유형을 엉망으로 만든 곳이 어디인지, 어떻게 알 수 있는지”라는 오류 메시지가 나타납니다. 마찬가지로 코드가 여러 명령문으로 분리되고 중간 값이 변수에 저장 될 때처럼 쉽게 디버거에서 작업을 수행하고 조사 할 수 없습니다. 마지막으로, 코드를 읽고 각 실행 단계에서의 유형과 동작을 이해하는 것은 쉽지 않을 수 있습니다.

  • 아픈 엄지 손가락처럼 튀어 나옵니다 . Java 언어에는 이미 for-each 문이 있습니다. 함수 호출로 대체해야하는 이유는 무엇입니까? 표현의 어딘가에 부작용을 숨기도록 권장하는 이유는 무엇입니까? 다루기 힘든 원 라이너를 장려하는 이유는 무엇입니까? 규칙적인 for-each와 new forEach willy-nilly를 혼합하는 것은 나쁜 스타일입니다. 코드는 관용구 (반복으로 인해 빠르게 이해되는 패턴)로 말해야하며, 관용구가 적을수록 코드가 더 명확 해지고 사용되는 관용구를 결정하는 데 소요되는 시간이 줄어 듭니다 (나 같은 완벽 주의자들에게는 큰 시간 낭비)! ).

보시다시피, 나는 의미가있는 경우를 제외하고는 forEach ()의 큰 팬이 아닙니다.

특히 나에게 불쾌한 Stream것은 구현하지 않고 Iterable(실제로 method가 있음에도 불구하고 iterator) forEach ()만으로 for-each에서 사용할 수 없다는 사실 입니다. 를 사용하여 스트림을 Iterables로 캐스팅하는 것이 좋습니다 (Iterable<T>)stream::iterator. 더 나은 대안은 구현을 포함하여 많은 Stream API 문제를 해결하는 StreamEx 를 사용 하는 것 Iterable입니다.

즉, forEach()다음에 유용합니다.

  • 동기화 된 목록을 원자 적으로 반복합니다 . 이전에는 Collections.synchronizedList()get 또는 set과 관련하여 생성 된 목록 이 원자 목록 이지만 반복 할 때 스레드로부터 안전하지 않았습니다.

  • 병렬 실행 (적절한 병렬 스트림 사용) . 이렇게하면 문제가 Streams and Spliterators에 내장 된 성능 가정과 일치하는 경우 ExecutorService를 사용하여 몇 줄의 코드를 절약 할 수 있습니다.

  • 동기화 된 목록과 같이 반복 제어를 통해 이익을 얻는 특정 컨테이너 (사람들이 더 많은 예제를 제시 할 수 없다면 이것은 이론적 인 것이지만)

  • forEach()및 메소드 참조 인수 (예 🙂 를 사용하여 단일 함수를보다 명확하게 호출합니다list.forEach (obj::someMethod) . 그러나 확인 된 예외, 더 어려운 디버깅 및 코드 작성시 사용하는 관용구 수 감소에 대한 요점을 명심하십시오.

내가 참조로 사용한 기사 :

편집 : 람다에 대한 원래 제안 중 일부 (예 : http://www.javac.info/closures-v06a.html )는 내가 언급 한 문제 중 일부를 해결 한 것처럼 보입니다 (물론 자체 합병증을 추가하는 동안).


답변

작업을 병렬로 실행할 수있는 경우 이점이 고려됩니다. ( http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and- 내부 및 외부 반복에 대한 섹션 참조 )

  • 필자의 관점에서 가장 큰 장점은 루프 내에서 수행 할 구현을 병렬 또는 순차적으로 실행할지 여부를 결정할 필요없이 정의 할 수 있다는 것입니다.

  • 루프를 병렬로 실행하려면 간단히 쓸 수 있습니다.

     joins.parallelStream().forEach(join -> mIrc.join(mSession, join));

    스레드 처리 등을위한 추가 코드를 작성해야합니다.

참고 : 내 대답으로는 java.util.Stream인터페이스 구현에 참여한다고 가정했습니다 . 조인 java.util.Iterable이 인터페이스 만 구현하면 더 이상 사실이 아닙니다.


답변

이 질문을 읽을 때 Iterable#forEach람다 식과 함께 전통적인 for-each 루프를 작성하는 지름길 / 대체 라는 인상을 얻을 수 있습니다 . 이것은 단순히 사실이 아닙니다. OP 의이 코드 :

joins.forEach(join -> mIrc.join(mSession, join));

작문의 지름길 이 아닙니다

for (String join : joins) {
    mIrc.join(mSession, join);
}

확실히 이런 식으로 사용해서는 안됩니다. 대신 쓰기를위한 지름길 ( 정확히 같지는 않지만 )

joins.forEach(new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
});

그리고 다음 Java 7 코드를 대체합니다.

final Consumer<T> c = new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
};
for (T t : joins) {
    c.accept(t);
}

위의 예와 같이 루프 본문을 기능적 인터페이스로 바꾸면 코드가보다 명확 해집니다. (1) 루프 본문은 주변 코드 및 제어 흐름에 영향을 미치지 않으며 (2) 루프의 본문은 주변 코드에 영향을주지 않으면 서 다른 기능 구현으로 대체 될 수 있습니다. 외부 스코프의 비 최종 변수에 액세스 할 수없는 것은 함수 / 람다의 결함이 아니며 , 전통적인 for-each 루프의 의미와 의미를 구별 하는 기능 입니다 Iterable#forEach. 의 구문에 Iterable#forEach익숙해지면 코드에 대한 추가 정보를 즉시 얻으므로 코드를 더 읽기 쉽게 만듭니다.

전통적인 for-each 루프는 자바에서 좋은 습관을 유지할 것입니다 (과도한 용어 ” 모범 사례 ” 를 피하기 위해 ). 그러나 이것이 Iterable#forEach나쁜 습관이나 나쁜 스타일로 간주되어야 한다는 것을 의미하지는 않습니다 . 작업을 수행하기 위해 올바른 도구를 사용하는 것은 항상 모범 사례이며, 여기에는 전통적인 for-each 루프와을 혼합하는 것이 포함됩니다 Iterable#forEach.

Iterable#forEach이 스레드에서 이미 단점을 논의 했으므로 다음과 같은 이유가 있습니다 Iterable#forEach.

  • 코드가 더 명확하게하려면 다음과 같이, 위에서 설명한 Iterable#forEach 수 있습니다 어떤 상황에서 당신의 코드가 더 명확하고 쉽게 읽을 수 있도록.

  • 코드를보다 확장 가능하고 유지 보수 가능하게하려면 : 함수를 루프 본문으로 사용하면이 기능을 다른 구현으로 대체 할 수 있습니다 ( 전략 패턴 참조 ). 예를 들어 람다 식을 메서드 호출로 쉽게 대체 할 수 있습니다.이 메서드는 하위 클래스로 덮어 쓸 수 있습니다.

    joins.forEach(getJoinStrategy());

    그런 다음 기능 인터페이스를 구현하는 열거 형을 사용하여 기본 전략을 제공 할 수 있습니다. 이를 통해 코드를 확장 할 수있을뿐만 아니라 루프 구현에서 루프 구현을 분리하므로 유지 관리 성이 향상됩니다.

  • 코드를 더 디버깅 가능하게 만들려면 : 선언에서 루프 구현을 분리하면 디버깅이 더 쉬워 질 수 있습니다. 특수 디버그 구현을 통해 메인 코드를 복잡하게 만들 필요없이 디버그 메시지를 인쇄 할 수 있기 때문입니다 if(DEBUG)System.out.println(). 디버그 구현은 실제 함수 구현 을 장식 하는 delegate 일 수 있습니다 .

  • 최적화 성능이 중요한 코드 : 어설의 일부는 반대로이 스레드, Iterable#forEach 않습니다 이미보다 더 나은 성능을 제공하는 전통에 대한-각 루프, 최소한의 ArrayList를 사용하여 “-client”모드에서 핫스팟을 실행할 때. 이 성능 향상은 작고 대부분의 사용 사례에서 무시할 만하지 만이 추가 성능으로 인해 차이가 발생할 수 있습니다. 예를 들어 라이브러리 유지 관리자는 기존 루프 구현 중 일부를로 대체해야하는 경우 확실히 평가하려고합니다 Iterable#forEach.

    이 진술을 사실로 뒷받침하기 위해 Caliper를 사용 하여 마이크로 벤치 마크를 수행했습니다 . 테스트 코드는 다음과 같습니다 (git의 최신 Caliper가 필요합니다).

    @VmOptions("-server")
    public class Java8IterationBenchmarks {
    
        public static class TestObject {
            public int result;
        }
    
        public @Param({"100", "10000"}) int elementCount;
    
        ArrayList<TestObject> list;
        TestObject[] array;
    
        @BeforeExperiment
        public void setup(){
            list = new ArrayList<>(elementCount);
            for (int i = 0; i < elementCount; i++) {
                list.add(new TestObject());
            }
            array = list.toArray(new TestObject[list.size()]);
        }
    
        @Benchmark
        public void timeTraditionalForEach(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : list) {
                    t.result++;
                }
            }
            return;
        }
    
        @Benchmark
        public void timeForEachAnonymousClass(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(new Consumer<TestObject>() {
                    @Override
                    public void accept(TestObject t) {
                        t.result++;
                    }
                });
            }
            return;
        }
    
        @Benchmark
        public void timeForEachLambda(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(t -> t.result++);
            }
            return;
        }
    
        @Benchmark
        public void timeForEachOverArray(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : array) {
                    t.result++;
                }
            }
        }
    }

    결과는 다음과 같습니다.

    “-client”로 실행 Iterable#forEach하면 ArrayList보다 전통적인 for 루프보다 성능이 뛰어나지 만 어레이를 직접 반복하는 것보다 여전히 느립니다. “-server”로 실행할 때 모든 접근 방식의 성능은 거의 같습니다.

  • 병렬 실행에 대한 선택적 지원을 제공하려면 다음과 같이 이미 스트림을Iterable#forEach 사용하여 기능 인터페이스를 병렬 로 실행할 수있는 가능성이 중요한 측면이라고합니다. 루프가 실제로 병렬로 실행되는 것을 보장하지는 않기 때문에 이를 선택적 기능으로 고려해야 합니다. 를 사용하여 목록을 반복하면 다음과 같이 명시 적으로 말할 수 있습니다.이 루프 병렬 실행을 지원 하지만 그에 의존하지는 않습니다. 다시 말하지만 이것은 기능이며 결함이 아닙니다!Collection#parallelStream()list.parallelStream().forEach(...);

    병렬 실행에 대한 의사 결정을 실제 루프 구현에서 멀어지게하면 코드 자체에 영향을주지 않고 코드를 선택적으로 최적화 할 수 있습니다. 또한 기본 병렬 스트림 구현이 요구 사항에 맞지 않으면 아무도 자신의 구현을 제공하지 못합니다. 예를 들어 기본 운영 체제, 컬렉션 크기, 코어 수 및 일부 기본 설정에 따라 최적화 된 컬렉션을 제공 할 수 있습니다.

    public abstract class MyOptimizedCollection<E> implements Collection<E>{
        private enum OperatingSystem{
            LINUX, WINDOWS, ANDROID
        }
        private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
        private int numberOfCores = Runtime.getRuntime().availableProcessors();
        private Collection<E> delegate;
    
        @Override
        public Stream<E> parallelStream() {
            if (!System.getProperty("parallelSupport").equals("true")) {
                return this.delegate.stream();
            }
            switch (operatingSystem) {
                case WINDOWS:
                    if (numberOfCores > 3 && delegate.size() > 10000) {
                        return this.delegate.parallelStream();
                    }else{
                        return this.delegate.stream();
                    }
                case LINUX:
                    return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator());
                case ANDROID:
                default:
                    return this.delegate.stream();
            }
        }
    }

    여기서 좋은 점은 루프 구현이 이러한 세부 사항을 알거나 신경 쓸 필요가 없다는 것입니다.


답변

forEach()iterable은 표준 반복자 방식과 달리 요소를 반복하는 가장 좋은 방법을 알고 있기 때문에 for-each 루프보다 빠르게 구현할 수 있습니다. 차이점은 내부 루프 또는 외부 루프입니다.

예를 들어 다음 ArrayList.forEach(action)과 같이 간단하게 구현할 수 있습니다.

for(int i=0; i<size; i++)
    action.accept(elements[i])

많은 비계가 필요한 for-each 루프와 달리

Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    do something with `next`

그러나을 사용하여 두 가지 오버 헤드 비용을 고려해야합니다 forEach(). 하나는 람다 객체를 만들고 다른 하나는 람다 메서드를 호출하는 것입니다. 그들은 아마도 중요하지 않습니다.

다양한 사용 사례에 대한 내부 / 외부 반복을 비교하려면 http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ 을 참조하십시오 .


답변

TL; DR : List.stream().forEach()가장 빠릅니다.

벤치마킹 반복의 결과를 추가해야한다고 생각했습니다. 벤치마킹 프레임 워크가없는 매우 간단한 접근 방식을 취하고 5 가지 방법을 벤치마킹했습니다.

  1. 권위 있는 for
  2. 클래식 foreach
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

테스트 절차 및 매개 변수

private List<Integer> list;
private final int size = 1_000_000;

public MyClass(){
    list = new ArrayList<>();
    Random rand = new Random();
    for (int i = 0; i < size; ++i) {
        list.add(rand.nextInt(size * 50));
    }
}
private void doIt(Integer i) {
    i *= 2; //so it won't get JITed out
}

이 클래스의 목록은 반복되며 doIt(Integer i)매번 다른 방법을 통해 모든 멤버에게 적용됩니다. Main 클래스에서 테스트 된 메소드를 세 번 실행하여 JVM을 예열합니다. 그런 다음 각 반복 방법에 걸리는 시간을 합산하여 테스트 방법을 1000 번 실행합니다 ( System.nanoTime()). 그 후에 나는 그 합계를 1000으로 나눕니다. 그것이 결과, 평균 시간입니다. 예:

myClass.fored();
myClass.fored();
myClass.fored();
for (int i = 0; i < reps; ++i) {
    begin = System.nanoTime();
    myClass.fored();
    end = System.nanoTime();
    nanoSum += end - begin;
}
System.out.println(nanoSum / reps);

Java 버전 1.8.0_05의 i5 4 코어 CPU에서 이것을 실행했습니다.

권위 있는 for

for(int i = 0, l = list.size(); i < l; ++i) {
    doIt(list.get(i));
}

실행 시간 : 4.21ms

클래식 foreach

for(Integer i : list) {
    doIt(i);
}

실행 시간 : 5.95ms

List.forEach()

list.forEach((i) -> doIt(i));

실행 시간 : 3.11ms

List.stream().forEach()

list.stream().forEach((i) -> doIt(i));

실행 시간 : 2.79ms

List.parallelStream().forEach

list.parallelStream().forEach((i) -> doIt(i));

실행 시간 : 3.6ms


답변

내 의견을 조금 확장해야한다고 생각합니다 …

패러다임에 대하여

아마도 가장 눈에 띄는 부분 일 것입니다. FP는 부작용을 피할 수 있기 때문에 인기를 얻었습니다. 나는 이것이 질문과 관련이 없기 때문에 당신이 이것으로부터 얻을 수있는 찬반을 깊이 파고 들지 않을 것입니다.

그러나 Iterable.forEach를 사용하는 반복은 FP에서 영감을 얻었으며 오히려 더 많은 FP를 Java로 가져 오는 결과입니다 (아이 론적으로는 순수한 FP에서 forEach를 많이 사용하지 않는다고 말합니다. 부작용).

결국 나는 그것이 현재 쓰고있는 취향 \ 스타일 \ 패러다임의 문제라고 말할 것이다.

병렬 처리

성능 관점에서 foreach (…)에 대해 Iterable.forEach를 사용하면 주목할만한 이점이 없습니다.

Iterable.forEach의 공식 문서에 따르면 :

모든 요소가 처리되거나 조치가 예외를 처리 할 때까지 반복 할 때 발생하는 순서대로 Iterable의 컨텐츠에 대해 지정된 조치를 수행합니다 .

… 즉, 문서는 암시 적 병렬 처리가 없다는 것을 분명히 알 수 있습니다. 하나를 추가하면 LSP 위반이됩니다.

이제, Java 8에는 “병렬 콜렉션”이 약속되어 있지만, 더 명확하게 작업하고 그것들을 사용하기 위해서는 약간의주의를 기울여야합니다 (예를 들어 mschenk74의 답변 참조).

BTW :이 경우 Stream.forEach 가 사용되며 실제 작업이 병렬로 수행된다고 보장하지는 않습니다 (기본 컬렉션에 따라 다름).

업데이트 : 눈에 띄지 않고 조금 뻗어 있지만 스타일과 가독성 관점에 또 다른 측면이 있습니다.

우선 평범한 오래된 forloops는 평범하고 오래되었습니다. 모두 이미 알고 있습니다.

둘째, 더 중요한 것은 아마도 하나의 라이너 람다에만 Iterable.forEach를 사용하고 싶을 것입니다. “몸”이 무거워지면 읽을 수없는 경향이 있습니다. 내부 클래스 (yuck) 또는 일반 오래된 forloop를 사용하는 두 가지 옵션이 있습니다. 사람들은 종종 동일한 코드베이스에서 동일한 것들 (컬렉션에 대한 항목)이 다양한 vay / 스타일로 수행되는 것을 보았을 때 짜증이납니다.

다시 말하지만 이것은 문제가 될 수도 있고 아닐 수도 있습니다. 코드 작업을하는 사람들에 따라 다릅니다.


답변

가장 강화 된 기능상 forEach의 한계 중 하나는 점검 된 예외 지원이 없다는 것입니다.

가능한가지 해결 방법 은 터미널 forEach을 일반 이전 foreach 루프 로 바꾸는 것입니다 .

    Stream<String> stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty());
    Iterable<String> iterable = stream::iterator;
    for (String s : iterable) {
        fileWriter.append(s);
    }

다음은 람다 및 스트림 내에서 확인 된 예외 처리에 대한 다른 해결 방법과 함께 가장 많이 사용되는 질문 목록입니다.

예외를 발생시키는 Java 8 Lambda 함수?

Java 8 : Lambda-Streams, 예외가있는 메소드 별 필터링

Java 8 스트림 내부에서 CHECKED 예외를 발생시키는 방법은 무엇입니까?

Java 8 : 람다 식에서 필수 검사 예외 처리 왜 선택이 아닌 필수입니까?