第六章:集合与泛型——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 里不能直接使用 break 或 continue,如果需要提前退出,还是用 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]
filter 再 map 效率更高,因为减少了后续操作的元素数量。我在优化一个列表加载性能时,就是靠调整操作顺序,把耗时从 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 支持两种方式:
- 声明处协变:在类定义时加
out或in,如interface List<out T> - 使用处协变:在函数参数中加
out或in,也叫类型投影
// 使用处协变:类型投影
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、高阶函数、内联函数,敬请期待。