MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin集合过滤与映射

2024-08-124.8k 阅读

Kotlin 集合过滤基础

在 Kotlin 中,集合过滤是一种非常重要的操作,它允许我们从一个集合中筛选出符合特定条件的元素。这在处理数据时非常有用,比如从一个包含所有用户的列表中筛选出活跃用户,或者从一个文件路径集合中筛选出特定类型的文件。

filter 函数

Kotlin 集合提供了 filter 函数,它接受一个谓词(一个返回布尔值的函数)作为参数,并返回一个新的集合,该集合包含原集合中所有使谓词返回 true 的元素。

以下是一个简单的示例,假设我们有一个整数列表,想要筛选出所有的偶数:

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)
    val evenNumbers = numbers.filter { it % 2 == 0 }
    println(evenNumbers)
}

在上述代码中,filter 函数遍历 numbers 列表中的每个元素,并将每个元素传递给 lambda 表达式 it % 2 == 0。如果 lambda 表达式返回 true,则该元素被包含在新的集合 evenNumbers 中。最终,evenNumbers 将会是 [2, 4, 6]

filterNot 函数

filter 相反,filterNot 函数返回一个新的集合,该集合包含原集合中所有使谓词返回 false 的元素。

继续以上面的整数列表为例,我们可以使用 filterNot 筛选出所有奇数:

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)
    val oddNumbers = numbers.filterNot { it % 2 == 0 }
    println(oddNumbers)
}

这里,filterNot 函数遍历 numbers 列表,对于每个元素,如果 it % 2 == 0 返回 false,即元素为奇数时,该元素被包含在 oddNumbers 集合中,所以 oddNumbers 将会是 [1, 3, 5]

复杂条件下的集合过滤

在实际应用中,我们的过滤条件可能不会像判断奇偶性这么简单,可能需要结合多个条件进行过滤。

多个条件组合

假设我们有一个用户类 User,包含 ageisActive 属性,我们想要筛选出年龄大于 18 且处于活跃状态的用户。

data class User(val name: String, val age: Int, val isActive: Boolean)

fun main() {
    val users = listOf(
        User("Alice", 20, true),
        User("Bob", 15, true),
        User("Charlie", 25, false),
        User("David", 30, true)
    )
    val filteredUsers = users.filter { it.age > 18 && it.isActive }
    filteredUsers.forEach { println(it.name) }
}

在上述代码中,filter 函数的 lambda 表达式 it.age > 18 && it.isActive 结合了两个条件。只有同时满足年龄大于 18 且 isActivetrue 的用户才会被包含在 filteredUsers 集合中。运行这段代码,将会输出 AliceDavid

条件函数封装

如果过滤条件比较复杂,我们可以将条件封装成一个单独的函数,这样可以提高代码的可读性和可维护性。

data class User(val name: String, val age: Int, val isActive: Boolean)

fun isEligibleUser(user: User): Boolean {
    return user.age > 18 && user.isActive
}

fun main() {
    val users = listOf(
        User("Alice", 20, true),
        User("Bob", 15, true),
        User("Charlie", 25, false),
        User("David", 30, true)
    )
    val filteredUsers = users.filter(::isEligibleUser)
    filteredUsers.forEach { println(it.name) }
}

这里,isEligibleUser 函数封装了过滤条件,filter 函数中通过双冒号语法 ::isEligibleUser 将该函数作为谓词传递。这样,如果过滤条件发生变化,我们只需要修改 isEligibleUser 函数即可,而不需要在 filter 调用处修改复杂的 lambda 表达式。

集合过滤与空安全

在 Kotlin 中,处理集合过滤时需要注意空安全问题,尤其是当集合可能为空或者谓词函数可能返回 null 时。

集合为空的情况

当使用 filter 对一个可能为空的集合进行操作时,Kotlin 会正确处理这种情况,返回一个空集合而不是抛出空指针异常。

fun main() {
    var numbers: List<Int>? = null
    val evenNumbers = numbers?.filter { it % 2 == 0 } ?: emptyList()
    println(evenNumbers)
}

在上述代码中,numbers 可能为 null。通过使用安全调用操作符 ?.,如果 numbers 不为 null,则执行 filter 操作;如果 numbersnull,则返回一个空列表 emptyList()。这样可以确保代码在集合为空时不会崩溃。

谓词函数返回 null 的情况

如果谓词函数可能返回 null,我们需要确保在 filter 中进行适当的处理,否则可能会抛出 NullPointerException

data class Product(val name: String, val price: Double?)

fun isExpensive(product: Product): Boolean? {
    return product.price?.let { it > 100.0 }
}

fun main() {
    val products = listOf(
        Product("Laptop", 1500.0),
        Product("Mouse", null),
        Product("Keyboard", 50.0)
    )
    val expensiveProducts = products.filterNotNull { isExpensive(it) }
    expensiveProducts.forEach { println(it.name) }
}

在这个例子中,isExpensive 函数可能返回 null(当 pricenull 时)。我们使用 filterNotNull 函数,它会过滤掉所有使谓词返回 null 的元素,从而避免了空指针异常。最终,expensiveProducts 集合将只包含价格大于 100 的产品,即 Laptop

Kotlin 集合映射基础

集合映射是将集合中的每个元素按照一定规则转换为另一个元素的操作。在 Kotlin 中,这通过 map 函数来实现。

map 函数

map 函数接受一个转换函数作为参数,并返回一个新的集合,新集合中的元素是原集合元素经过转换函数处理后的结果。

假设我们有一个整数列表,想要将每个元素平方,我们可以这样做:

fun main() {
    val numbers = listOf(1, 2, 3, 4)
    val squaredNumbers = numbers.map { it * it }
    println(squaredNumbers)
}

在上述代码中,map 函数遍历 numbers 列表中的每个元素,并将每个元素传递给 lambda 表达式 it * it,该表达式将元素平方。新的集合 squaredNumbers 包含原集合元素平方后的结果,即 [1, 4, 9, 16]

mapIndexed 函数

mapIndexed 函数与 map 类似,但它传递给转换函数的参数除了元素本身外,还包含元素的索引。

fun main() {
    val names = listOf("Alice", "Bob", "Charlie")
    val indexedNames = names.mapIndexed { index, name -> "$index: $name" }
    println(indexedNames)
}

这里,mapIndexed 函数的 lambda 表达式 { index, name -> "$index: $name" } 接收元素的索引 index 和元素 name,并将它们组合成一个新的字符串。最终,indexedNames 集合将是 ["0: Alice", "1: Bob", "2: Charlie"]

复杂类型的集合映射

当集合中的元素是复杂类型时,集合映射可以进行更复杂的转换操作。

数据类转换

假设我们有一个 Point 数据类,包含 xy 坐标,我们想要将一个 Point 列表转换为它们到原点的距离列表。

data class Point(val x: Double, val y: Double)

fun main() {
    val points = listOf(
        Point(3.0, 4.0),
        Point(0.0, 0.0),
        Point(1.0, 1.0)
    )
    val distances = points.map { Math.sqrt(it.x * it.x + it.y * it.y) }
    println(distances)
}

在上述代码中,map 函数的 lambda 表达式 Math.sqrt(it.x * it.x + it.y * it.y) 计算每个 Point 到原点的距离。新的集合 distances 包含每个点到原点的距离,即 [5.0, 0.0, 1.4142135623730951]

多层嵌套集合映射

如果集合中的元素又是一个集合,我们可能需要进行多层映射操作。

fun main() {
    val nestedLists = listOf(
        listOf(1, 2),
        listOf(3, 4),
        listOf(5, 6)
    )
    val flattenedAndSquared = nestedLists.flatMap { it.map { it * it } }
    println(flattenedAndSquared)
}

这里,首先使用 flatMap 函数,它先对每个内部列表应用 map 函数将元素平方,然后将所有结果扁平化为一个单一的列表。flattenedAndSquared 最终将是 [1, 4, 9, 16, 25, 36]

集合过滤与映射的结合使用

在实际编程中,我们经常需要先对集合进行过滤,然后再对过滤后的结果进行映射。

先过滤后映射

假设我们有一个包含商品价格的列表,我们只想对价格大于 100 的商品进行折扣计算。

fun main() {
    val prices = listOf(50.0, 150.0, 200.0, 80.0)
    val discountedPrices = prices.filter { it > 100 }.map { it * 0.9 }
    println(discountedPrices)
}

在上述代码中,首先使用 filter 函数筛选出价格大于 100 的商品,然后对这些商品的价格使用 map 函数应用 9 折折扣。最终,discountedPrices 集合将包含折扣后的价格 [135.0, 180.0]

先映射后过滤

有时先进行映射再过滤也很有用。比如我们有一个包含商品名称和价格的 Pair 列表,我们想要先将价格转换为折扣价格,然后筛选出折扣后价格小于 150 的商品。

fun main() {
    val products = listOf(
        "Laptop" to 1500.0,
        "Mouse" to 50.0,
        "Keyboard" to 200.0
    )
    val affordableDiscountedProducts = products.map { it.first to it.second * 0.9 }.filter { it.second < 150 }
    println(affordableDiscountedProducts)
}

这里,先使用 map 函数将每个 Pair 转换为商品名称和折扣后价格的 Pair,然后使用 filter 函数筛选出折扣后价格小于 150 的商品。最终,affordableDiscountedProducts 集合将包含符合条件的商品 ("Mouse", 45.0)

自定义集合过滤与映射操作

除了使用 Kotlin 提供的标准集合过滤和映射函数外,我们还可以自定义这些操作,以满足特定的需求。

自定义过滤函数

假设我们想要实现一个过滤函数,它不仅可以根据普通谓词过滤,还可以在过滤过程中记录一些信息。

fun <T> myFilter(collection: Collection<T>, predicate: (T) -> Boolean): Pair<List<T>, Int> {
    var count = 0
    val result = mutableListOf<T>()
    for (element in collection) {
        if (predicate(element)) {
            result.add(element)
            count++
        }
    }
    return Pair(result, count)
}

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)
    val (filteredNumbers, count) = myFilter(numbers) { it % 2 == 0 }
    println("Filtered numbers: $filteredNumbers")
    println("Count: $count")
}

在上述代码中,myFilter 函数接受一个集合和一个谓词,返回一个 Pair,其中第一个元素是过滤后的列表,第二个元素是过滤出的元素数量。运行代码,将会输出过滤后的偶数列表以及偶数的数量。

自定义映射函数

我们也可以自定义映射函数,实现一些特殊的转换逻辑。

fun <T, R> myMap(collection: Collection<T>, transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()
    for (element in collection) {
        val transformed = transform(element)
        result.add(transformed)
    }
    return result
}

fun main() {
    val numbers = listOf(1, 2, 3, 4)
    val squaredNumbers = myMap(numbers) { it * it }
    println(squaredNumbers)
}

这里,myMap 函数接受一个集合和一个转换函数,将集合中的每个元素进行转换并返回新的列表。运行代码,将会输出元素平方后的列表。

通过自定义集合过滤与映射操作,我们可以更好地控制集合处理的逻辑,满足复杂的业务需求。同时,理解这些自定义实现也有助于我们更深入地理解 Kotlin 标准库中集合操作函数的本质。

集合过滤与映射的性能考量

在使用集合过滤与映射操作时,性能是一个重要的考量因素。不同的操作方式和数据规模可能会对性能产生显著影响。

过滤与映射的顺序

在结合使用过滤和映射时,顺序可能会影响性能。一般来说,如果过滤操作可以显著减少元素数量,那么先过滤后映射会更高效。

fun main() {
    val largeList = (1..1000000).toList()

    // 先过滤后映射
    val startTime1 = System.currentTimeMillis()
    val result1 = largeList.filter { it % 2 == 0 }.map { it * it }
    val endTime1 = System.currentTimeMillis()
    println("Time taken for filter then map: ${endTime1 - startTime1} ms")

    // 先映射后过滤
    val startTime2 = System.currentTimeMillis()
    val result2 = largeList.map { it * it }.filter { it % 4 == 0 }
    val endTime2 = System.currentTimeMillis()
    println("Time taken for map then filter: ${endTime2 - startTime2} ms")
}

在上述代码中,我们创建了一个包含一百万元素的列表。先过滤后映射的方式,filter 操作先筛选出一半的偶数,然后 map 操作只对这些偶数进行平方运算。而先映射后过滤的方式,map 操作会对所有一百万元素进行平方运算,然后 filter 再从这一百万平方后的结果中筛选出符合条件的元素。运行代码可以看到,先过滤后映射通常会花费更少的时间。

避免不必要的中间集合

在链式调用集合操作时,尽量避免创建不必要的中间集合。例如,使用 flatMap 代替先 mapflatten 的操作。

fun main() {
    val nestedLists = listOf(
        listOf(1, 2),
        listOf(3, 4),
        listOf(5, 6)
    )

    // 先 map 再 flatten
    val startTime1 = System.currentTimeMillis()
    val result1 = nestedLists.map { it.map { it * it } }.flatten()
    val endTime1 = System.currentTimeMillis()
    println("Time taken for map then flatten: ${endTime1 - startTime1} ms")

    // 使用 flatMap
    val startTime2 = System.currentTimeMillis()
    val result2 = nestedLists.flatMap { it.map { it * it } }
    val endTime2 = System.currentTimeMillis()
    println("Time taken for flatMap: ${endTime2 - startTime2} ms")
}

这里,先 mapflatten 的操作会创建一个中间的嵌套集合,而 flatMap 直接将结果扁平化为一个单一集合,避免了中间集合的创建,通常会有更好的性能表现。

大数据集的处理

当处理大数据集时,内存管理也变得至关重要。Kotlin 的集合操作通常会返回新的集合,这可能会导致大量的内存开销。在这种情况下,可以考虑使用迭代器或者流(在 Java 兼容的情况下)来处理数据,以减少内存占用。

fun main() {
    val largeList = (1..1000000).toList()

    // 使用迭代器
    val startTime1 = System.currentTimeMillis()
    val result1 = mutableListOf<Int>()
    val iterator = largeList.iterator()
    while (iterator.hasNext()) {
        val num = iterator.next()
        if (num % 2 == 0) {
            result1.add(num * num)
        }
    }
    val endTime1 = System.currentTimeMillis()
    println("Time taken using iterator: ${endTime1 - startTime1} ms")

    // 使用标准集合操作
    val startTime2 = System.currentTimeMillis()
    val result2 = largeList.filter { it % 2 == 0 }.map { it * it }
    val endTime2 = System.currentTimeMillis()
    println("Time taken using standard operations: ${endTime2 - startTime2} ms")
}

在上述代码中,使用迭代器手动处理数据,可以减少内存的占用,尤其是在大数据集的情况下。虽然代码看起来更繁琐,但在性能和内存管理方面可能更具优势。

集合过滤与映射在实际项目中的应用场景

集合过滤与映射在各种实际项目中都有广泛的应用,下面我们来看一些常见的场景。

数据清洗与预处理

在数据处理项目中,原始数据可能包含一些无效或不需要的记录。通过集合过滤,可以去除这些无效数据,然后使用映射对数据进行标准化或转换。

例如,我们从文件中读取了一些用户注册信息,可能包含一些年龄为负数或者空名字的无效记录。

data class UserRegistration(val name: String, val age: Int)

fun main() {
    val rawRegistrations = listOf(
        UserRegistration("", 20),
        UserRegistration("Alice", 25),
        UserRegistration("Bob", -5),
        UserRegistration("Charlie", 30)
    )
    val validRegistrations = rawRegistrations.filter { it.name.isNotEmpty() && it.age > 0 }.map {
        UserRegistration(it.name.capitalize(), it.age)
    }
    validRegistrations.forEach { println(it) }
}

在上述代码中,首先使用 filter 去除名字为空或年龄为负数的无效记录,然后使用 map 将名字首字母大写,得到清洗和预处理后的用户注册信息。

业务逻辑处理

在业务系统中,集合过滤与映射常用于实现业务规则。比如在一个电商系统中,计算订单的总金额,可能需要先过滤出有效的商品项,然后映射计算每个商品项的总价并求和。

data class OrderItem(val product: String, val price: Double, val quantity: Int)

fun main() {
    val orderItems = listOf(
        OrderItem("Laptop", 1500.0, 1),
        OrderItem("Mouse", 50.0, 2),
        OrderItem("Keyboard", 0.0, 1), // 无效商品项
        OrderItem("Monitor", 300.0, 1)
    )
    val totalAmount = orderItems.filter { it.price > 0 }.map { it.price * it.quantity }.sum()
    println("Total amount: $totalAmount")
}

这里,先使用 filter 排除价格为 0 的无效商品项,然后使用 map 计算每个有效商品项的总价,最后使用 sum 函数计算订单的总金额。

数据展示与可视化

在数据展示和可视化项目中,我们可能需要从原始数据集合中提取特定的信息并进行转换,以适应前端展示的需求。

假设我们有一个包含城市天气信息的集合,我们想要提取每个城市的名称和温度,并将温度从摄氏度转换为华氏度,以便在前端展示。

data class WeatherInfo(val city: String, val temperatureCelsius: Double)

fun celsiusToFahrenheit(celsius: Double): Double {
    return celsius * 1.8 + 32
}

fun main() {
    val weatherData = listOf(
        WeatherInfo("Beijing", 25.0),
        WeatherInfo("Shanghai", 28.0),
        WeatherInfo("Guangzhou", 30.0)
    )
    val displayData = weatherData.map { WeatherInfo(it.city, celsiusToFahrenheit(it.temperatureCelsius)) }
    displayData.forEach { println("${it.city}: ${it.temperatureCelsius}°F") }
}

在上述代码中,使用 map 函数将每个 WeatherInfo 对象中的温度从摄氏度转换为华氏度,得到适合前端展示的数据格式。

通过这些实际应用场景可以看出,集合过滤与映射在各种项目中都是非常实用的操作,能够有效地处理和转换数据,满足不同的业务需求。

总结

Kotlin 的集合过滤与映射操作是其强大功能的重要组成部分。通过 filterfilterNot 等函数,我们可以轻松地从集合中筛选出符合条件的元素,而 mapmapIndexed 等函数则允许我们对集合中的元素进行转换。在实际应用中,我们可以结合这些操作,先过滤后映射或者先映射后过滤,以满足复杂的数据处理需求。

同时,我们还了解了如何在空安全、性能、自定义操作以及实际项目场景中应用集合过滤与映射。空安全方面,要注意集合可能为空以及谓词函数可能返回 null 的情况;性能方面,合理安排过滤与映射的顺序、避免不必要的中间集合以及针对大数据集选择合适的处理方式都很重要;自定义操作则让我们可以根据特定需求扩展集合操作的功能;而在实际项目中,集合过滤与映射广泛应用于数据清洗、业务逻辑处理和数据展示等多个方面。

掌握 Kotlin 的集合过滤与映射,对于编写高效、简洁且功能强大的 Kotlin 代码至关重要。无论是小型项目还是大型企业级应用,这些操作都能帮助我们更好地处理和管理数据。希望通过本文的介绍,读者能够对 Kotlin 的集合过滤与映射有更深入的理解,并在实际编程中灵活运用。