第六章:集合与泛型——Kotlin 数据结构的核心

说实话,Kotlin 的集合和泛型,是我当年从 Java 转过来时最惊喜的部分。Java 里写个集合过滤,动不动就是匿名内部类、for 循环,代码又臭又长。Kotlin 呢?一行 lambda 搞定。但别高兴太早——泛型的协变和逆变,才是真正考验功底的地方。我见过不少项目,因为泛型用错,运行时直接 ClassCastException,排查起来让人头大。

这一章,咱们就把 List、Set、Map 的创建与操作讲透,再深入泛型类、泛型函数,最后攻克协变与逆变。嗯,内容不少,但都是日常开发高频使用的。

6.1 List、Set、Map 的创建与操作

Kotlin 的集合分两类:可变集合(MutableList、MutableSet、MutableMap)和不可变集合(List、Set、Map)。我个人习惯是:能不可变就不可变,除非明确需要增删改。为啥?不可变集合线程安全,也更容易推理代码逻辑。

6.1.1 List 的创建

// 不可变 List
val list: List<String> = listOf("Apple", "Banana", "Cherry")

// 可变 List
val mutableList: MutableList<String> = mutableListOf("Apple", "Banana")
mutableList.add("Cherry")

我在项目中遇到过一个问题:用 listOf() 创建空列表时,如果不指定泛型,Kotlin 会推断为 Nothing,导致后续无法添加元素。所以空列表一定要写泛型:listOf<String>()

6.1.2 Set 的创建

Set 的特点是元素唯一,无序。Kotlin 默认使用 LinkedHashSet,保留插入顺序。

val set: Set<Int> = setOf(1, 2, 3, 3, 2)  // 实际为 [1, 2, 3]
val mutableSet: MutableSet<Int> = mutableSetOf(1, 2, 3)

你想想看,如果只需要判断元素是否存在,Set 比 List 快得多——底层是哈希表,O(1) 复杂度。

6.1.3 Map 的创建

val map: Map<String, Int> = mapOf("Alice" to 25, "Bob" to 30)
val mutableMap: MutableMap<String, Int> = mutableMapOf()
mutableMap["Charlie"] = 35

注意 to 是中缀表达式,本质是创建 Pair。我早期写 Kotlin 时,总写成 mapOf("Alice", 25),结果编译报错——嗯,这个坑踩过一次就不会忘了。

6.2 集合的遍历与过滤

Kotlin 的集合操作符,说白了就是函数式编程的精华。你不需要写 for 循环,一行 lambda 就能搞定过滤、映射、分组。

6.2.1 遍历

val fruits = listOf("Apple", "Banana", "Cherry")

// for 循环
for (fruit in fruits) {
    println(fruit)
}

// forEach 高阶函数
fruits.forEach { println(it) }

// 带索引遍历
fruits.forEachIndexed { index, fruit ->
    println("$index: $fruit")
}

我个人更推荐 forEach,代码更简洁。但注意:forEach 里不能直接使用 breakcontinue,如果需要提前退出,还是用 for 循环。

6.2.2 过滤与映射

val numbers = listOf(1, 2, 3, 4, 5, 6)

// 过滤出偶数
val evens = numbers.filter { it % 2 == 0 }  // [2, 4, 6]

// 映射为字符串
val strings = numbers.map { "Number: $it" }

// 先过滤再映射
val result = numbers
    .filter { it > 3 }
    .map { it * 10 }  // [40, 50, 60]
提示:链式调用时,注意操作顺序。先 filtermap 效率更高,因为减少了后续操作的元素数量。我在优化一个列表加载性能时,就是靠调整操作顺序,把耗时从 200ms 降到了 80ms。

6.2.3 分组与聚合

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

val people = listOf(
    Person("Alice", 25),
    Person("Bob", 30),
    Person("Charlie", 25)
)

// 按年龄分组
val grouped: Map<Int, List<Person>> = people.groupBy { it.age }

// 统计每个年龄的人数
val countByAge: Map<Int, Int> = people.groupingBy { it.age }.eachCount()

分组操作在报表统计中非常常用。我曾经在做一个用户画像系统时,用 groupBy 配合 eachCount,一行代码就完成了按城市统计用户数——Java 里至少得写十几行。

6.3 泛型类与泛型函数

泛型,说白了就是「类型占位符」。你写一个类或函数时,不确定具体类型,那就用 T、E、K、V 这些字母占个位置,等调用时再确定。

6.3.1 泛型类

class Box<T>(val value: T)

val intBox = Box<Int>(123)
val stringBox = Box("Hello")  // 类型推断

Kotlin 支持类型推断,所以 Box<String>("Hello") 可以简写为 Box("Hello")。但注意:如果泛型参数无法推断,必须显式指定。

6.3.2 泛型函数

fun <T> List<T>.secondOrNull(): T? {
    return if (size >= 2) this[1] else null
}

val list = listOf(1, 2, 3)
val second = list.secondOrNull()  // 2

泛型函数可以定义在顶层,也可以作为扩展函数。我经常用这种方式封装一些通用工具,比如 List<T>.randomOrNull(),避免重复写边界判断。

注意:泛型在运行时会被擦除(类型擦除)。你不能用 is T 来检查类型,但可以用 reified 关键字配合内联函数解决。这个后面章节会详细讲。

6.4 协变与逆变

协变和逆变,是泛型里最绕的部分。我当年学的时候也晕了好一阵。咱们用一个例子来理解。

6.4.1 为什么需要协变?

假设有一个 List<String>,能不能赋值给 List<Any>?直觉上可以,因为 String 是 Any 的子类。但 Java 里不行——泛型默认是不变的。Kotlin 用 out 关键字解决:

// 协变:生产者,只能读取,不能写入
val stringList: List<String> = listOf("a", "b")
val anyList: List<Any> = stringList  // 协变,允许

为什么 List<String> 可以赋值给 List<Any>?因为 List 在 Kotlin 中声明为 interface List<out E>out 表示 E 只能出现在「输出」位置(即方法的返回值)。你只能从 List 里读取元素,不能添加——所以类型安全。

6.4.2 逆变:消费者

逆变正好相反。假设有一个 Comparable<Any>,能不能赋值给 Comparable<String>?可以,因为能比较 Any 的,当然也能比较 String。Kotlin 用 in 关键字:

// 逆变:消费者,只能写入,不能读取
val anyComparable: Comparable<Any> = object : Comparable<Any> {
    override fun compareTo(other: Any): Int = 0
}
val stringComparable: Comparable<String> = anyComparable  // 逆变,允许

in 表示泛型参数只能出现在「输入」位置(即方法的参数)。你只能往里面写值,不能保证读出来的类型——所以叫消费者。

6.4.3 声明处协变与使用处协变

Kotlin 支持两种方式:

  • 声明处协变:在类定义时加 outin,如 interface List<out T>
  • 使用处协变:在函数参数中加 outin,也叫类型投影
// 使用处协变:类型投影
fun copy(from: Array<out Any>, to: Array<Any>) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

val stringArray: Array<String> = arrayOf("a", "b")
val anyArray: Array<Any> = arrayOf<Any>(1, 2)
copy(stringArray, anyArray)  // 使用处协变,允许
核心记忆法:
  • out = 生产者 = 只读 = 协变
  • in = 消费者 = 只写 = 逆变
  • 不变 = 既生产又消费 = 默认情况

我曾经在封装网络请求的泛型基类时,因为没搞清楚协变,导致子类无法赋值给父类变量,编译报错。后来加上 out 关键字,问题迎刃而解。嗯,这种坑踩一次,你就再也不会忘了。

6.5 本章小结

这一章我们覆盖了 Kotlin 集合与泛型的核心内容:

  • List、Set、Map 的创建与可变/不可变选择
  • 集合的遍历、过滤、映射、分组等函数式操作
  • 泛型类和泛型函数的定义与使用
  • 协变(out)与逆变(in)的原理和应用

集合操作是日常开发最频繁的代码之一,泛型则是构建可复用组件的基石。把这两块吃透,你的 Kotlin 水平会上一个台阶。下一章咱们会深入函数式编程的更多高阶玩法——lambda、高阶函数、内联函数,敬请期待。


公众号:蓝海资料掘金营,微信 deep3321