泛型

  1. 泛型允许你定义带类型形参的类型。
  2. 但是当这个类型的实例被创建出来时,类型形参将会被替换为类型实参的具体类型。即T,E这样的类型形参会被替换为具体的StringInt之类的。
  3. 此外,kotlin的编译器可以根据你给定的类型自动推导出所需的类型实参。但是如果你没有给定相关的类型,即编译器无法判断。如你只给定了一个空数组。此时你必须显式的说明你的类型实参是什么(就是必须写清楚<>中的参数类型)

    注意,因为java中,泛型是在1.6后的版本才被引入。所以为了和老版的java兼容,他允许使用没有类型参数的泛型类型–所谓的原生态类型。而kotlin不然,他必须声明泛型的类型。

泛型类型和属性


如图所示,你必须可以接受一个类型实参进入,用来代替T。

1
2
3
4
fun <T> testFanxing(x: T): T {
println( x.hashCode())
return x as T
}

他不仅可以适用与函数,也可以适用于属性

1
2
val <T> List<T>.lastIndex:T
get() = 1 as T

但是如果作为非扩展的属性,他将意义不大,因此kotlin编译器拒绝了对普通属性的泛型化。只有扩展属性才能泛型化。

对于类也是不必多言,他同样可以适用于泛型。

类型参数约束

我们可以约束泛型的范围,有两种方法:

  1. 为泛型添加上界约束
    1
    2
    3
    4
    fun <T:Number> sum(a:T, b:T){
    println(a)
    println(b)
    }
    这样子泛型直接为数字类型。
    还有其他的例子:

    如果要限定多个参数类型,需要使用where关键字
    1
    2
    3
    4
    fun <T> sum(a:T, b:T) where T:Appendable,T:Number{
    println(a)
    println(b)
    }

运行时泛型:擦除和实化类性参数

JVM上的泛型都是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。

运行时泛型

和java一样,kotlin的泛型在运行时也会被擦除。这意味着即便你创建一个List的类,在运行时也只能看到List而已。你无法判断他具体是什么类型,只能判断他是一个List。

如果你尝试去判断一个list类具体是什么类型。他只会抛出错误,“无法检查一个被擦除的类”
擦除也是有好处的,它可以降低应用程序的内存用量,因为保存在内存的类型信息更少了

如果要判断一个类是不是list而不是set或其他的集合类,可以使用星号投影来做检查:

1
2
3
 if(list is List<*>){

}

注意,在 as as ?转换中仍然可以使用一般的泛型类型 但是如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时转换发生的时候类型实参是未知的。因此,这样的转换会导致编译器发出“unchecked cast ”(未受检转换)的警告。这仅仅是一个警告,你仍然可以继续使用这个值,就当它拥有必要的类型,如下所示。

也就是说,如果你要把set类型强行转化为list<*>类型,那么他的基础类型转化就是错误的,他会直接抛出一个IllegalArgumentException。表示参数错误。因为他也无法判断你的类型实参是否正确。而如果你是从list转化为list那么他将会抛出ClassCastException。

带实化类型参数的函数


在一般的情况下,我们的类型实参都会被JVM擦除掉,而这只有一种情况可以避免:内联函数。内联函数可以实化类型参数。
inline函数的作用此时被挖掘了出来,他将不止能够提高lambda函数的运行效率,也可以把参数类型实化。只需要

1
inline fun<reified T> isA(value: Any)=value is T

添加inline和refied关键词就可以。
然后他就可以正常检查数据类型是否正确。
注意,带 reified 类型参数的 inline 函数不能在 Java 代码中调用。

实化类型参数可以代替引用类


如果是使用普通的类,那么他就无法提取T的反射。只有实化才能使用。

实化类型参数的限制

变型、泛型和子类型化

为什么会出现变型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

fun main(){
//只读
val list = listOf("abc","123")
fun listOnlyRead(list: List<Any>){
println(list.joinToString())
}
listOnlyRead(list)
//可变
val mutableList = mutableListOf("abc","123")
fun listChange(list: MutableList<Any>){
list.add(12)
}
listChange(mutableList)
}

对于只读的list集合,我们把一个List的变量传递给List也是安全的。但是如果我们传递的是可变的,那么他将会出现异常,因为我们给一个Stirng类型的list添加int类型的数据。
当然,这个错误已经被编译器捕获,他不会让我们编译成功。
因此,才会有对变型的讨论,避免出现类型转换后的报错问题。

类,类型

类的名称可以直接当作类型使用。例如,如果你这样写var x:String,就是声明了一个可以保存String类的实例的变量。但是注意,同样的类名称也可以用来声明可空类型var x:String?。这意味着每一个Kotlin类都可以用于构造至少两种类型。泛型类的情况就变得更复杂了。要得到一个合法的类型,需要用一个作为类型实参的具体类型替换(泛型)类的类型形参。List不是一个类型(它是一个类),但是下面列举出来的所有替代品都是合法的类型:List<Int>,List<String?>,List<List<String>>,等。每一个泛型类都可能生成潜在的无限数量的类型。


简单的情况下,子类型和子类本质上意味着一样的事物。例如,Int类是Number的子类,因此Int类型是Number类型的子类型。如果一个类实现了一个接口,它的类型就是该接口类型的子类型:String是CharSequence的子类型。
如果对于任意两种类型 A,B。MutableList<A>既不是 MutableList<B> 的子类型也不是它的超类型,它被称为在该类型参数上是不变型的。Java 中所有的类都是不变型的

协变,逆变


使用out关键词约束就只能放在out位置上,同理in也一样。

  1. 协变:
    如果A是B的子类型,那么 Producer<A>就是
    Producer<B>的子类型。我们说子类型化被保留了 。
  • 子类型化会被保留( Producer<Cat>Producer <Anirnal> 的子类型),也就是可以输出为子类型
  • 只能用在 out 位置
  1. 逆变:


更多关键内容查看该文章

点变型

使用out,in这些关键词就是声明点变型。
但是有些不同的地方就在于,使用了点变型的都被称为类型投影,他们不是一个常规的类,而是一个可以返回类型是泛型参数类型的方法。

例如你选择了一个Any的泛型类型,那么这个他最多只能使用Any的方法,至于他的儿子Int,String的特有方法都不能使用。

星号投影

当类型实参的信息并不重要的时候,可以使用星号投影的语法:不需要使用任何在签名中引用类型参数的方法,或者只是读取数据而不关心它的具体类型。