介绍
受限于 Java 字节码的限制,kotlin 依旧遵循着 Java 的 泛型 规范,在编译期间判断一个元素是否是某个泛型的子类,如下是 Kotlin 泛型例子,总体来说,大似相同,只是 Kotlin 将泛型的上界约束在最后进行声明
interface Edge<T : Number> {}class DirectEdge<T>(val from: T, val to: T) : Edge<T>where T : Comparable<T>, T : Number {}
但是,与 默认Java的编译器相比较,Kotlin 编辑器为了在编译期间和运行期间获取类型做出了更多的努力,使得能够进行一些类型判断和类型引用,不过总体来说,提升不大
实例化参数
编译期间
public static void main(String[] args) {List<DirectEdge<Double>> directEdges = new ArrayList<>();// 编译器错误,Illegal generic type for instanceofif (directEdges instanceof List<DirectEdge<Double>>){}}fun main() {val directEdges = ArrayList<DirectEdge<Double>>()// kotlin 在编译期间通过类型推断允许改判断,但字节码层面,仍然是 instanceOf List// 增加了编译期间判断的可靠性if (directEdges is List<DirectEdge<Double>>) {}}
运行期间
为了拜托泛型擦除导致的类型擦除,Kotlin 依靠 inline 可以声明一个带有具体实参的函数
inline fun <reified T> isType(value: Any): Boolean = value is T
fun main() {
isType<String>("type").apply(System.out::println)
isType<String>(12).apply(System.out::println)
}
kotlin 中 inline 在编译期间会把每一次函数调用都换成具体函数细节的实现,接下来看看编译后的字节码
第一行代码的解释:
L0
LINENUMBER 19 L0
// push "type" to L0
LDC "type"
ASTORE 0
L2-3
// push 1 to L0
LINENUMBER 22 L2
ICONST_1
ISTORE 0
L4
// push PrintStream to L1
LINENUMBER 19 L4
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ASTORE 1
L5-8
// push 0 to L2 L3 L4 L4
ICONST_0
ISTORE 2
...
L9
ALOAD 1
ILOAD 4
L10
LINENUMBER 19 L10
INVOKEVIRTUAL java/io/PrintStream.println (Z)V
public final static main(){
boolean r4=true;
System.out.println(r4);
}
public final static main()V
L15
// push 12 to L0, 0 to L1
LINENUMBER 20 L15
BIPUSH 12
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 0
ICONST_0
ISTORE 1
L17
LINENUMBER 23 L17
// L0 instanceOf String to L0
ALOAD 0
INSTANCEOF java/lang/String
L18
ISTORE 0
L19
// push PrintStream to L1
LINENUMBER 20 L19
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ASTORE 1
L22
// push L0 to L4
ILOAD 0
ISTORE 4
L24
ALOAD 1
ILOAD 4
L25
LINENUMBER 20 L25
INVOKEVIRTUAL java/io/PrintStream.println (Z)V
public final static main(){
int L0=12;
boolean L0= L0 instanceOf String;
PrintStram L1=System.out.PrintSteam()
boolean L4=L0
printSteam.Println(L4)
}
通过字节码的判断,对于 isType
得出结论,Kotlin 依旧无法摆脱泛型的擦除,只是依赖于字节码的重编写,将具体函数内联过程时,将 泛型类型 T 替代为具体类型 String, 所以类型参数的实例化只适用于下面这种场景,在编译期间,能够让编译器感知到这个具体类型是什么
:::warning
Note1: 在泛型类的内部,声明了内联函数却是无法调用的,Kotlin 规定 isType
Note2: 实例化参数只试用与获取具体的类型和进行类型判断,并不能用来创建实例
:::
通过 reified 的妙用,在编写工具函数时,能够代替具体类 Class 的引用 ,如
inline fun <reified T> loadService(){
return ServiceLoader.load(T::class.java);
}
子类型
谈起泛型,必不可少的不能不讨论类型参数的泛化,Kotlin 对泛化做出了一些不同Java 的变化,有如下概念:
协变类:如果A:B() ,则 List 是 List 的子类型,保留子类型泛化
限制了T 在函数中只能在 out 位置,不能在 in 位置,即只生产不消费 ,即 只读
逆变类:如果A: B() ,则 Consumer 就是 Consumer 的子类型
限制了T在函数中只能处于 in位置,即只消费不生产,即 只写
:::info 协变与逆变只是 kotlin 在编译期进行类型判断,根据字节码可以判断出,最后都是泛型擦除到 Object,无其他元素添加 ::: 如下展示一个逆变类的错误例子与正确用法
// 错误例子,逆变类说明的是 Consumer<B> 是 Consumer<A> 的子类型, 但对于具体 Consumer中的泛型 T,传入参数
//时仍然要保持类型匹配,这跟协变与逆变无关
fun main(){
val aConsuumer=Consumer<A>()
val bConsumer=Consumer<B>()
val a=A()
val b=B()
aConsumer(a)
// 错误,类型是不匹配
aConsumer(b)
bConsumer(a)
bConsumer(b)
}
fun doConsumer(consumer:Consumer<A>){}
fun main() {
doConsumer(aConsumer)
doConsumer(bConsumer)
}
:::info 总的来说,利用的是Java 继承的一套,可以将 斜边类返回的是一个父类A,而具体返回 B ,C 都没有关系,因为都是继承与A,都含有A的方法 :::
interface Transformer<T>{
fun transform(in:T) : out: T{
}
}
