자바에서 Stream API
를 사용하기 위해서는
stream()
메소드를 이용해서 스트림으로 변환해야 합니다.
하지만 코틀린에서는 따로 스트림으로 변경하지 않아도 Stream API
를 사용할 수 있도록
Iterable
의 확장함수로 구현되어 있습니다.
그래서 보통 다음과 같이 그냥 사용하는 방법을 사용합니다.
listOf(1, 2, 3)
.map { it * it }
.filter { it % 2 == 0 }
.forEach { println(it) }
하지만 컬렉션에 데이터가 많을 경우 asSequence()
를 이용하여 시퀀스로 변경하라고 합니다.
그 이유로는 시퀀스의 Stream API
는 지연 계산을 통해 각 Stream API
를
바로 실행하지 않고 나중에 한 원소에 일련의 연산들을 한 번에 하기 때문이라고 합니다.
그리고 실제로도 빠른 속도를 보이고 있습니다.
그렇다면 무조건 시퀀스로 변경하여 사용하면 되지
왜 데이터가 많을 때만 시퀀스로 변경하라는 걸까요?
일단 컬렉션의 Stream API
와 시퀀스의 Stream API
가 뭐가 다른지 알아보도록 하겠습니다.
// _Collections.map
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
// _Collections.mapTo
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}
// _Sequences.map
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}
// Sequences.TransformingSequence
internal class TransformingSequence<T, R>
constructor(private val sequence: Sequence<T>, private val transformer: (T) -> R) : Sequence<R> {
override fun iterator(): Iterator<R> = object : Iterator<R> {
val iterator = sequence.iterator()
override fun next(): R {
return transformer(iterator.next())
}
override fun hasNext(): Boolean {
return iterator.hasNext()
}
}
internal fun <E> flatten(iterator: (R) -> Iterator<E>): Sequence<E> {
return FlatteningSequence<T, R, E>(sequence, transformer, iterator)
}
}
컬렉션의 Stream API
는 인라인 함수이고
람다를 바로 실행 한다는 특징을 가지고 있고,
시퀀스의 Stream API
는 일반 함수이고
람다를 실행하지 않고 저장한다는 특징을 가지고 있습니다.
여기서 더 정확히 알기 위해서는 inline function
에 대해 이해하여야 합니다.
inline function
인라인 함수는 JIT 컴파일 방식을 강제하도록하는 함수로
람다, 익명 함수와 깊은 연관이 있습니다.매개변수로 SAM Interface를 받는 함수를 익명 함수를 이용해 호출하게 되면
함수 호출마다 객체를 생성하게 되므로 오버헤드가 발생하게 됩니다.
하지만 캡처링 하지 않은 람다식으로 호출하게 되면 람다를 따로 저장해두었다가
계속 사용하는 방식으로 바이트코드가 컴파일됩니다.하지만 캡처링 한 람다식의 경우에는 람다가 가지고 있는 변수가 각각 다르므로
저장해두었다가 사용하는 방법을 사용할 수 없으므로 계속 객체를 생성하게 됩니다.여기서 인라인 함수는 바이트코드로 컴파일할 때 함수의 본문을 함수를 호출한 곳으로
이동하여 바이트코드를 생성하도록 강제합니다.원래 JIT 컴파일러가 많이 사용되는 부분을 native 코드로 변경하여
재사용하도록 하게 하지만 JIT 컴파일러가 캡처링 하지 않은 람다식을 구별하여
결정할 정도로 똑똑하지 않아서 개발자가 inline function을 지정해주어야합니다.
더 정확한 정보는 이 글을 참조해주세요.함수의 본문이 그리 크지 않다면 바이트코드가 많아져서 무거워질 일도 없으니
캡처링하지 않은 람다를 받는 함수를 인라인 함수로 만들면 속도의 향상이 있을 것입니다.
컬렉션의 Stream API
는 캡처링하지 않은 람다를 받으므로 인라인 함수로 구현하였고
시퀀스의 Stream API
는 람다를 실행하지 않고 새로운 클래스에 저장하게 되므로
람다가 상태를 가지게 되어 인라인 함수로 구현할 수 없었습니다.
따라서 시퀀스를 사용하게 되면 람다를 실행할 때마다
새로운 (익명) 객체가 생성되는 오버헤드가 있고
컬렉션을 사용하게 되면 람다 실행 후 무조건 스트림을 생성해야 한다는 오버헤드가 있습니다.
이렇게 람다를 만드는 객체를 생성하는 것의 오버헤드가 큰가,
매번 스트림을 생성하는 오버헤드가 큰지를 따져서 asSequence()
를 사용해야 합니다.
보통 원소의 수가 적을 때는 람다 객체를 만드는 것이 오버헤드가 더 크고
원소의 수가 많을 때는 스트림을 매번 생성하는 것이 오버헤드가 더 크기 때문에
컬렉션의 크기가 클 때만 asSequence()
를 이용하여 시퀀스를 사용하라고 하는 것입니다.
컬렉션의 크기가 클 때만 asSequence()
를 이용해서 시퀀스를 사용하자!!