BottleH Blog

Kotlin in Action - 6장 컬렉션과 시퀀스

    Tags

  • Kotlin
Kotlin in Action - 6장 컬렉션과 시퀀스 thumbnail

📖 6.1 컬렉션에 대한 함수형 API

🔖 6.1.1 원소 제거와 변환: filter와 map

data class Person(val name: String, val age: Int) fun main() { val list = listOf(1, 2, 3, 4) println(list.filter { it % 2 == 0 }) }
  • filter 함수는 컬렉션을 순회하면서 주어진 람다가 true를 반환하는 원소들만 모은다.
  • filter 함수는 주어진 술어와 일치하는 원소들로 이뤄진 새 컬렉션을 만들 수 있지만 그 과정에서 원소를 변환하지는 않는다.
fun main() { val list = listOf(1, 2, 3, 4) println(list.map { it * it }) }
  • map은 입력 컬렉션의 원소를 변환할 수 있게 해준다.
fun main() { val numbers = listOf(1, 2, 3, 4, 5, 6, 7) val filtered = numbers.filterIndexed { index, element -> index % 2 == 0 && element > 3 } println(filtered) val mapped = numbers.mapIndexed { index, element -> index + element } println(mapped) }
  • index와 원소를 함께 제공

🔖 6.1.2 컬렉션 값 누적: reduce와 fold

fun main() { val list = listOf(1, 2, 3, 4, 5, 6, 7) println(list.reduce { acc, element -> acc + element }) }
  • reduce를 사용하면 컬렉션의 첫 번째 값을 누적기에 넣는다.
    • 빈 컬렉션에 호출하면 안 된다.
fun main() { val people = listOf( Person("Alice", 29), Person("Bob", 31), ) val folded = people.fold("") { acc, person -> acc + person.name } println(folded) }
  • fold 함수는 reduce와 비슷하지만 첫 번째 원소를 누적 값으로 시작하는 대신, 임의의 시작 값을 선택할 수 있다.
  • runningReduce, runningFold를 사용하면 중간 단계의 모든 누적 값을 뽑아낼 수 있다.
    • 반환객체는 리스트다.

🔖 6.1.3 컬렉션에 술어 적용: all, any, none, count, find

val canBeInClub27 = { p: Person -> p.age <= 27 } fun main() { val people = listOf( Person("Alice", 27), Person("Bob", 31), ) println(people.all(canBeInClub27)) }
  • all은 모든 원소가 술어를 만족시키는지 판단한다.
  • any는 술어를 만족하는 원소가 하나라도 있는지 판단한다.
  • none은 술어를 만족하는 원소가 없는지를 판단한다.
val canBeInClub27 = { p: Person -> p.age <= 27 } fun main() { val people = listOf( Person("Alice", 27), Person("Bob", 31), ) println(people.count(canBeInClub27)) }
  • 술어를 만족하는 원소의 개수를 알고 싶다면 count를 사용한다.
  • size보다는 count가 효율적이다.
    • count는 개수만 추적할 뿐 객체 생성하지 않음
val canBeInClub27 = { p: Person -> p.age <= 27 } fun main() { val people = listOf( Person("Alice", 27), Person("Bob", 31), ) println(people.find(canBeInClub27)) }
  • 술어를 만족하는 원소를 하나 찾고 싶으면 find 함수 사용
  • 원소가 전혀 없는 경우 null 반환

🔖 6.1.4 리스트를 분할해 리스트의 쌍으로 만들기: partition

fun main() { val people = listOf( Person("Alice", 26), Person("Bob", 29), Person("Carol", 31), ) val comeIn = people.filter(canBeInClub27) val stayOut = people.filterNot(canBeInClub27) println(comeIn) println(stayOut) }
  • 위 로직을 더 간결하게 처리할 수 있다.
val (comeIn, stayOut) = people.partition(canBeInClub27) println(comeIn) println(stayOut)
  • partition 함수는 컬렉션을 술어를 만족하는 그룹과 만족하지 않는 그룹으로 나눈다.

🔖 6.1.5 리스트를 여러 그룹으로 이뤄진 맵으로 바꾸기: groupBy

fun main() { val people = listOf( Person("Alice", 31), Person("Bob", 29), Person("Carol", 31), ) println(people.groupBy { it.age }) }
  • 원소를 구분하는 특성이 키이고, 키 값에 따른 그룹을 생성한다.

🔖 6.1.6 컬렉션을 맵으로 변환: associate, associateWith, associateBy

fun main() { val people = listOf( Person("Joe", 22), Person("Mary", 31), ) val nameToAge = people.associate { it.name to it.age } println(nameToAge) }
  • 원소를 그룹화하지 않으면서 컬렉션으로부터 맵을 만들어내고 싶다면 associate 함수를 사용
fun main() { val people = listOf( Person("Joe", 22), Person("Mary", 31), Person("Jamie", 22) ) val personToAge = people.associateWith { it.age } println(personToAge) val ageToPerson = people.associateBy { it.age } println(ageToPerson) }
  • associateWith는 컬렉션의 원래 원소를 키로 사용
  • associateBy는 컬렉션의 원래 원소를 맵의 값으로 하고, 람다가 만들어내는 값을 맵의 키로 사용

🔖 6.1.7 가변 컬렉션의 원소 변경: replaceAll, fill

fun main() { val names = mutableListOf("Martin", "Samuel") println(names) names.replaceAll { it.uppercase() } println(names) names.fill("(redacted)") println(names) }
  • replaceAll함수를 가변 컬렉션에 적용하면 지정한 람다로 모든 원소를 변경한다.
  • 가변 리스트의 모든 원소를 똑같은 값으로 바꾸는 경우에는 fill함수를 쓸 수 있다.

🔖 6.1.8 컬렉션의 특별한 경우 처리: ifEmpty

fun main() { val empty = emptyList<String>() val full = listOf("apple", "orange", "banana") println(empty.ifEmpty { listOf("no", "values", "here") }) println(full.ifEmpty { listOf("no", "values", "here") }) }
  • ifEmpty 함수를 사용화면 아무 원소도 없을 때 기본값을 생성하는 람다를 제공할 수 있다.
  • ifBlank 함수는 공백만 들어있는 문자열을 다룰 수 있다.

🔖 6.1.9 컬렉션 나누기: chunked와 windowed

val temperatures = listOf(27.7, 29.8, 22.0, 35.5, 19.1) fun main() { println(temperatures.windowed(3)) //[[27.7, 29.8, 22.0], [29.8, 22.0, 35.5], [22.0, 35.5, 19.1]] println(temperatures.windowed(3) { it.sum() / it.size }) //[26.5, 29.099999999999998, 25.53333333333333] }
  • 슬라이딩 윈도우를 생성하고자 windowed 함수를 사용할 수 있다.
fun main() { println(temperatures.chunked(2)) // [[27.7, 29.8], [22.0, 35.5], [19.1]] println(temperatures.chunked(2) { it.sum() }) // [57.5, 57.5, 19.1] }
  • 컬렉션을 어떤 주어진 크기의 서로 겹치지 않는(서로소) 부분으로 나누고 싶을 때는 chunked 함수를 사용

🔖 6.1.10 컬렉션 합치기: zip

fun main() { val names = listOf("Joe", "Mary", "Jamie") val ages = listOf(22, 31, 22, 44, 0) println(names.zip(ages)) // [(Joe, 22), (Mary, 31), (Jamie, 22)] println(names.zip(ages) { name, age -> Person(name, age) }) // [Person(name=Joe, age=22), Person(name=Mary, age=31), Person(name=Jamie, age=22)] }
  • zip 함수를 사용해 두 컬렉션에서 같은 인덱스에 있는 원소들의 쌍으로 이뤄진 리스트를 만들 수 있다.
  • 결과 컬렉션의 길이는 더 짧은 쪽의 길이와 같다.
fun main() { val countries = listOf("DE", "NL", "US") println(names zip ages zip countries) // [((Joe, 22), DE), ((Mary, 31), NL), ((Jamie, 22), US)] }
  • 중위 표기법을 쓸 수 있다.
  • 연쇄 호출이 가능하고, 리스트의 리스트가 되지는 않는다.

🔖 6.1.11 내포된 컬렉션의 원소 처리: flatMap과 flatten

class Book(val title: String, val authors: List<String>) val library = listOf( Book("Kotlin in Action", listOf("Isakova", "Elizarov", "Aigner", "Jemerov")), Book("Atomic Kotlin", listOf("Eckel", "Isakova")), Book("The Three-Body Problem", listOf("Liu")) ) fun main() { val authors = library.map { it.authors } println(authors) // [[Isakova, Elizarov, Aigner, Jemerov], [Eckel, Isakova], [Liu]] }
  • 내포된 컬렉션으로 반환된다.
fun main() { val authors = library.flatMap { it.authors } println(authors) // [Isakova, Elizarov, Aigner, Jemerov, Eckel, Isakova, Liu] println(authors.toSet()) // [Isakova, Elizarov, Aigner, Jemerov, Eckel, Liu] }
  • flatMap 함수는 컬렉션의 각 원소를 파라미터로 주어진 함수를 사용해 변환한다.
  • 변환한 결과를 하나의 리스트로 합친다.
  • 변환할 것이 없고 내포된 컬렉션을 평평한 컬렉션으로 만들고 싶다면 flatten 함수를 사용

📖 6.2 지연 계산 컬렉션 연산: 시퀀스

people.map(Person::name).filter { it.startsWith("A") }
  • 위 함수는 리스트를 결국 2개를 만든다.
  • 원소가 수백만 개가 되면 효율이 떨어진다.
people .asSequence() .map(Person::name) .filter { it.startsWith("A") } .toList()
  • 중간 결과를 저장하는 컬렉션이 생기지 않기 때문에 원소가 많은 경우 성능이 눈에 띄게 좋아진다.
  • 시퀀스의 원소는 필요할 때 lazy 계산이 된다.

🔖 6.2.1 시퀀스 연산 실행: 중간 연산과 최종 연산

  • 중간 연산은 다른 시퀀스를 반환하고, 지연 계산된다.
  • 최종 연산은 결과를 반환한다.
fun main() { listOf(1, 2, 3, 4) .asSequence() .map { print("map($it) ") it * it } .filter { print("filter($it) ") it % 2 == 0 } }
  • 이 코드는 아무 내용도 출력되지 않는다.
fun main() { listOf(1, 2, 3, 4) .asSequence() .map { print("map($it) ") it * it } .filter { print("filter($it) ") it % 2 == 0 }.toList() }
  • 최종 연산을 호출하면 연기됐던 모든 계산이 수행된다.
fun main() { println( listOf(1, 2, 3, 4) .asSequence() .map { it * it } .find { it > 3 } ) // 4 }
  • 시퀀스에서 연산순서는 각 원소에 대해 순차적으로 적용된다.
    • 원소에 연산을 차례대로 적용하다가 결과가 얻어지면 그 이후의 원소에 대해서는 변환이 이뤄지지 않을 수도 있다는 것
  • 즉시 계산은 전체 컬렉션에 연산을 적용시키지만 지연 계산은 원소를 한번에 하나씩 처리한다.
fun main() { val people = listOf( Person("Alice", 29), Person("Bob", 31), Person("Charles", 31), Person("Dan", 21) ) println( people .asSequence() .map(Person::name) .filter { it.length < 4 } .toList() ) // [Bob, Dan] println( people .asSequence() .filter { it.name.length < 4 } .map(Person::name) .toList()) // [Bob, Dan] }
  • 연쇄적인 연산에서 더 빨리 원소들을 제거하면 할수록 코드의 성능이 좋아진다.

🔖 6.2.2 시퀀스 만들기

fun main() { val naturalNumbers = generateSequence(0) { it + 1 } val numbersTo100 = naturalNumbers.takeWhile { it <= 100 } println(numbersTo100.sum()) // 5050 }
  • generateSequence 함수는 이전의 원소를 인자로 받아 다음 원소를 계산한다.
  • 위 변수는 모두 지연 계산된다.
import java.io.File fun File.isInsideHiddenDirectory() = generateSequence(this) { it.parentFile }.any { it.isHidden } val file = File("/Users/svtk/.HiddenDir/a.txt") println(file.isInsideHiddenDirectory()) // true
  • 객체의 조상들로 이뤄진 시퀀스를 만드는 것도 시퀀스를 사용하는 방법이다.
Written by@BottleH
Back-End Developer

GitHub