面试题答案
一键面试类型擦除的定义
在Kotlin(以及Java等语言)中,类型擦除指的是在编译阶段,泛型类型信息会被擦除,替换为其限定的边界类型(通常是Any
,如果有指定上界则为上界类型)。例如,List<String>
在运行时实际上是List
,编译器会在编译期确保类型安全,运行时并不保留具体的泛型类型参数。
类型擦除对泛型代码的影响
- 运行时无法获取真实类型:
- 例如,在下面代码中:
fun <T> printType(list: List<T>) { // 这里无法获取到T的具体类型,因为类型擦除,运行时T被擦除 println(list.javaClass.typeParameters.contentToString()) }
- 无法在运行时准确得知
list
中元素的具体类型,list.javaClass.typeParameters
获取到的是擦除后的类型参数信息,在运行时List<T>
就是List
。
- 数组创建问题:
- 由于类型擦除,无法直接创建泛型数组。例如,
val array: Array<T> = Array(5) { null }
这样的代码是不允许的,因为运行时T
被擦除,无法确定数组元素的实际类型。
- 由于类型擦除,无法直接创建泛型数组。例如,
- 泛型方法重载困难:
- 考虑以下两个方法:
fun <T> process(list: List<T>) {} fun <U> process(list: List<U>) {}
- 这两个方法在编译时会报错,因为类型擦除后,它们的签名在运行时是相同的(都是
process(List)
),编译器无法区分这两个方法。
Kotlin中应对这些影响的解决方案
- 使用
reified
关键字(内联函数中):- 当函数被声明为
inline
并且类型参数被声明为reified
时,编译器会在调用点进行实化,从而在运行时可以获取到具体的类型。例如:
inline fun <reified T> printElementType(list: List<T>) { println(T::class.java.simpleName) }
- 调用
printElementType(listOf("a"))
时,会输出String
,能够在运行时获取到具体的类型。
- 当函数被声明为
- 使用类型令牌(Type Token):
- 可以手动传递类型信息。例如:
class TypeHolder<T>(private val type: KClass<T>) { fun printType() { println(type.simpleName) } } val holder = TypeHolder(String::class) holder.printType()
- 这里通过
KClass
传递类型信息,从而在运行时能够获取到具体类型。
- 避免直接创建泛型数组:
- 可以使用
ArrayList
等集合类来代替直接创建泛型数组。例如:
fun <T> createList(size: Int): List<T> { val list = ArrayList<T>() for (i in 0 until size) { list.add(null as T) } return list }
- 这样就可以绕开无法直接创建泛型数组的问题。
- 可以使用
- 利用泛型约束和边界:
- 通过明确指定泛型的上界或下界,可以在编译期利用类型擦除后的信息进行类型检查。例如:
fun <T : Number> sum(list: List<T>): Number { var sum: Number = 0 for (num in list) { sum = sum.toDouble() + num.toDouble() } return sum }
- 这里通过
T : Number
指定了泛型的上界,编译器可以在编译期利用Number
类型的信息进行类型检查,虽然运行时T
被擦除,但依然能保证代码的类型安全。