java - with - 자바 스트림 성능




Java 8 Iterable.forEach()와 foreach 루프 (6)

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

Java 8 :

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

Java 7 :

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

나는 람다 (lambdas)로 "단순화"될 수있는 많은 루프를 가지고 있지만, 실제로 그것을 사용하는 이점이 있습니까? 성능과 가독성을 향상시킬 수 있습니까?

편집하다

또한이 질문을 더 긴 방법으로 확장 할 것입니다. 나는 당신이 람다로부터 부모 함수를 반환하거나 깨뜨릴 수 없다는 것을 안다. 또한 이것들을 비교할 때 고려되어야하지만, 고려해야 할 다른 것이 있는가?


1.7 이상의 Java 1.8 forEach 메소드의 장점 향상된 루프는 코드를 작성하는 동안 비즈니스 로직에만 집중할 수 있다는 것입니다.

forEach 메소드는 java.util.function.Consumer 객체를 인수로 사용하므로 비즈니스 로직을 별도의 위치에 두어 언제든지 다시 사용할 수 있습니다.

아래의 스 니펫을 살펴보고,

  • 여기에 소비자 클래스의 수락 클래스 메소드를 오버라이드 할 새로운 클래스를 만들었습니다.이 클래스에서는 추가 기능성, 반복 이상을 추가 할 수 있습니다. !!!!!!

    class MyConsumer implements Consumer<Integer>{
    
        @Override
        public void accept(Integer o) {
            System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o);
        }
    }
    
    public class ForEachConsumer {
    
        public static void main(String[] args) {
    
            // Creating simple ArrayList.
            ArrayList<Integer> aList = new ArrayList<>();
            for(int i=1;i<=10;i++) aList.add(i);
    
            //Calling forEach with customized Iterator.
            MyConsumer consumer = new MyConsumer();
            aList.forEach(consumer);
    
    
            // Using Lambda Expression for Consumer. (Functional Interface) 
            Consumer<Integer> lambda = (Integer o) ->{
                System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o);
            };
            aList.forEach(lambda);
    
            // Using Anonymous Inner Class.
            aList.forEach(new Consumer<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
    

내 의견을 약간 연장해야한다고 생각합니다.

패러다임 / 스타일 정보

아마 가장 주목할만한 부분 일 것입니다. FP는 부작용을 피할 수 있기 때문에 인기가있었습니다. 나는이 질문과 관련이 없기 때문에 당신이 이것을 통해 얻을 수있는 장점에 대해 깊이 파고 들지 않을 것이다.

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

결국 나는 그것이 현재 쓰는 맛 \ 스타일 \ 패러다임의 문제라고 말하고 싶습니다.

병렬성에 대해서.

성능 관점에서 foreach (...)를 통해 Iterable.forEach를 사용하면 별다른 이점이 없습니다.

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

반복 처리시에 요소가 발생 해 모든 요소가 처리 될 때까지, 또는 액션이 ​​예외를 Throw 할 때까지, Iterable의 내용으로 지정된 액션을 실행합니다.

... 즉, 암묵적인 병렬 처리가 없을 것이라는 점을 꽤 잘 알고 있습니다. 하나를 추가하는 것은 LSP 위반 일 것입니다.

이제는 Java 8에서 약속 한 "병렬 컬렉션"이 있지만 더 분명하게 필요한 사용자와 함께 작업하고이를 사용하기 위해 특별히주의해야합니다 (예 : mschenk74의 대답 참조).

BTW :이 경우 Stream.forEach 가 사용되며 실제 작업이 병렬로 수행되는 것을 보증하지 않습니다 (기본 모음에 따라 다름).

업데이트 : 그다지 눈에 띄지는 않을 수도 있지만 스타일과 가독성 관점의 또 다른 측면이 있습니다.

무엇보다도 우선 일반 오래된 forloops는 평범하고 오래된 것입니다. 모두가 이미 알고 있습니다.

둘째, 그리고 더 중요한 - 아마 one-liner lambda 만 사용하여 Iterable.forEach를 사용하고 싶을 것입니다. "몸"이 무거워 진다면, 그들은 읽을 수없는 경향이 있습니다. 여기에서 두 가지 옵션을 사용할 수 있습니다 - 내부 클래스 (yuck)를 ​​사용하거나 일반 old forloop을 사용하십시오. 사람들은 동일한 코드베이스에서 다양한 vay / 스타일을 수행하는 것과 같은 일 (콜렉션에 대한 iteratins)을 볼 때 종종 짜증을냅니다.

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


더 나은 방법은 for-each 를 사용하는 것 for-each . Keep It Simple, Stupid 원칙을 위반하는 것 외에도 새로운 forEach() 에는 적어도 다음과 같은 결함이 있습니다.

  • 비 최종 변수는 사용할 수 없습니다 . 그래서, 다음과 같은 코드는 for each 람다로 변환 될 수 없습니다 :

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • 확인 된 예외를 처리 할 수 ​​없습니다 . 람다는 실제로 체크 예외를 던지는 것이 금지되어 있지는 않지만, Consumer 와 같은 일반적인 기능 인터페이스는 any를 선언하지 않습니다. 따라서 검사 된 예외를 throw하는 모든 코드는 try-catch 또는 Throwables.propagate() 래핑해야합니다. 그러나 당신이 그렇게하더라도, 던져진 예외에 어떤 일이 일어나는지 항상 명확하지는 않습니다. forEach() 의 내장에서 어딘가에 삼킬 수 있습니다.

  • 제한된 유량 제어 . 람다에서의 return 은 for-each에서의 continue 와 같지만, break 와 같은 것은 없습니다. 반환 값, 단락 또는 집합 플래그 ( 비 최종 변수 규칙을 위반하지 않은 경우 조금 완화했을 수 있음)와 같은 작업을 수행하는 것도 어렵습니다. 이것은 단순한 최적화는 아니지만 파일의 행을 읽는 것과 같은 일부 시퀀스에는 부작용이 있거나 무한 시퀀스가있을 수 있다고 생각할 때 매우 중요합니다. "

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

  • JIT가 forEach () + lambda를 일반 루프와 동일한 정도로 최적화 할 수 없기 때문에 성능이 저하 될 수 있습니다. 특히 람다가 새로운 것이므로 더욱 그렇습니다. "최적화"란 lambda를 호출하는 오버 헤드가 아니라 현대 JIT 컴파일러가 실행중인 코드에서 수행하는 정교한 분석 및 변환을 의미합니다.

  • 병렬 처리가 필요하다면 ExecutorService를 사용하는 것이 훨씬 빠르고 어렵지 않을 것 입니다. 스트림은 자동 (읽기 : 문제에 대해 많이 알지 못함) 병렬화 전략 ( 포크 - 조인 재귀 분해 ) 같은 특수화 된 (읽기 : 비효율적 인) 일반화 된 방법을 사용합니다.

  • 중첩 된 호출 계층 구조 및 신 금지, 병렬 실행으로 인해 디버깅을보다 혼란스럽게 만듭니다 . 디버거에서 주변 코드의 변수를 표시하는 데 문제가있을 수 있으며 단계별 실행과 같은 작업이 예상대로 작동하지 않을 수 있습니다.

  • 일반적으로 스트림은 코드 작성, 읽기 및 디버그가 더 어렵습니다 . 실제로 이는 일반적으로 복잡한 " fluent "API에 해당합니다. 복잡한 단일 명령문의 결합, 제네릭의 과도한 사용 및 중간 변수의 부족은 혼란스러운 오류 메시지를 생성하고 디버깅을 방해합니다. 대신 "이 방법은 X 유형에 대한 과부하가 없습니다"라는 오류 메시지가 "어딘가에서 유형을 엉망으로 만들었지 만 우리는 어디서 어떻게 알 수 없습니다." 마찬가지로 코드를 여러 문장으로 나누고 중간 값을 변수에 저장할 때처럼 쉽게 디버거에서 단계를 밟아 검사 할 수 없습니다. 마지막으로 코드를 읽고 실행의 각 단계에서 유형과 동작을 이해하는 것은 중요하지 않을 수 있습니다.

  • 아픈 엄지처럼 스틱 . Java 언어에는 이미 for-each 문이 있습니다. 왜 함수 호출로 대체할까요? 왜 표현의 어딘가에 부작용을 숨기는 것이 좋습니다? 왜 다루기 힘든 one-liners를 장려 하는가? 규칙적인 for-each와 for for willly-nilly는 나쁜 스타일입니다. 코드는 관용구 (반복으로 인해 이해하기 쉬운 패턴)로 사용되어야하며 관용구가 적을수록 코드가 명확하고 사용법을 결정하는 데 소요되는 시간이 적어집니다 (나 자신과 같은 완벽 주의자에게는 큰 시간 낭비입니다! ).

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

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

forEach() 는 다음에 유용합니다.

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

  • 병렬 실행 (적절한 병렬 스트림 사용) . 이렇게하면 Streams 및 Spliterator에 내장 된 성능 가정과 문제가 일치하는 경우 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 인터페이스 만 구현하면 더 이상 참이되지 않습니다.


forEach() 는 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 : List.stream().forEach() 가 가장 빠릅니다.

나는 벤치마킹 반복에서 내 결과를 추가해야한다고 느꼈다. 나는 매우 간단한 접근법 (벤치마킹 프레임 워크가 없음)을 취하고 5 가지 방법을 벤치마킹했다 :

  1. 고전적인
  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을 예열합니다. 그런 다음 각 반복 메소드 ( System.nanoTime() 사용)에 소요되는 시간을 합산하여 1000 회 테스트 메소드를 실행합니다. 완료되면 그 합계를 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(int i = 0, l = list.size(); i < l; ++i) {
    doIt(list.get(i));
}

실행 시간 : 4.21 ms

고전적인 foreach

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

실행 시간 : 5.95 ms

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.6 ms







java-stream