说kotlin中这个关键字之前先简单说下Java中的泛型,我们在编程中,出于复用和高效的目的,经常使用泛型。泛型是通过在JVM底层采取类型擦除的机制实现的,Kotlin也是这样。
泛型
泛型是 Java SE 1.5 中的才有的特性,泛型的本质是参数化类型,可分为泛型类、泛型接口、泛型方法。在没有泛型的情况的下只能通过对Object 的引用来实现参数的任意化,带来的缺点就是要显式的强制类型转换,而强制转换在编译期是不做检查的,容易把问题留到运行时,所以泛型的好处是在编译时检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率,避免在运行时出现 ClassCastException。
JDK 1.5 中引入了泛型来允许强类型在编译时进行类型检查;JDK 1.7 中泛型实例化类型具备了自动推断的能力,譬如List<String> mList = new ArrayList<String>() 可以写成 List<String> mList = new ArrayList<>()
类型擦除
泛型通过类型擦来实现,编译器在编译时擦除所有泛型类型相关信息,即运行时就不存在任何泛型类型相关的信息,譬如 List<Integer> 在运行时仅用一个 List 来表示,这样做的目的是为了和 Java 1.5 之前版本进行兼容。
fun test() {
val mList= ArrayList<String>()
mList.add("123")
Log.v("tag",mList[0])
}
字节码如下:
public final test()V
L0
LINENUMBER 18 L0
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 1
L1
LINENUMBER 19 L1
ALOAD 1
LDC "123"
INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z
POP
L2
LINENUMBER 20 L2
LDC "tag"
ALOAD 1
ICONST_0
INVOKEVIRTUAL java/util/ArrayList.get (I)Ljava/lang/Object;
CHECKCAST java/lang/String
INVOKESTATIC android/util/Log.v (Ljava/lang/String;Ljava/lang/String;)I
POP
L3
LINENUMBER 21 L3
RETURN
L4
LOCALVARIABLE mList Ljava/util/ArrayList; L1 L4 1
LOCALVARIABLE this Lcom/github/coroutinesdemo/Test; L0 L4 0
MAXSTACK = 3
MAXLOCALS = 2
INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z list.add("123")实际上是"123"作为Object存入集合中的
INVOKEVIRTUAL java/util/ArrayList.get (I)Ljava/lang/Object 从list实例中读取出来Object然后转换成String之后才能使用
CHECKCAST java/lang/String进行类型转换
泛型擦除在编译成字节码时首先进行类型检查,再进行类型擦除(即所有类型参数都用限定类型替换,包括类、变量和方法如果类型变量有限定则原始类型就用第一个边界的类型来替换,譬如 class Test<T extends Comparable & Serializable> {} 的原始类型就是 Comparable)
如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。
类型擦除的问题
类型擦除会有一系列的问题,这里不展开了
- 泛型读取时会进行自动类型转换问题,所以如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换
- 泛型类型参数不能是基本类型, 擦除后的Object 是引用类型不是基本类型
- 无法进行具体泛型参数类型的运行时类型检查,
instanceof ArrayList<?> - 不能抛出也不能捕获泛型类的对象,因为异常是在运行时捕获和抛出的,而在编译时泛型信息会被擦除,擦除后两个 catch 会变成一样的东西。不能在 catch 子句中使用泛型变量,因为泛型信息在编译时已经替换为原始类型(譬如 catch(T) 在限定符情况下会变为原始类型 Throwable),如果可以在 catch 子句中使用,则违背了异常的捕获优先级顺序
fun <T>Int.toCase():T?{
return (this as T)
}
上述代码在转换类型时,没有进行检查,所以有可能会导致运行时崩溃,编译器会提示unchecked cast警告,如果获得的数据不是它期望的类型,这个函数会出现崩溃
fun testCase() {
1.toCase<String>()?.substring(0)
}
这就会出现TypeCastException错误,所以为了安全获取数据一般都是需要显式传递class信息:
fun <T> Int.toCase(clz:Class<T>):T?{
return if (clz.isInstance(this)){
this as? T
}else{
null
}
}
fun testCase() {
1.toCase(String::class.java)?.substring(0)
}
但这需要通过显示传递class的方式过于麻烦繁琐尤其是传递多类型参数,基于类型擦除机制无法在运行时得到T的类型信息,所以用到安全转换操作符as或者as?
fun <T> Bundle.putCase(key: String, value: T, clz:Class<T>){
when(clz){
Long::class.java -> putLong(key,value as Long)
String::class.java -> putString(key, value as String)
Char::class.java -> putChar(key, value as Char)
Int::class.java -> putInt(key, value as Int)
else -> throw IllegalStateException("Type not supported")
}
}
那有没有排除这种传递参数之外的优雅实现???
reified 关键字
reified关键字的使用很简单:
-
在泛型类型前面增加
reified修饰 -
在方法前面增加
inline改进上述代码
inline fun <reified T> Int.toCase():T?{ return if (this is T) { this } else { null } }testCase()方法调用转成Java 代码看下 :
public final void testCase() { int $this$toCase$iv = 1; int $i$f$toCase = false; String var10000 = (String)(Integer.valueOf($this$toCase$iv) instanceof String ? Integer.valueOf($this$toCase$iv) : null); // inline部分 String var1; if (var10000 != null) { // 替换开始 var1 = var10000; $this$toCase$iv = 0; if (var1 == null) { throw new TypeCastException("null cannot be cast to non-null type java.lang.String"); } var10000 = var1.substring($this$toCase$iv); Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).substring(startIndex)"); } else { var10000 = null; } // reified替换结束 var1 = var10000; System.out.println(var1); }Inline的作用这里不再多说了,noinline和crossinline又是啥?这里可以看下。
泛型在运行时会被类型擦除,但是在inline函数中我们可以指定类型不被擦除, 因为inline函数在编译期会将字节码copy到调用它的方法里,所以编译器会知道当前的方法中泛型对应的具体类型是什么,然后把泛型替换为具体类型,从而达到不被擦除的目的,在inline函数中我们可以通过reified关键字来标记这个泛型在编译时替换成具体类型
示例
我们在用 Gson 解析 JSON 数据时,如果涉及泛型类型(例如 List),由于 Java 的泛型在运行时会发生类型擦除,Gson 无法仅通过 Class 对象获取完整的泛型参数类型信息。 为了解决这个问题,可以使用 TypeToken。TypeToken 通过 getType() 方法获取包含泛型参数信息的 Type 对象,从而将完整的泛型类型结构传递给 Gson。 需要注意的是,Java 的泛型机制在编译期间会进行类型擦除,但并不是所有泛型信息都会完全丢失。编译器会在 class 字节码中的 Signature 属性 中保留一部分泛型信息。这些信息不属于 JVM 指令的一部分,而是作为 class 文件的元数据存在,通常存储在常量池相关结构中。 具体来说:
- 类定义处的泛型信息会被保留
- 接口声明处的泛型信息会被保留
- 方法声明处的泛型信息会被保留
- 成员变量声明处的泛型信息会被保留
- 但局部变量和运行时表达式中的泛型信息会被擦除 编译后,使用泛型的类、方法或字段会在 class 文件中生成一个 Signature 属性,用于描述完整的泛型类型结构。JDK 提供了反射 API(如 getGenericSuperclass()、getGenericType() 等)用于读取这些泛型签名信息。
TypeToken 的实现原理正是利用了这一点。通过创建一个匿名子类:
new TypeToken<List<User>>() {}
此时 List 是作为父类泛型参数写在类定义上的,因此会被保存在该匿名类的 Signature 中。TypeToken 在运行时通过:
getClass().getGenericSuperclass()
获取父类的 ParameterizedType,再通过:
getActualTypeArguments()
提取出具体的泛型参数类型(如 User)。 这样,Gson 就可以获得完整的泛型类型结构,而不仅仅是擦除后的 Class 类型。 另外,在构造对象实例时,Gson 会优先使用无参构造方法;如果不存在无参构造方法,则可能通过内部机制(如 UnsafeAllocator)绕过构造方法直接创建对象实例。因此,并不是严格依赖默认无参构造。
最后: 一般Gson解析:
inline fun <reified T> Gson.fromJson(jsonStr: String) =
fromJson(json, T::class.java)
如果用Moshi解析:
inline fun <reified T> Moshi.fromJson(jsonStr: String) = Moshi.Builder().add(KotlinJsonAdapterFactory()).build().adapter(T::class.java).fromJson(jsonStr)