1. 简介
类型擦除(Type Erasure) 是 Java 编译器在运行时移除泛型类类型信息的一种机制。也就是说,泛型的类型检查只在编译期进行,运行时具体的类型信息会被“擦除”,从而保证运行时的类型安全。
而 Kotlin 的泛型允许我们编写抽象且可复用的代码,适配多种数据类型。在编译期,泛型参数的类型是已知的,编译器可以据此做类型检查,确保类型安全。
但在实际开发中,我们有时会面对类型未知的 List<*>
或 List<Any>
,这时如果需要将其转换为特定类型的数组,就必须借助一些技巧。本文将介绍在 Kotlin 中实现这种转换的几种常用方式。
⚠️ 注意:本文讨论的是「类型已被擦除」或「泛型通配」的列表转数组问题,不是普通
List<T>
转Array<T>
,后者直接调用toTypedArray()
即可。
2. 使用 toTypedArray()
方法
最直接的方式是使用 Kotlin 内置的 toTypedArray()
方法。但由于列表元素类型未知,唯一安全的做法是将其转为 Array<Any>
:
@Test
fun `convert type-erased list to array using toTypedArray`() {
val intList: List<Any> = listOf(1, 2, 3, 4, 5)
val stringList: List<Any> = listOf("one", "two", "three", "four", "five")
val array1 = intList.toTypedArray()
val array2 = stringList.toTypedArray()
assertArrayEquals(arrayOf(1, 2, 3, 4, 5), array1)
assertArrayEquals(arrayOf("one", "two", "three", "four", "five"), array2)
}
✅ 上述代码中,array1
和 array2
都是 Array<Any>
类型。
❌ 如果你试图将其强转为 Array<Int>
或 Array<String>
,会在运行时抛出 ClassCastException
—— 因为 JVM 不允许改变数组的实际组件类型。
💡 踩坑提醒:
Array<Any>
和Array<Int>
在 JVM 层面是两种完全不同的类型,不能互相赋值,即使Int
是Any
的子类型。
2.1 使用内联函数与具体化类型参数(reified)
如果我们希望得到一个真正指定类型的数组(比如 Array<Int>
),就不能依赖普通的泛型函数,因为它们同样受类型擦除影响。
解决方案是使用 内联函数 + 具体化类型参数(reified),这样可以在运行时获取泛型的实际类型。
inline fun <reified T> mapToArray(list: List<*>): Array<T> {
return list.mapNotNull { it as? T }.toTypedArray()
}
📌 关键点解析:
- ✅
inline
:函数被内联展开,避免了泛型擦除。 - ✅
reified T
:允许在函数体内使用T
的类型信息(如as? T
)。 - ✅
mapNotNull { it as? T }
:安全地尝试将每个元素转为T
类型,失败的(类型不匹配或 null)会被过滤掉。 - ✅ 最终通过
toTypedArray()
得到Array<T>
。
我们来验证一下效果:
@Test
fun `convert type-erased list to array using toTypedArray with reified and inline`() {
val intList: List<Any> = listOf(1, 2, 3, 4, 5)
val stringList: List<Any> = listOf("one", "two", "three", "four", "five")
val array1 = mapToArray<Int>(intList)
val array2 = mapToArray<String>(stringList)
assertArrayEquals(arrayOf(1, 2, 3, 4, 5), array1)
assertArrayEquals(arrayOf("one", "two", "three", "four", "five"), array2)
}
✅ 成功得到了 Array<Int>
和 Array<String>
,类型明确且可通过编译器检查。
💡 小贴士:这个方法适合处理可能存在混合类型的
List<Any>
,比如从 JSON 解析出来的数据列表。
3. 使用 Array()
构造函数
另一种方式是直接使用 Array
的构造函数,它接受两个参数:
- 数组长度
- 一个 lambda,用于根据索引生成每个元素
@Test
fun `convert type-erased list to array using array constructor`() {
val intList: List<Any> = listOf(1, 2, 3, 4, 5)
val stringList: List<Any> = listOf("one", "two", "three", "four", "five")
val array1 = Array(intList.size) { i -> intList[i] as Int }
val array2 = Array(stringList.size) { i -> stringList[i] as String }
assertArrayEquals(arrayOf(1, 2, 3, 4, 5), array1)
assertArrayEquals(arrayOf("one", "two", "three", "four", "five"), array2)
}
📌 特点分析:
- ✅ 手动控制每个元素的类型转换。
- ❌ 不具备泛型灵活性,每个类型都要写一遍。
- ⚠️ 强转
as Int
存在风险,若列表中存在非Int
类型会抛ClassCastException
。
💡 建议仅在确定列表元素类型一致时使用此方式,否则应改用
as?
+ 判空处理。
4. 总结
本文探讨了在 Kotlin 中将类型擦除的 List<*>
或 List<Any>
转换为特定类型数组的三种方式:
方法 | 是否支持指定类型 | 安全性 | 灵活性 |
---|---|---|---|
toTypedArray() |
❌ 只能生成 Array<Any> |
✅ 安全 | ❌ 低 |
reified + inline |
✅ 支持任意 T |
✅ 安全(配合 as? ) |
✅ 高 |
Array() 构造函数 |
✅ 支持指定类型 | ⚠️ 强转有风险 | ❌ 低 |
✅ 推荐做法:
- 日常使用优先选择
reified
内联函数,兼顾类型安全与复用性。 - 已知类型且性能敏感场景可用
Array()
构造函数,但务必确保类型匹配。 - 单纯转为
Array<Any>
可直接用toTypedArray()
,但后续无法强转。
🔗 示例代码已上传至 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-collections-list-2