我们在编程中,出于复用和高效的目的,我们使用到了泛型。但是泛型在JVM底层采取了类型擦除的实现机制,Kotlin也是这样。然后这也带来了一些问题和对应的解决方案。这里我们介绍一个reified用法,来实现更好的处理泛型。
类型擦除
如下面的代码,在编译成class文件后,就采用了类型擦除
1
2
3
4
5
6
7
8
public class TestTypeErasure {
public List < String > list = new ArrayList <>();
public void test () {
list . add ( "123" );
System . out . println ( list . get ( 0 ));
}
}
list实例真实的保存是多个Object
list.add("123")
实际上是"123"
作为Object
存入集合中
System.out.println(list.get(0));
是从list
实例中读取出来Object
然后转换成String
才能使用的
辅助证明的字节码内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Compiled from "TestTypeErasure.java"
public class TestTypeErasure {
//省略部分代码
public void test ();
Code:
0 : aload_0
1 : getfield # 4 // Field list:Ljava/util/List;
4 : ldc # 5 // String 123
6 : invokeinterface # 6 , 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
11 : pop
12 : getstatic # 7 // Field java/lang/System.out:Ljava/io/PrintStream;
15 : aload_0
16 : getfield # 4 // Field list:Ljava/util/List;
19 : iconst_0
20 : invokeinterface # 8 , 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
25 : checkcast # 9 // class java/lang/String
28 : invokevirtual # 10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31 : return
}
其中
第6行对应的6: invokeinterface #6, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
对应添加元素参数的类型为Object
第20行对应的20: invokeinterface #8, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
对应的获取元素的返回类型为Object
第25行为进行类型转换操作
类型擦除带来的问题
安全问题:未检查的异常
1
2
3
4
//unchecked cast
fun < T > Int . toType (): T ? {
return ( this as ? T )
}
上面的代码会导致编译器警告unchecked cast
上面的代码由于在转换类型时,没有进行检查,所以有可能会导致运行时崩溃
当我们执行这样的代码时
1
2
3
fun testCast () {
println ( 1. toType < String >()?. substring ( 0 ))
}
会得到java.lang.Integer cannot be cast to java.lang.String
的类型错误。
显式传递Class
针对前面的问题,我们最常用的办法就是显式传递class信息
1
2
3
4
5
6
7
8
//need pass class explicitly
fun < T > Any . toType ( clazz : Class < T >): T ? {
return if ( clazz . isInstance ( this )) {
this as ? T
} else {
null
}
}
但是显式传递Class信息也会感觉有一些问题,尤其是下面这段代码
1
2
3
4
5
6
7
8
fun < T > Bundle . plus ( key : String , value : T , clazz : Class < T >) {
when ( clazz ) {
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 )
}
}
上面的代码(传value值和clazz)我们会感觉到明显的有一些笨拙,不够智能。
但是这也是基于Java的类型擦除机制导致无法再运行时得到T
的类型信息,无法改进(至少在Java中)
可能导致更多方法的产生
同时,由于上面的显式传递Class信息比较麻烦和崩溃,我们有时候会增加更多的方法,比如下面的这样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Bundle {
fun putInt ( key : String , value : Int ) {
println ( "Bundle.putInt key=$key;value=$value" )
}
fun putLong ( key : String , value : Long ) {
}
fun putString ( key : String , value : String ) {
}
fun putChar ( key : String , value : Char ) {
}
}
上面的putInt
,putLong
,putString
和putChar
没有泛型引入
我们没有排除显式传递Class参数之外的优雅实现,比如我们只提供一个叫做put(key: String,value: T)
reified 方式
不过,好在Kotlin有一个对应的解决方案,这就是我们今天文章标题提到的reified(中文意思:具体化)
使用reified很简单,主要分为两步
在泛型类型前面增加reified
在方法前面增加inline
(必需的)
接下来我们使用reified改进之前的方法
类型转换改进后的代码
1
2
3
4
5
6
7
8
//much better way using reified
inline fun < reified T > Any . asType (): T ? {
return if ( this is T ) {
this
} else {
null
}
}
方法传参不需要多余传递参数类型信息
1
2
3
4
5
6
7
8
inline fun < reified T > Bundle . plus ( key : String , value : T ) {
when ( value ) {
is Long -> putLong ( key , value )
is String -> putString ( key , value )
is Char -> putChar ( key , value )
is Int -> putInt ( key , value )
}
}
reified实现原理
不是说,泛型是使用了类型擦除么,为什么运行时能得到T
的类型信息呢?
是的,采用类型擦除没有错,至于能在运行时得到T
的类型信息是如何做到的,就需要了解reified
的内部机制了。
其原理为
Kotlin编译器会将reified方法asType
内联(inline)到调用的地方(call-site)
方法被内联到调用的地方后,泛型T会被替换成具体的类型
所以 reified 使得泛型的方法假装在运行时能够获取泛型的类信息
为了便于理解,我们举个例子,如下是我们的代码
1
2
3
fun testCast2 () {
println ( 1. asType < String >()?. substring ( 0 ))
}
对应的反编译后的java代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static final void testCast2 () {
Object $this$asType$iv = 1 ;
int $i$f$asType = false ;
String var10000 = ( String )( $this$asType$iv instanceof String ? $this$asType$iv : null );
String var3 ;
/**
* 后续的代码对应的Kotlin代码(也包含了部分call-site的逻辑,比如substring)
return if (this is T) {
this
} else {
null
}
*/
//inline和reified替换开始
if (( String )( $this$asType$iv instanceof String ? $this$asType$iv : null ) != null ) {
var3 = var10000 ;
byte var4 = 0 ;
boolean var2 = false ;
if ( var3 == null ) {
throw new TypeCastException ( "null cannot be cast to non-null type java.lang.String" );
}
var10000 = var3 . substring ( var4 );
Intrinsics . checkExpressionValueIsNotNull ( var10000 , "(this as java.lang.String).substring(startIndex)" );
} else {
var10000 = null ;
}
//inline和reified替换结束
var3 = var10000 ;
$i$f$asType = false ;
System . out . println ( var3 );
}
all in(lined)?
既然是inline,应该是把被inline的方法全部提取到调用处(call-site)吧?
是的,通常是这样,不过reified可能有一些差异
reified方法并不会完全inline所有的方法实现,而是更加智能一些的类型匹配中断提取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun testBundlePlusLong () {
Bundle (). plus ( "hello" , 1000L )
}
fun testBundlePlusString () {
Bundle (). plus ( "hello" , "World" )
}
fun testBundlePlusChar () {
Bundle (). plus ( "hello" , 'h' )
}
fun testBundlePlusInt () {
Bundle (). plus ( "hello" , 1 )
}
再次贴一些Bundle.plus实现
1
2
3
4
5
6
7
8
inline fun < reified T > Bundle . plus ( key : String , value : T ) {
when ( value ) {
is Long -> putLong ( key , value )
is String -> putString ( key , value )
is Char -> putChar ( key , value )
is Int -> putInt ( key , value )
}
}
上面的when表达式的类型检查次序依次为
反编译后的方法如下(类型不同,提取的方法体也不同)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public static final void testBundlePlusLong () {
Bundle $this$plus$iv = new Bundle ();
String key$iv = "hello" ;
long value$iv = 1000L ;
int $i$f$plus = false ;
//第一个就是Long类型,无需包含后面的检查代码
$this$plus$iv . putLong ( key$iv , value$iv );
}
public static final void testBundlePlusString () {
Bundle $this$plus$iv = new Bundle ();
String key$iv = "hello" ;
Object value$iv = "World" ;
int $i$f$plus = false ;
//不是Long类型,需要继续匹配,找到String类型,终止inline后续代码
if ( value$iv instanceof Long ) {
$this$plus$iv . putLong ( key$iv , (( Number ) value$iv ). longValue ());
} else {
$this$plus$iv . putString ( key$iv , value$iv );
}
}
public static final void testBundlePlusChar () {
Bundle $this$plus$iv = new Bundle ();
String key$iv = "hello" ;
Object value$iv = 'h' ;
int $i$f$plus = false ;
//不是Long类型,需要继续匹配,
if ( value$iv instanceof Long ) {
$this$plus$iv . putLong ( key$iv , (( Number ) value$iv ). longValue ());
//不是String类型,需要继续匹配,
} else if ( value$iv instanceof String ) {
$this$plus$iv . putString ( key$iv , ( String ) value$iv );
} else {
//找到String类型,终止inline后续代码
$this$plus$iv . putChar ( key$iv , value$iv );
}
}
public static final void testBundlePlusInt () {
Bundle $this$plus$iv = new Bundle ();
String key$iv = "hello" ;
Object value$iv = 1 ;
int $i$f$plus = false ;
//最差的一种情况,inline全部的方法体实现
if ( value$iv instanceof Long ) {
$this$plus$iv . putLong ( key$iv , (( Number ) value$iv ). longValue ());
} else if ( value$iv instanceof String ) {
$this$plus$iv . putString ( key$iv , ( String ) value$iv );
} else if ( value$iv instanceof Character ) {
$this$plus$iv . putChar ( key$iv , ( Character ) value$iv );
} else {
$this$plus$iv . putInt ( key$iv , (( Number ) value$iv ). intValue ());
}
}
以上就是关于reified的内容,其实在Kotlin中有很多的特性是依赖于编译器的工作来实现的。