컬렉션 함수를 사용하면 간결하고, 효율적이고, 이해하기 쉬운 코드를 만들 수 있다.
또한, 컬렉션 함수는 아래처럼 연쇄 호출(method chaining)을 할 수 있다.
people.filter { it.name.startsWith("A") }.map { it.name }
하지만 단점이 없는 건 아니다. 연쇄 호출을 하면 매번 중간(임시) 컬렉션을 생성한다.
위 예시에서 filter, map의 결과로 컬렉션 객체가 2개 생성된다. 이것은 원소의 수가 수십만 개 이상일 때 효율을 떨어뜨린다.
이러한 상황을 효율적으로 바꾸고 싶으면 컬렉션을 직접 사용하는 대신 시퀀스(Sequence)를 사용해야 한다.
시퀀스 (Sequence)
people.asSequence() // 원본 컬렉션을 시퀀스로 변환
.map(Person::name) // 시퀀스도 컬렉션과 같은 API를 제공한다.
.filter { it.startsWith("A") }
.toList() // 결과 시퀀스를 리스트로 되돌린다
위 코드는 이전 예시의 시퀀스 사용 버전이다.
시퀀스를 사용하면 중간 컬렉션이 생성되지 않아서 원소가 많을 때 성능이 매우 좋아진다.
시퀀스란?
시퀀스(Sequence)는 기본적으로 인터페이스이다.** 시퀀스 자체는 자신이 갖고 있는 데이터를 순서대로 반환할 수 있는 데이터 타입**임을 의미한다.
public interface Sequence<out T> {
/**
* Returns an [Iterator] that returns the values from the sequence.
*/
public operator fun iterator(): Iterator<T>
}
인터페이스에는 iterator 메서드 하나만 정의되어 있는데, 반환되는 Iterator를 통해 원소를 하나씩 리턴받을 수 있다.
시퀀스가 중간 컬렉션을 생성하지 않는 이유는 이 인터페이스를 구현하고 있는 클래스의 연산 수행 방식에 있다.
시퀀스의 연쇄 연산 순서
시퀀스는 원소 단위로 연산을 수행한다.
listOf(1,2,3,4).asSequence() // 컬렉션을 시퀀스로 변경
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
.toList() // 결과 시퀀스를 리스트로 되돌린다
컬렉션으로 위 연산을 진행한다면 모든 원소에 대해 map 함수를 수행하고, 그 결과 컬렉션에 filter 함수를 수행한다. 따라서, map에 의한 중간 컬렉션이 생성된다.
반면 시퀀스는 첫 번째 원소를 map 연산하고, 그 결과 값에 filter를 수행한다. 그리고 두 번째 원소에 대해 map -> filter 연산을 수행한다.
위 코드의 결과는 아래와 같다.
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)
이렇게 원소 단위로 연산을 연쇄 수행하기 때문에 중간 컬렉션이 생기지 않는다.
꼭 시퀀스를 컬렉션으로 되돌려야 하나
예시를 보면 마지막에 시퀀스를 리스트로 되돌린다. 시퀀스가 효율적이면 시퀀스를 계속 쓰는 게 좋을텐데 말이다.
시퀀스의 결과를 차례로 이터레이션 하기만 할거면 시퀀스를 써도 된다. 하지만 인덱스 접근과 같이 컬렉션에서 제공하는 기능을 사용해야 한다면 컬렉션으로 되돌려야 한다.
지연(lazy) 연산
시퀀스의 또 하나의 특징은 지연 연산을 한다는 것이다.
val sequence = List(1, 2, 3, 4).asSequence()
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
위 코드는 이전 예시에서 마지막 toList() 호출부를 제외한 것이다. 이 코드를 수행하면 아무런 내용도 출력하지 않는다. 시퀀스는 최종 시퀀스를 이터레이션하거나 리스트로 변환해야 비로소 연산이 수행되기 때문이다.
위 코드는 그저 어떤 연산을 수행해서 원소를 반환하는 시퀀스인지를 알려줄 뿐이다.
정리
원소의 개수가 많은 거대한 컬렉션에 연쇄적인 연산을 적용해야 할 경우 시퀀스를 사용하자.
시퀀스의 특징
- 중간 컬렉션을 사용하지 않아서 연속적인 계산을 효율적으로 할 수 있다.
- 지연 연산을 한다.
참조
책: Kotlin In Action
'TIL' 카테고리의 다른 글
[Kotlin] 람다(lambda)란 무엇이고 왜 사용하는 걸까? (0) | 2023.06.14 |
---|---|
(2) SOLID 원칙 정리 (객체 지향 프로그래밍) (0) | 2023.04.05 |
[Kotlin] 코틀린의 scope 함수 정리 (let, with, run, apply, also) (0) | 2023.04.04 |
(1) 의존성 주입(Dependency Injection, DI)이란? - 개념과 장점 (0) | 2023.04.03 |
[Android] ViewPager2 좌우 미리 보기 + 페이지 확대/축소 애니메이션 구현 기록 (0) | 2023.03.15 |