说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数据的时候,是如何解析数据拿到泛型类型 Bean
结构的?TypeToken 是一种方案,可以通过getType() 方法获取到我们使用的泛型类的泛型参数类型,不过采用反射解析的时候,Gson构造对象实例时调用的是默认无参构造方法,所以依赖 Java 的 Class 字节码中存储的泛型参数信息,Java 的泛型机制虽然在编译期间进行了擦除,但是Java 在编译时会在字节码里指令集以外的地方保留部分泛型的信息,接口、类、方法定义上的所有泛型、成员变量声明处的泛型都会被保留类型信息,其他地方的泛型信息都会被擦除,这些信息被保存在 class 字节码的常量池中,使用泛型的代码处会生成一个 signature 签名字段,通过签名 signature 字段指明这个常量池的地址,JDK 提供了方法去读取这些泛型信息的方法,利用反射就可以获得泛型参数的具体类型,譬如:
(mList.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
一般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)