for-loop vs stream
Definition?
for-loop
- 일반적으로 사용하는 for 구문
- 자바 8 이전에 Collection를 다루기 위해서 사용
for (Person person : list) {
func(person);
}
//
for (int i = 0; i < list.size(); i++){
func(list.get(i));
}
stream
- 자바 8부터 Collection를 다루기 위해서 사용 가능
- Lazy Evaluation
- filter
- 특정 element를 걸러내기 위한 작업이 가능
- collect
- stream으로 처리된 element들이 원하는 Collection으로 손쉽게 변환이 가능
- map
- element를 원하는 형태로 변환이 가능
final List<Byte> collect = list.stream()
.filter(i -> i > 1)
.map(Integer::byteValue)
.collect(Collectors.toList());
Lazy Evaluation
- Lazy Evaluation
- 함수형 프로그래밍에서 적용되며, 계산값(Parameter)가 들어올때까지 계산을 늦춤
-
@Test public void lazy_test(){ final Supplier<Integer> lazy = () -> { System.out.println("Lazy called"); return Integer.valueOf(5 + 3 * (1 + 5 ^ 2)); }; System.out.println(lazy.get()); System.out.println(lazy.get() + 2); }
- 수식을 저장하는 별도의 함수 공간이 필요하며, lazy.get()에 대한 수식 결과를 저장하지 않고, 계산해야 할 값이 들어올때 수식 계산
- Eager Evalution
- 함수형 프로그래밍과 반대로 수식에 대해서 별도의 저장이 필요하지 않음
-
x = 5 + 3 * (1 + 5 ^ 2); System.out.println(x); System.out.println(print x + 2);
- x에 처음 출력될 때, 83이 x의 공간에 저장되고, x + 2가 호출되면 83 + 2로 변환
- 수식에 대한 결과가 바로 저장되기 때문에, 계산해야 할 값이 하나만 존재한다면 효율적으로 사용 가능
Example
@Test
public void for문_stream_변환(){
final List<Person> copy = new ArrayList<>(list);
int sum = 0;
for(int i = 0; i < list.size(); i++){
if(list.get(i).getIndex() % 2 == 0) {
if(list.get(i).getIndex() % 3 == 0) {
sum += list.get(i).getIndex();
}
}
}
final int streamSum = copy.stream()
.map(Person::getIndex)
.filter(index -> index % 2 == 0)
.filter(index -> index % 3 == 0)
.reduce(Integer::sum)
.get();
assertEquals(sum, streamSum);
}
- 유지 보수를 위해 동작을 추가할 때, 기존 코드에 영향을 최소화하며 추가 가능
- 여러가지 연산을 한 줄의 코드로 간단히 표현할 수 있음(가독성 증가)
Difference?
for-loop
- 일반적으로 stream의 forEach()보다 performance가 좋음(이유에 대해선 Conclusion에서 설명)
- stream보다 가독성이 좋지 않음
- 원시적 타입(int)의 Array 탐색에 대해서는 for-loop가 훨씬 성능이 좋음
- 원시적 타입은 Stack 영역에서 접근되므로, 데이터에 대한 접근 시간이 오래 걸리지 않고 원시적 타입에 대한 컴파일러 최적화가 잘 이루어져 있음
- 반면에, 원시적 타입이 아닌 선언한 타입(Class)에 대해서는 Heap 영역에서 접근이 이루어지기 때문에, 접근 시간이 오래 걸리며 컴파일러 최적화가 Stack 영역만큼 좋지 않기 때문에 for-loop가 stream의 차이가 원시적 타입보단 작음
stream
- parellel 처리가 가능하므로, 데이터의 수가 많다면, for-loop보다 빠를 수 있음
- parllelStream()으로 간단하게 병렬 처리가 가능
- 데이터 수에 대한 정확한 기준이 없으므로, 구축된 환경에 맞게 데이터 수에 대한 기준을 세우기 위한 테스트 필요
- 가독성이 좋고 유지 보수가 용이함
- for문에 많은 로직이 포함되어 있다면, 가독성이 떨어짐
Conclusion?
list.forEach(item -> func(item));
위 코드처럼 stream을 단순히 forEach()만을 위해서 쓰는 것은 올바른 사용이 아니다. 또한 forEach()를 호출하는 stream이 parllelStream()이 아니라면 성능적으로 손해가 발생한다.
아래에 코드에서 볼 수 있듯이, forEach도 for-loop를 호출하기 때문에 결과적으론 같은 행위를 하는 것이다. 아래 코드를 보면 데이터가 작을수록 for-loop가 stream의 성능 차이가 나는 이유를 알 수 있다.
// ArrayList forEach
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (E e : a) {
action.accept(e);
}
}
for-loop에 대한 호출과 함수형 인터페이스를 검증하기 위한 코드가 있기 때문에 오버헤드가 발생했다. 데이터의 수가 작을수록 for문으로 순회하는 시간이 작기 때문에 추가된 코드들이 경과 시간에 영향을 줄 수 있게 된다.
forEach()만을 사용하는 stream()은 지양해야 한다. 데이터를 여러 차례에 걸쳐 가공이 필요할 때 stream()을 쓰는 것이 맞다.