java
java stream
java8의 새로운 기능
java stream 구성 요소
- a stream source
- zero or more intermediate operations
- a terminal operation
someList // Data Source
.stream()
.filter(...)
.map(...) // Intermediate Operation
.collect(Collectors.toList()); // Terminal Operation
- intermediate operation은 Terminal Operation이 실행될 때 비로소 실행된다.
- lazy operation
- 여러 Intermeidate Operation은 Terminal Operation에서 최적화되어서 수행될 수 있다.
- m크기의 stream에 n개의 operation을 돈다고 O(m*n)의 수행 시간이 걸리는 것이 아니라 최소 O(m)으로도 최적화될 수 있다.
java parallel stream
parallel stream의 경우 기존 stream을 쉽게 parallel하게 처리할 수 있도록 해준다.
someList
**.parallel()** // parallel 처리
.filter(...)
.map(...)
.collect(Collectors.toList());
parallel stream은 내부적으로 멀티 쓰레드를 사용하고, 모든 parallel stream은 동일한 쓰레드풀을 공유한다.
- 따라서 하나의 stream에서 blocking IO와 같은 시간이 오래 걸리는 작업이 존재한다면 다른 parallel stream도 영향을 받게 된다.
parallel stream은 fork-join 방식의 병렬처리를 사용
- 각 작업을 재귀적으로 분할 후 서브 테스크의 결과를 합쳐서 최종 결과를 생성
- 분할 및 통합 과정에서의 비용도 존재

- 따라서 데이터의 크기가 크기 않다면 성능 효과가 적거나 오히려 악화될 수 있다.
parallel stream에서의 boxing을 유의
- 각 stream마다 모두 boxing이 되므로, boxing을 하게 되는 경우 기본 stream보다 성능 악화가 심하다
- 기본형 특화 스트림(IntStream, LongStream, DoubleStream) 사용을 고려
요소 순서에 의존하는 연산의 경우 비싼 비용을 요구
- ex) limit, findFirst
kotlin
kotlin collection
kotlin의 collection은 java stream과 유사한 문법과 함수를 지원
- 하지만 이는 단순히 collection의 확장 함수
- lazy operation이 아니다
someList
.map { ... }
.filter { ... }
- 즉 위와 같은 경우 O(2*m)의 수행 시간이 걸린다.
kotlin sequence
kotlin에서도 동일한 lazy operation을 적용하기 위해서 sequence를 사용할 수 있다.
someList
.asSequence()
.map { ... }
.filter { ... }
.toList()
- asSequence를 사용해 sequence로 변환
- toList가 호출될 때 intermediate operation이 수행된다.
- 이때 해당 함수는 최적화되어서 수행된다.
java의 sequence는 java8 이상부터 지원되지만, kotlin의 sequence는 java8 미만에서도 사용할 수 있다.
kotlin sequence vs java stream
java stream의 장점
- auto-boxing을 하지 않고 primitive type을 즉시 사용할 수 있는 기본형 특화 스트림이 존재
- primitive type은 data가 stack에 존재하는 반면, wrapper type은 heap에 존재하기 때문에 auto-boxing을 하지 않고 primitive type을 바로 사용하게 되면 성능 상 이점이 존재
- kotlin에서도 auto-boxing을 완화할 수 있는 방식이 존재하지만 코드 상에서 일일이 설정하고 고려해줘야 한다.
- // Before val adultAgesSquared = people.asSequence() .map { it.age } // autobox non-nullable age .filter { it >= 18 } // throw away some autoboxed values .map { it * it } // square and autobox again .toList() // After - No unnecesarry autoboxing val adultAgesSquared = people.asSequence() .filter { it.age >= 18 } .map { it.age * it.age } // single autobox .toList()
- 병렬처리가 쉽다.
- parallel stream을 사용해 쉽게 병렬처리가 가능
- 하지만 병렬 처리 시에는 blocking IO 및 처리 시간이 오래 걸리는 작업 등을 주의해서 수행해야 한다.
kotlin sequence의 장점
- platform type을 사용하지 않는다.
- kotlin에서 java stream을 사용하게 되면 반환값이 platform-type으로 반환되게 된다.
- 이는 kotlin에서의 null-safety함을 위배할 수 있고, runtime error의 발생 위험성을 증가시킨다.
- java stream에 비해 더 간결한 코드 작성이 가능하다.
- 간결한 aggregation 및 terminal operation 작성이 용이
// Sequence
people.asSequence()
.mapNotNull { it.testScore } // map & filter in 1 action
...
// Stream
people.stream()
.map { it.testScore }
.filter { it != null }
...
// Sequence
val nameOfOldestHealthyPerson = people.asSequence()
.filter { it.isHealthy() }
.maxBy { it.age }
?.name
// Stream
val nameOfOldestHealthyPerson = people.stream()
.filter { it.isHealthy() }
.max(Comparator.comparing(Person::age))
.get()
.name
- nullable 처리가 쉽다.
// Sequence
val nameOfAdultWithLongName = people.asSequence()
...
.find { it.name.length > 5 }
?.name
// Stream
val nameOfAdultWithLongName = people.stream()
...
.filter { it.name.length > 5 }
.findAny()
.get() // unsafe unwrapping of Optional
.name
conclusion
- 코드 가독성 및 null-type으로부터의 안정성이 중요한 경우 kotlin sequence
- 큰 list에 대해 성능 최적화가 필요한 경우 java 기본형 stream
- 큰 list에 대해 parallel 처리가 필요한 경우 java parallel stream
참고
Java Streams vs. Kotlin Sequences
코틀린의 collection, sequence, stream