map, filter와 같은 함수가 반환하는 컬렉션은 즉시 생성된다. 이는 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬랙션에 임시로 담는다는 의미인데, 시퀀스를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.
fun main(args: Array<String>) {
val people = listOf(
Person("Alice", 31),
Person("Bob", 29),
Person("Carol", 31))
println(people.map(Person::name).filter { it.startsWith("A") })
}
- filter, map 이 리스트를 반환하므로, 연쇄 호출은 리스트 2개를 만든다는 의미이다.
- 원본 리스트에 원소가 많이 생길수록 효율이 떨어진다.
- 더 효율적으로 사용하기 위해서는 각 연산이 컬렉션을 직접 사용하는 대신 시퀀스를 사용하게 만들어야 한다.
fun main(args: Array<String>) {
val people = listOf(
Person("Alice", 31),
Person("Bob", 29),
Person("Carol", 31))
println(people.asSequence() //원본 컬렉션을 시퀀스로 변환
.map(Person::name) // 시퀀스도 컬렉션과 똑같은 API를 제공
.filter{it.startsWith("A")}
.toList())// 결과를 다시 리스트로 반환
}
- 전체 연산은 이전 결과와 같다. 하지만 방금 살펴본 예제에서 중간 결과를 저장하는 컬렉션이 생기지 않기 때문에 원소가 많을수록 성능이 월등하게 좋아진다.
- 코틀린의 지연 계산 시퀀스는 Sequence 인터페이스에서 시작한다.
- Sequence 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현할 뿐이다
- Sequence안에는 iterator라는 단 하나의 메서드만 존재
- iterator 메서드를 통해 시퀀스로부터 원소 값 추출
- Sequence 인터페이스의 강점은 그 인터페이스 위에 구현된 연산이 게산을 수행하는 방법 때문에 생긴다
- 시퀀스의 원소는 필요할 때 비로소 계산
- 중간 결과를 저장하지 않고도 연산을 연쇄적으로 적용해 효과적으로 계산을 수행할 수 있다.
- asSequence 확장함수를 호출하면 어떤 컬렉션이든 시퀀스로 변환할 수 있다.
- Sequence를 리스트로 바꿀때는 toList 사용
- 시퀀스를 반환해도 되지만, 원소를 순차적으로 접근하는 것이 아닌 해당 인덱스를 통해 바로 접근할 때는 Sequence는 iterator를 돌며 접근해야 되기 때문에 List로 변환해서 인덱스로 접근하는 것이 좋다
- 시퀀스에 대한 연산을 지연 계산하기 때문에 정말 계산을 실행하게 만들려면 최종 시퀀스의 원소를 하나씩 이터레이션 하거나 최종 시퀀스를 리스트로 변환해야 한다.
시퀀스 연산 실행(중간 연산과 최종 연산)
시퀀스에 대한 연산은 중간 연산과 최종연산으로 나뉜다.
중간 연산은 다른 시퀀스를 반환한다 그 시퀀스는 최초 시퀀스의 원소를 변환하는 방법을 알고 있고. 최종 연산은 결과를 반환한다.
결과는 최초 컬렉션에 대해 변환을 적용한 시퀀스로부터 일련의 계산을 수행해 얻을 수 있는 컬렉션이나 원소, 숫자, 객체이다.
// 중간 연산 //최종 연산
sequence.map{ ... } .filter { ... } .toList()
fun main(args: Array<String>) {
//최종연산이 없는 코드 -> 아무내용도 출력되지 않는다.
listOf(1, 2, 3, 4).asSequence()
.map{ print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2== 0 }
listOf(1, 2, 3, 4).asSequence()
.map{ print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2== 0 }
.toList() //최종연산이 적용된 코드
}
- 최종 연산이 없는 코드는 아무 내용도 출력하지 않는다
- map, filter 변환이 늦춰져서 결과를 얻을 필요가 있을 때 적용되는 것을 알 수 있다.
- 최종 연산을 호출하면 연기되었던 모든 계산이 수행된다.
- 직접 연산을 구현한다면 map함수를 각 원소에 대해 실행해서 새 시퀀스를 얻고 다시 filter를 수행할 것이지만 시퀀스에 대한 map과 filter 같은 경우 모든 연산은 각 원소에 대해 순차적으로 적용된다.
- 첫 번째 원소가 처리되고 다시 두번째 원소가 처리되며 이런 처리가 모든 원소에 적용된다.
- 따라서 첫번째 원소에 연산을 적용하다가 결과가 얻어지면 그 이후 연산은 적용을 안 할 수도 있다
fun main(args: Array<String>) {
println(listOf(1, 2, 3, 4).asSequence().map { it * it }.find { it > 3 })
}

- 같은 연산을 시퀀스가 아닌 컬렉션에 수행하면 map의 결과가 먼저 평가돼 최초 컬렉션의 모든 원소가 반환하게 되고, 두 번째 단계에서 map을 적용해서 얻은 중간 컬렉션으로부터 술어를 만족하는 원소를 찾는다
- 시퀀스를 사용하면 지연 계산으로 인해 원소 중 일부의 계산은 이뤄지지 않는다.
- 최초 시퀀스로 부터 수를 하나 가져와서 map에 지정된 변환을 수행한 다음에 find에 지정된 술어를 만족하는지 검사한다.
- 최초 시퀀스에서 2를 가져오면 제곱 값이 3보다 커지기 때문에 그 제곱 값을 결과로 반환하고, 결과를 이미 찾았으므로 뒤에 연산은 생략된다.
컬렉션에 대해 수행하는 연산의 순서도 성능에 영향을 끼친다.
fun main(args: Array<String>) {
val people = listOf(
Person("Alice", 31),
Person("Bob", 29),
Person("Carol", 31),
Person("Dan", 21)
)
println(people.asSequence().map(Person::name).filter { it.length < 4 }.toList())
println(people.asSequence().filter { it.name.length < 4 }.map(Person::name).toList())
}

- map을 먼저 하면 모든 원소를 변환한다.
- filter를 먼저하면 부적절한 원소를 먼저 제외하기 때문에 그런 원소는 변환되지 않는다
TIP 자바 스트림 VS 시퀀스
시퀀스와 자바 스트림과 같다. 그럼에도 코틀린에서 같은 개념을 따로 구현해 제공하는 이유는 안드로이드 등에서 예전 자바를 사용하는 경우 자바 8 스트림이 없기 때문이다. 자바 8을 채택하면 현재 코틀린 컬렉션과 시퀀스에서 제공하지 않는 중요한 기능을 사용할 수 있다(스트림 연산을 여러 CPU에서 병렬적으로 실행하는 기능). 필요와 사용할 자바의 버전에 따라 시퀀스와 스트림 중 적절한 것을 선택하자
시퀀스 만들기
시퀀스를 만드는 다른 방법으로 generateSequence 함수를 사용할 수 있다.(이전의 원소를 인자로 받아 다음 원소를 계산)
fun main(args: Array<String>) {
val naturalNumbers = generateSequence(0){ it + 1}
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
println(numbersTo100.sum())
}
- naturalNumbers, numbersTo100 모두 시퀀스며 연산을 지연한다.
- 최종 연산이 수행되기 전까지는 시퀀스의 각 숫자는 계산되지 않는다 (여기서는 sum)
- 시퀀스를 사용하는 일반적인 상황은 객체의 조상으로 이뤄진 시퀀스를 만들어 내는 것이다
fun main(args: Array<String>) {
val file = File("/Users/svtk/.HiddenDir/a.txt")
println(file.isInsideHiddenDirectory())
}
fun File.isInsideHiddenDirectory() =
generateSequence(this) { it.parentFile }.any { it.isHidden}
- 첫 번째 원소를 지정하고, 시퀀스의 한 원소로부터 다음 원소를 계산하는 방법을 제공함으로써 시퀀스를 만든다
- any를 find로 바꾸면 원하는 디렉터리도 찾을 수 있다.
- 이렇게 시퀀스를 사용하면 조건을 만족하는 디렉터리를 찾은 뒤 상위 디렉터리를 뒤지지 않는다
코틀린 인 액션 - 5장 람다로 프로그래밍
람다 식은 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.코틀린 표준 라이브러리는 람다를 아주 많이 사용한다.수신 객체 지정 람다(lambda with receiver)에 대해 살펴본다.일련의 동작을 변수
velog.io
'KotlinInAction > 람다로 프로그래밍' 카테고리의 다른 글
| 수신 객체 지정 람다 (with와 apply) (0) | 2022.08.03 |
|---|---|
| 자바 함수형 인터페이스 활용 (0) | 2022.08.03 |
| 컬렉션 함수형 API (0) | 2022.08.01 |
| 람다 식과 멤버 참조 (0) | 2022.07.31 |