컬렉션 연산시 지연(lazy), 즉시(eagerly) 계산의 차이는 무엇인가?
- 즉시(eagerly)연산은 커렉션함수를 연쇄할 때마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다.
- 시퀀스(sequence)를 사용하면 중간 임시 컬렉션을 사용하지 않고 컬렉션 연산을 연쇄한다.
people
.filter { it.lastName != null }
.filter { it. age!= null }
.find { it.age == 31 }
people.asSequence() // 컬렉션을 시퀀스로 변환
.filter { it.lastName != null } // Intermediate operation
.filter { it. age!= null } // Intermediate operation
.find { it.age == 31 } // Terminal operation
- sequence를 사용하지 않으면 원소가 수백만 개가 되었을 때 효율이 떨어질 수 있다. (sequence는 중간 결과를 저장하는 컬렉션을 만들지 않기 때문)
- 시퀀스 원소를 인덱스를 사용해 접근하는 등의 API 메서드가 필요하다면 시퀀스 -> Collection으로 변환하는 것이 좋다.
map이나 filter 같은 몇 가지 컬렉션 함수는 결과컬렉션을 즉시(eagerly) 생성한다. 이는 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다는 말이다. 시퀀스(sequence) 를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.
people.map(Person::name).filter { it.startsWith("A") }
filter와 map이 리스트를 반환한다. 이는 이 연쇄 호출이 리스트를 2개 만든다는 뜻이다. 한 리스트는 filter의 결과를 담고, 다른 하나는 map의 결과를 담는다. ⇒ 원소가 많아지면 효율이 떨어진다.
이를 더 효율적으로 만들기 위해서는 각 연산이 컬렉션을 직접 사용하는 대신 시퀀스를 사용하게 만들어야 한다.
people.asSequence() // 원본 컬렉션을 시퀀스로 변환
.map(Person::name)
.filter { it.startsWith("A") }
.toList() // 결과 시퀀스를 다시 리스트로 변환
중간 결과를 저장하는 컬렉션이 생기지 않기 때문에 원소가 많은 경우 성능이 눈에 띄게 좋아진다.
Sequence 인터페이스
이 인터페이스는 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현할 뿐이다. Sequence 안에는 iterator라는 단 하나의 메소드가 있다. 그 메소드를 통해 시퀀스로부터 원소 값을 얻을 수 있다.
시퀀스의 원소는 필요할 때 비로소 계산된다. 따라서 중간 처리 결과를 저장하지 않고도 연산을 연쇄적으로 적용해서 효율적으로 계산을 수행할 수 있다.
큰 컬렉션에 대해서 연산을 연쇄시킬 때는 시퀀스를 사용하는 것을 규칙으로 삼아야한다. 컬렉션에 들어있는 원소가 많으면 중간 원소를 재배열하는 비용이 커지기 때문에 지연 계산이 더 낫다.
시퀀스 연산 실행: 중간 연산과 최종 연산
시퀀스에 대한 연산은 중간(intermediate) 연산과 최종(terminal) 연산으로 나뉜다.
- 중간 연산(intermediate operation) - 다른 시퀀스를 반환
- 최종 연산(terminal operation) - 결과를 반환
listOf(1, 2, 3, 4).asSequence()
.map { print("map($it) "); it * it }
.filter{ print("filter($it) "); it % 2 ==0}
.toList()
println()
listOf(1, 2, 3, 4)
.map { print("map($it) "); it * it }
.filter{ print("filter($it) "); it % 2 ==0}
.toList()
// 시퀀스 호출 순서
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)
// collection stream 호출 순서
map(1) map(2) map(3) map(4) filter(1) filter(4) filter(9) filter(16)
최종 연산을 호출하면 연기됐던 모든 계산이 수행된다. (최종연산이 존재하지 않으면, 아무런 내용이 출력되지 않는다.) ⇒ toList가 존재하지 않으면 아무 내용도 출력되지 않음
시퀀스를 사용하는 경우 모든 연산은 각 원소에 대해 순차적으로 적용된다. 즉, 첫 번재 원소가 처리되고, 다시 두 번째 원소가 처리되며, 이런 처리가 모든 원소에 대해 적용된다.
println(listOf(1,2,3,4).asSequence().map( it * it ).find{ it > 3 })
Kotlin Sequence == Java Stream
무명 객체는 메서드를 호출할 때마다 새로운 객체가 생성되며, 람다는 (변수를 포획하지 않은 람다에 한해서) 메서드를 호출할 때마다 반복 사용된다.
// 반복 사용
postponeComputation(1000) { println(42) }
// 새로운 객체 생성
val runnable = Runnable { println(42) }
fun handleComputation() {
postponeComputation(1000, runnable)
}
'프로그래밍 노트 > Kotlin' 카테고리의 다른 글
[Kotlin] 널이 될 수 있는 타입, 안전한 호출 연산자(?.) (0) | 2020.12.15 |
---|---|
[Kotlin] 수신 객체 지정 람다: with와 apply (0) | 2020.12.14 |
[Kotlin] 코틀린 컬렉션 함수 API(filter, map ...) (0) | 2020.12.06 |
[Kotlin] 코틀린 람다 맛보기 (0) | 2020.12.06 |
[Kotlin] 코틀린 object/companion 클래스 (동반객체) (0) | 2020.11.30 |