컬렉션 함수를 사용하면 간결하고, 효율적이고, 이해하기 쉬운 코드를 만들 수 있다.
또한, 컬렉션 함수는 아래처럼 연쇄 호출(method chaining)을 할 수 있다.
people.filter { it.name.startsWith("A") }.map { it.name }
하지만 단점이 없는 건 아니다. 연쇄 호출을 하면 매번 중간(임시) 컬렉션을 생성한다.
위 예시에서 filter, map의 결과로 컬렉션 객체가 2개 생성된다. 이것은 원소의 수가 수십만 개 이상일 때 효율을 떨어뜨린다.
이러한 상황을 효율적으로 바꾸고 싶으면 컬렉션을 직접 사용하는 대신 시퀀스(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)
이렇게 원소 단위로 연산을 연쇄 수행하기 때문에 중간 컬렉션이 생기지 않는다.
예시를 보면 마지막에 시퀀스를 리스트로 되돌린다. 시퀀스가 효율적이면 시퀀스를 계속 쓰는 게 좋을텐데 말이다.
시퀀스의 결과를 차례로 이터레이션 하기만 할거면 시퀀스를 써도 된다. 하지만 인덱스 접근과 같이 컬렉션에서 제공하는 기능을 사용해야 한다면 컬렉션으로 되돌려야 한다.
시퀀스의 또 하나의 특징은 지연 연산을 한다는 것이다.
val sequence = List(1, 2, 3, 4).asSequence()
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
위 코드는 이전 예시에서 마지막 toList() 호출부를 제외한 것이다. 이 코드를 수행하면 아무런 내용도 출력하지 않는다. 시퀀스는 최종 시퀀스를 이터레이션하거나 리스트로 변환해야 비로소 연산이 수행되기 때문이다.
위 코드는 그저 어떤 연산을 수행해서 원소를 반환하는 시퀀스인지를 알려줄 뿐이다.
원소의 개수가 많은 거대한 컬렉션에 연쇄적인 연산을 적용해야 할 경우 시퀀스를 사용하자.