컬렉션 필터링

컬렉션 필터링

컬렉션에서 조건에 맞는 요소만 골라내는 작업은 정말 자주 마주치게 돼요. Kotlin 표준 라이브러리는 이런 필터링을 한 번의 호출로 끝내는 확장 함수들을 제공하는데요, 원본 컬렉션은 건드리지 않으면서 결과를 어떻게 다룰지까지 함께 살펴볼게요.

출처: Kotlin 공식 문서

본문

필터링은 컬렉션 처리에서 가장 인기 있는 작업 중 하나예요. Kotlin에서 필터링 조건은 predicate로 정의되는데, 이는 컬렉션 요소를 받아 불리언 값을 반환하는 람다 함수예요. true는 주어진 요소가 predicate와 일치한다는 뜻이고, false는 그 반대예요.

표준 라이브러리에는 컬렉션을 한 번의 호출로 필터링하게 해주는 확장 함수 그룹이 있어요. 이 함수들은 원본 컬렉션을 변경하지 않으므로, mutable 컬렉션과 읽기 전용 컬렉션 모두에서 사용할 수 있어요. 필터링 결과를 사용하려면 그 결과를 변수에 할당하거나 필터링 뒤의 함수 체인에 연결해야 해요.

predicate로 필터링

기본 필터링 함수는 filter()예요. predicate와 함께 호출하면 일치하는 컬렉션 요소를 반환해요. ListSet 모두 결과 컬렉션은 List이고, Map은 결과도 Map이에요.

fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")
    val longerThan3 = numbers.filter { it.length > 3 }
    println(longerThan3)

    val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
    val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10}
    println(filteredMap)
//sampleEnd
}

filter()의 predicate는 요소의 값만 확인할 수 있어요. 요소의 위치를 필터에 사용하고 싶다면 filterIndexed()를 사용해요. 인덱스와 값, 두 인자를 받는 predicate를 취하죠.

부정 조건으로 컬렉션을 필터링하려면 filterNot()을 사용해요. predicate가 false를 반환하는 요소들의 리스트를 돌려주죠.

fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")

    val filteredIdx = numbers.filterIndexed { index, s -> (index != 0) && (s.length < 5)  }
    val filteredNot = numbers.filterNot { it.length <= 3 }

    println(filteredIdx)
    println(filteredNot)
//sampleEnd
}

주어진 타입의 요소를 필터링해서 요소 타입을 좁히는 함수도 있어요.

  • filterIsInstance()는 주어진 타입의 컬렉션 요소를 반환해요. List<Any>에 대해 호출하면 filterIsInstance<T>()List<T>를 반환하므로, 항목에 T 타입의 함수를 호출할 수 있게 돼요.
fun main() {
//sampleStart
    val numbers = listOf(null, 1, "two", 3.0, "four")
    println("All String elements in upper case:")
    numbers.filterIsInstance<String>().forEach {
        println(it.uppercase())
    }
//sampleEnd
}
  • filterNotNull()은 모든 nullable 요소를 반환해요. List<T?>에 대해 호출하면 filterNotNull()List<T: Any>를 반환하므로 요소를 non-nullable 객체로 취급할 수 있게 돼요.
fun main() {
//sampleStart
    val numbers = listOf(null, "one", "two", null)
    numbers.filterNotNull().forEach {
        println(it.length)   // nullable String에는 length를 사용할 수 없어요
    }
//sampleEnd
}

파티션 나누기

또 다른 필터링 함수인 partition()은 predicate로 컬렉션을 필터링하면서 일치하지 않는 요소는 별도의 리스트에 보관해요. 그래서 반환 값은 List들의 Pair예요. 첫 번째 리스트에는 predicate와 일치하는 요소가, 두 번째 리스트에는 원본 컬렉션의 나머지가 들어 있죠.

fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")
    val (match, rest) = numbers.partition { it.length > 3 }

    println(match)
    println(rest)
//sampleEnd
}

predicate 테스트

마지막으로, 컬렉션 요소에 대해 predicate를 단순히 테스트하는 함수들이 있어요.

  • any()는 최소한 하나의 요소가 주어진 predicate와 일치하면 true를 반환해요.
  • none()은 요소 중 어느 것도 주어진 predicate와 일치하지 않으면 true를 반환해요.
  • all()은 모든 요소가 주어진 predicate와 일치하면 true를 반환해요. all()은 빈 컬렉션에서 유효한 predicate로 호출하면 true를 반환한다는 점에 주의하세요. 이런 동작은 논리학에서 공허한 참(vacuous truth)으로 알려져 있어요.
fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")

    println(numbers.any { it.endsWith("e") })
    println(numbers.none { it.endsWith("a") })
    println(numbers.all { it.endsWith("e") })

    println(emptyList<Int>().all { it > 5 })   // vacuous truth
//sampleEnd
}

any()none()은 predicate 없이도 사용할 수 있어요. 이 경우 컬렉션이 비어 있는지만 확인하죠. any()는 요소가 있으면 true, 없으면 false를 반환하고, none()은 그 반대예요.

fun main() {
//sampleStart
    val numbers = listOf("one", "two", "three", "four")
    val empty = emptyList<String>()

    println(numbers.any())
    println(empty.any())

    println(numbers.none())
    println(empty.none())
//sampleEnd
}

더 알아보기