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 ()에 대한 큰 팬이 아닙니다.
특히 나에게 불쾌한 점은 Stream
이 Iterable
구현하지 않고 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)
)를 사용하여 단일 함수를보다 명확하게 호출합니다 . 그러나 검사 된 예외, 더 어려운 디버깅 및 코드 작성시 사용하는 관용구 수를 줄이십시오.
참조 용으로 사용한 기사 :
- Java 8에 관한 모든 것
- 내부 및 외부 반복 (다른 포스터에서 지적한대로)
편집 : 람다 (예 : 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 가지 방법을 벤치마킹했다 :
- 고전적인
- 고전적인 foreach
-
List.forEach()
-
List.stream().forEach()
-
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