코드를 작성하다보면 for문과 stream문을 이용하여 리스트 내의 모든 요소에 접근하는 로직을 자주 볼 수 있습니다.
stream은 for문 보다 더 좋은 가독성을 갖고 있지만 여전히 for문을 사용하는 경우가 있는데, stream과 for문 로직을 보면서 어떤 로직이 더 좋은 성능을 갖고있는지, stream이 for문을 완전 대체할 수 있는지 구별하는 글을 담고 있습니다.
일반 for문 | 향상된 for문 | Stream | |
---|---|---|---|
형식 | for(초기화 ; 조건 ; 후처리 ) | for (변수 : 리스트) | Stream 생성 -> 중간 연산 -> 최종 연산 |
등장 시기 | Java 1 | Java 5 | Java 8 |
특징 | 빠른 성능 | 높은 가독성 & 안정성 | 가독성 증가 |
// 많이 사용되는 for문
String arr[] = new String[] {"a" , "b" , "c"};
for(int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
String arr[] = new String[] {"a" , "b" , "c"};
for(String data : arr) {
System.out.println(data);
}
일반 for문과 차이점은 java.lang.IndexOutOfBoundsException
예외를 마주치는 상황을 예방할 수 있습니다.
배열 내에 인덱스로 반복하는것이 아니라 배열 내의 원소 하나씩 반복하기 때문에 안정성
이 높습니다.
public void introduceStream(List<Integer> numbers){
return numbers.stream()
.filter(number -> number > 5)
.map(Distance::new)
.collect(Collectors.toList());
}
for문의 경우 코드 블록을 사용합니다.
빨간 네모로 쳐져있는 부분은 하나의 코드블록으로 설명할 수 있습니다.
Stream은 파이프라인을 사용하고 함수 객체로 표현되어, 람다식이나 메서드 참조가 가능합니다. 이는 표현을 간결하게 해주어 가독성 증가
에 좋은 영향을 줄 수 있습니다.
람다 표현식
Stream은 람다식으로 요소 처리 코드를 제공합니다. Stream이 제공하는 대부분 요소는 함수형 인터페이스이며 ,람다식이기 때문에 지역 변수를 수정할 수 없는 단점이 존재합니다. 이는 final로 선언된 변수만 조회할 수 있습니다.
생성, 중간처리, 최종 처리
Stream은 생성, 중간처리, 최종 처리 3단계로 구분되로 로직이 작동합니다.
재사용 불가능
스트림은 생성되고, 중간처리를 거쳐 최종 처리로 가게되면 Stream이 종료됩니다. 이미 닫힌 Stream은 재사용할 수 없으며 Stream을 재사용할 경우 예외가 발생합니다.
continue, break 사용 불가
Stream은 continue와 break 사용이 불가합니다. 사용해야 하는 경우 for문을 사용합니다.
때에 따라 다릅니다
위의 경우 중첩된 if문으로 들여쓰기가 상당이 많이 된것을 볼 수 있습니다.
밑에 stream문을 보면 filter 를 사용하여 if문을 대신 사용하여 가독성이 좋아진 모습을 볼 수 있습니다.
다른 예시로 중첩 for문을 사용하여 구구단을 계산한 것을 볼 수 있습니다.
위의 코드에서는 중첩 for문이 stream 보다 가독성이 좋은 것을 볼 수 있습니다.
Stream의 경우 내부 메서드들을 참조하는 형식이기 때문에 스택 트레이싱이 다소 어렵습니다.
지연 연산
의 특징을 갖고 있어 순차적인 에러 트레이싱이 어렵습니다. 지연 연산에 대한 설명은 해당 블로그 에서 확인할 수 있습니다
반대로 for문의 경우 오류가 한눈에 보기 쉽습니다.
원시 데이터의 경우 stream보다 for문이 7배 가량 더 빠른 것을 볼 수 있습니다. 이는 원시 타입의 데이터
를 처리할 땐 for문을 사용하는 것이 더 빠른 것을 볼 수 있습니다.
stream과 달리 for문은 Java1 부터 나왔기 때문에 JVM 내부에서 최적화가 잘 되어있기 때문입니다.
또한 for문은 단순 인덱스 기반으로 메모리에 바로 접근하기 때문에 오버 헤드가 없지만, Stream의 경우 JVM 처리 ( Stream 객체 생성, Stream에 필요한 객체 생성 , Wrapper 클래스의 박싱 / 언박싱 )이 있어 오버헤드가 존재합니다.
ArrayList와 같은 참조 데이터 형식은 for문과 stream 시간 차이는 크게 나지 않습니다. ( 물론 for문이 조금 더 빠르다. )
ArrayList는 순회하는 비용이 크기때문에 for문과 stream 차이가 없습니다.
함수 내부에 시간 복잡도가 충분히 클때 ( 데이터의 양이 많을 때 ) for문과 stream의 차이는 미미합니다.
// 10 size
for-loop: 0ms
Stream: 3596ms
// 1,000 size
for-loop: 14ms
Stream: 4035ms
// 10,000 size
for-loop: 144ms
Stream: 5477ms
// 100,000 size
for-loop: 1367ms
Stream: 21874ms
// 10 size
for-loop: 12ms
Stream: 867ms
// 1,000 size
for-loop: 543ms
Stream: 1071ms
// 10,000 size
for-loop: 1961ms
Stream: 1750ms
// 100,000 size
for-loop: 9156ms
Stream: 6173ms
?? 참조 데이터의 경우 데이터가 방대해질수록 stream이 for 문 보다 더 빠릅니다..
pc마다 차이가 있을 수 있지만 의외의 결과나 나온 것을 볼 수 있습니다.
참고 블로그 1 : https://velog.io/@foureaf/JAVA-For%EA%B3%BC-Stream%EC%9D%80-%EC%96%B4%EB%96%A4-%EC%B0%A8%EC%9D%B4%EA%B0%80-%EC%9E%88%EB%8A%94%EA%B1%B8%EA%B9%8C
참고 블로그 2 : https://pamyferret.tistory.com/49
참고 블로그 3 : https://velog.io/@tjdtn0219/JavaStream-%EA%B0%9C%EB%85%90-%EC%84%B1%EB%8A%A5-%EC%82%AC%EC%9A%A9-%EC%98%88%EC%A0%9Cfeat.-for%EB%AC%B8-%EB%B9%84%EA%B5%90#:~:text=Stream%20VS%20For%2Dloop,-primitive%20type(%EC%9B%90%EC%8B%9C&text=%EB%B0%B0%EC%97%B4%EC%9D%98%20%EA%B2%BD%EC%9A%B0%20Index%EB%A5%BC,%EA%B0%80%20%EB%B0%9C%EC%83%9D%ED%95%98%EC%97%AC%2C%20%EB%8D%94%20%EB%8A%90%EB%A6%AC%EB%8B%A4.