1. 概述
本文将深入对比 Kotlin 中的 Array<T>
和 List<T>
。我们会从多个维度分类讨论它们之间的差异,并逐一展开分析,帮助你在实际开发中做出更合理的选择。
踩坑提示:刚从 Java 转 Kotlin 的同学容易混淆两者在泛型、可变性上的行为,尤其数组协变 vs 泛型不变的问题,务必小心!
2. Array vs List
2.1 数据结构
✅ Array<T>
是 JVM 原生数组的直接映射。
Kotlin 编译器会把 Array<T>
直接编译成 JVM 的数组类型(JVM array),因此它的大小在创建时就固定了,无法动态扩容。
例如:
val cities = arrayOf("Tehran", "Sari", "Neka")
通过 javap
查看字节码,结果如下:
0: iconst_3
1: anewarray #8 // class java/lang/String
iconst_3
将整数 3 推入操作栈;anewarray
根据栈顶值创建指定长度的对象数组。
⚠️ 这说明 Kotlin 数组底层就是标准 JVM 数组,性能高且受 JVM 特殊优化。
❌ 而 List<T>
和 MutableList<T>
是接口,有多种实现方式:
ArrayList<T>
:基于动态数组实现,支持自动扩容,随机访问快(O(1)),但插入/删除慢(O(n))。LinkedList<T>
:基于链表实现,插入删除快(O(1)),但随机访问慢(O(n))。
👉 因此,List 的内存布局和运行时特性取决于具体实现类,不像 Array 那样统一。
2.2 可变性(Mutability)
✅ Array
cities[0] = "Berlin" // 合法
但 ❌ 不能添加或删除元素,因为长度固定。
✅ List<T>
接口是只读的:它不提供任何修改方法(如 add/remove)。哪怕你用 listOf()
创建的列表,也不能增删改。
如果需要可变操作,必须使用 MutableList<T>
:
val colors = mutableListOf("Blue") // [Blue]
colors[0] = "Green" // 替换: [Green]
colors.add(0, "Red") // 插入头部: [Red, Green]
colors.add("Blue") // 尾部追加: [Red, Green, Blue]
📌 总结:
| 类型 | 是否可变 | 是否可变长 |
|------------------|----------|-----------|
| Array<T>
| ✅ | ❌ |
| List<T>
| ❌ | ❌ |
| MutableList<T>
| ✅ | ✅ |
2.3 泛型协变性(Generic Variance)
这是最容易踩坑的部分之一,尤其对熟悉 Java 的开发者。
✅ List<T>
是协变的(covariant),因为它声明为 interface List<out T>
。这意味着子类型关系被保留:
val colors = listOf("Red") // List<String>
val colorsAsAny: List<Any> = colors // ✅ 编译通过
解释:由于 String
是 Any
的子类,而 List<out T>
允许协变,所以 List<String>
可以赋值给 List<Any>
。
❌ MutableList<T>
是不变的(invariant),因为它的泛型既出现在输入也出现在输出位置(比如 add(T)
和 get()
):
val colors = mutableListOf("Red") // MutableList<String>
val colorsAsAny: MutableList<Any> = colors // ❌ 编译失败!
同理,Array<T>
也是 invariant:
val colors: Array<Any> = arrayOf<String>("Red") // ❌ 编译错误
💡 对比 Java:
- Java 中泛型是 invariant,但数组是 covariant(会导致运行时
ArrayStoreException
); - Kotlin 更安全:泛型默认 invariant,只读集合允许协变,可变集合一律不变。
2.4 原始类型特化(Specialized Primitives)
✅ Kotlin 提供了针对基本类型的专用数组构造函数,避免装箱开销:
val bytes = byteArrayOf(42)
val shorts = shortArrayOf(42)
val ints = intArrayOf(42)
val longs = longArrayOf(42)
val floats = floatArrayOf(42.0f)
val doubles = doubleArrayOf(42.0)
val chars = charArrayOf('a')
val booleans = booleanArrayOf(true)
这些数组会被编译成 JVM 的 primitive array(如 [I
表示 int[]
),使用专门的字节码指令:
0: iconst_1
1: newarray byte
这里的 newarray
指令专用于原始类型数组,比 anewarray
更高效。
❌ 而 List 没有这种优化 —— 即使你写 listOf(1, 2, 3)
,存储的仍是 Integer
对象,存在装箱/拆箱开销。
📌 场景建议:
- 高频数值计算、大数据量场景 → 优先用
IntArray
等原始数组; - 普通业务逻辑 → 用
List<Int>
完全没问题。
2.5 其他细微差异
Java 互操作性(Interop)
Kotlin 对部分 Java 类型做了“映射”处理:
java.util.List
↔kotlin.collections.List
T[]
↔kotlin.Array<T>
详见官方文档:Java 互操作 - 映射类型
⚠️ 注意:数组的映射规则略有不同,请参考 Java 数组互操作
相等性比较(Equality)
一个非常隐蔽但重要的区别:
println(intArrayOf(1) == intArrayOf(1)) // false(引用比较)
println(listOf(1) == listOf(1)) // true(内容比较)
Array<T>
使用 引用相等(即==
等价于===
)List<T>
使用 结构相等(逐个元素比较)
📌 实际影响:如果你把数组作为 Map 的 key 或 Set 的元素,很可能出问题,除非你自己包装或使用 contentEquals()
。
3. 总结
维度 | Array |
List |
---|---|---|
底层结构 | JVM 原生数组 | 接口,多种实现(ArrayList、LinkedList) |
大小是否可变 | ❌ 固定长度 | ✅ MutableList 可变 |
是否可修改元素 | ✅ | ❌ List 不可变,MutableList 可变 |
泛型协变性 | ❌ 不变(invariant) | ✅ List 协变(covariant) |
原始类型优化 | ✅ 有 byteArrayOf/intArrayOf 等 | ❌ 无,只能装箱 |
相等性比较 | 引用比较 | 内容比较 |
Java 互操作 | 直接对应 T[] | 对应 java.util.List |
✅ 最佳实践建议:
- 需要高性能、大量原始数据 → 用
IntArray
等原始数组 - 通用集合操作、函数式编程 → 优先用
List<T>
(不可变更安全) - 需要频繁增删 → 用
MutableList<T>
+ArrayList
实现 - 别拿数组当 key!要用内容比较时记得手动处理
最后提醒一句:不要被语法糖迷惑,arrayOf()
和 listOf()
看似相似,底层机制完全不同。理解清楚才能写出高效又安全的 Kotlin 代码。