...을 들어가기 전에 이 결과를 맞혀보자!
Stream.of(1, 2, 3)
.map(it -> {
System.out.println("map " + it + " -> ");
return it;
})
.filter(it -> {
System.out.println("filter " + it + " -> ");
return true;
})
.forEach(it -> {
System.out.println("forEach " + it + " -> ");
});
// map 1 -> map 2 -> map 3 -> filter 1 -> filter 2 -> filter 3 -> forEach 1 -> forEach 2 -> forEach3
이걸 예상했다면 틀렸다.. 글을 읽고 약간의 지식을 얻어갈 수 있었으면 좋겠다!!
정답은 바로
// map 1 -> filter 1 -> forEach 1 -> map 2 -> filter 2 -> forEach 2 -> map 3 -> filter 3 -> forEach 3
순으로 출력이 된다!!
대표적인 특징 중 하나가 바로 지연 연산이다.
지연 연산이란 결과 값이 필요할 때까지 계산을 늦추는 기법이다.
계산을 지연시켜서 필요한 연산만 실행될 수 있도록 스트림 파이프라인을 최적화시킨다.
JVM은 스트림 연산을 곧바로 실행시키지 않고 스트림 파이프라인이 어떤 연산을 가지고 있는지, 어떻게 구성되어있는지 검사를 하고 최적화를 진행하여 수행시킨다.
스트림에서 제공하는 최적화 전략에는 대표적으로 루프 퓨전과 쇼트 서킷이 있다.
루프 퓨전이란 파이프라인에서 체이닝된 복수의 스트림 연산을 하나의 연산 과정으로 병합시키는 것을 의미한다.
위의 예시도 루프 퓨전이 적용된 것이다.
루프 퓨전은 개별 스트림 요소에 접근하는 횟수를 줄여 최적화하는 방식이다.
루프 퓨전이 없었더라면 개별의 스트림 연산에서 모든 요소를 다 순회해야 할 것이다. 위의 예시를 대입해본다면 map에서 1, 2, 3을 순회하고 filter에서 1, 2, 3을 순회하고 forEach에서 1, 2, 3을 순회하는.. 총 9번을 요소에 접근해야 한다.
그러나 루프 퓨전 방식이 적용되었기에 요소 순서대로 1이 map, filter, forEach 다 돌고 2가 다 돌고 3이 다 도는 방법으로 각 한 번씩, 총 3번만 접근된다는 것을 알 수 있었다.
Stream.of(1, 2, 3)
.map(it -> {
System.out.println("map " + it + " -> ");
return it;
})
.filter(it -> {
System.out.println("filter " + it + " -> ");
return true;
})
.limit(2)
.forEach(it -> {
System.out.println("forEach " + it + " -> ");
});
이 예시에 대해서도 결과를 예측해보자!
위의 예시에서 최종 연산 전 limit() 연산만 추가된 것이다.
// map 1 -> filter 1 -> forEach 1 -> map 2 -> filter 2 -> forEach 2
결과를 보면 요소 3이 스트림 연산을 수행하지 않았다.
그 이유는 limit(2) 연산으로 인해 3번째 요소는 수행되지 않은 것이다.
이러한 것처럼 불필요한 연산을 의도적으로 수행하지 않음으로써 실행 속도를 높이는 기법이 쇼트 서킷 이다.
쇼트 서킷은 무한 스트림을 다루는데 있어서 필수이다.
IntStream.iterate(1, it -> it + 1)
.limit(10); // 이 라인이 없다면 1부터 끝 없이 수가 계속 생성
루프 퓨전과 쇼트 서킷은 당연히 같이 적용될 수 있다.
이러한 경우에 적용되는 과정을 알아야 헷갈리지 않고 Stream을 사용할 수 있다.
IntStream.of(7, 1, 10, 6, 2)
.map(it -> {
System.out.print(it + " ");
return it;
})
.sorted()
.limit(3)
.forEach(it -> System.out.print(" " + it);
// 7 1 10 6 2 1 2 6
IntStream.of(7, 1, 10, 6, 2)
.map(it -> {
System.out.print(it + " ");
return it;
})
.limit(3)
.sorted()
.forEach(it -> System.out.print(" " + it));
// 7 1 10 1 7 10
두 코드는 sorted()와 limit()의 위치만 서로 다를 뿐인데 결과가 아예 다르다.
그 이유는 sorted() 메서드는 이전 요소들의 값을 가지고 있어야하는 상태 연산이기 때문이다.
첫 번째 코드 기준, 원래는 루프 퓨전이 적용되어 sorted()를 지나 forEach()까지 요소마다 개별 연산이 시행되어야한다.
그러나 상태 연산인 sorted() 때문에 루프 퓨전이 중지되고 모든 요소에 대해 정렬이 된다. 그 후 limit() 연산이 수행된다.
두 번째 코드는 limit() 연산으로 6과 2는 쇼트 서킷을 통해 아예 map() 연산도 되지 않고 [7, 1, 10]에 대해서만 정렬 연산이 수행된다.
또 문제가 될 수 있는게 위의 예시 코드에서는 유한 스트림을 생성해서 연산을 적용한거지만 만약 무한 스트림이라면 어떻게 될까?
1부터 10까지 역순으로 출력하는 스트림을 만드려는데 실수로 순서를 반대로 만들었다면...
Stream.iterate(1, it -> it + 1)
.sorted(Comparator.reverseOrder())
.limit(10)
.forEach(System.out::print);
이 코드를 실행시키면 OutOfMemoryError 가 발생한다.
쇼트-서킷이 실행되지 않아 스트림의 생성이 무한히 된다.