聊一聊Kotlin的泛型

Kotlin的泛型

简介

与java一样,kotlin也支持泛型,用法和java泛型差别不大,kotlin特色是型变支持。

基本用法:
定义类:

跟java相同,定义在类后面的尖括号:

open class Basket<T>{
    
}

定义方法:

定义在fun 关键字和 方法名之间。

//java
  public <S> void  testFunction(S s){
        //todo
    }
//kotlin
  fun <S> testFunction(s:S){
        //todo
    }

以声明一个水果篮为例,在构造方法中声明了泛型,里面提供一个list支持set和get操作:

open class Basket<T> {

    var content: T? = null

    fun set(fruit: T) {
        content = fruit
    }

    fun get(): T? {
        return content
    }
}

定义一个水果类:

open class Fruit {
    open fun desc() {
        println("它是水果")
    }
}

使用:

fun main(args: Array<String>) {
    val fruit1 = Fruit()
    val basket = Basket(fruit1)
}
与java的尖括号语法不同,如果我在类的构造方法中指定了类型的话,在kotlin中可省略不写,其可帮我们自动推断。
从泛型类派生子类:

我现在写一个小水果篮子继承自果篮类:

class SmallBasket : Basket<Fruit>()
注意点:

与java不同的是,无论是通过显示指定还是让系统推断。kotlin要求始终为泛型参数明确地指定类型,所以上面参数我指定为水果类。
而在java中,以下两种都是允许的:

 public  class SmallBasketJ extends BasketJ<String> {
    }

 public  class SmallBasketJ extends BasketJ{

    }
型变:

回顾一下,java的泛型是不支持型变的,如何理解这句话呢?
首先这行代码是没有问题的:

String string = new String("sss");
Object object = string;

因为string是Object的子类,子类可以协变为父类,但是在泛型中:

List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // 不允许

因此,Java 禁止这样以保证运行时的安全,因为如果上面的代码允许被编译通过那么:

//这里我们把一个整数放入一个字符串列表
objs.add(1); 
//报 ClassCastException
String s = strs.get(0); 

所以泛型不支持型变的设计保证了其是“类型安全的”,但是通过通配符,可以让他们有“型变”的能力,具体为:

java通配符上限:
<? extends E>

表示此方法接受 E 或者 E 的 一些子类型对象的集合,而不只是 E 自身。 这意味着我们可以安全地从其中(该集合中的元素是 E 的子类的实例)读取 E,但不能写入, 因为我们不知道什么对象符合那个未知的 E 的子类型。 反过来,该限制可以让Collection表示为Collection<? extends Object>的子类型。 简而言之,带 extends 限定(上界)的通配符类型使得类型是协变的(covariant)。

interface Collection<E> …… {
  void addAll(Collection<? extends E> items);
}

我们可以往Collection中添加E类型或者它的任意子类,这是泛型的协变。
我们用一个图来表示就是“正三角漏斗”,顶部就是我们的E:

image

意味着从泛型中取出(out)对象是安全的(一定是我们的E类型),但传入对象并不知道具体类型(可能是E或者它的子类)。

java通配符下限:
<? super T>

与通配符上限相反,限制传入的参数下限是T(即T或者它的父类)当我们用下限修饰符去修饰的话,将对象传给泛型对象是安全的,如Collections.copy方法:

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
       。。。。。。
    }

第一个参数使用通配符下限,限制了目标list只能是T或者T它的父类,而第二个参数拷贝源限制了参数只能是T或者T的子类,这样就保证了类型的合法。

        List<Apple> source = Arrays.asList(new Apple());
        //Object 也是最终父类 也ok
//        List<Object> destination = Arrays.asList(new Object());
        List<Fruit> destination = Arrays.asList(new Fruit());
        Collections.copy(destination,source);

我们上面的例子,我们可以完成这样的业务类型:

把包含苹果的List放入原有包含Fruit的List(或者Object的List).

通配符上限保证了传入参数的安全,如下“倒三角漏斗”所示:

image

我们可以往漏斗中放入E类型和任何它的父类,这就是泛型的逆变,意味着向其中传入(in)对象是安全的,但就不能保证取出来的参数的类型(可能是T,也可能是它的父类)

结论速记:

  • 通配符上限-extends-正三角-取出安全-out
  • 通配符下限-super-倒三角-存入安全-in
Kotlin型变:

无论java的通配符上限还是下限,都多少有缺陷,要么存不安全,要么取不安全,而在kotlin中,就解决了这个问题,让out:“纯输出” ,让in “纯输入” 。

在此之前,我们借助上面java的通配符的 (in) 和 (out) 的操作来理解一个概念:
我们称只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。

Kotlin声明处型变:
out: (协变注解)生产者:

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出-位置,但回报是 C 可以安全地作为 C的超类。
简而言之,他们说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 你可以认为 C 是 T 的生产者,而不是 T 的消费者。

还是水果篮和水果的例子,我定义了一个水果篮类,构造方法传入了T的示例,仅提供了get方法:

class Basket2<out T>(private val content: T) {

    fun get(): T {
        return content
    }
}

那么我们之前在java中不能实现的泛型型变,现在就是ok的了:

var basketFruit: Basket2<Fruit> = Basket2(Fruit())
var basketApple: Basket2<Apple> = Basket2(Apple())
 //ok的 符合协变的规则
basketFruit = basketApple
in: (逆变注解)消费者:。它使得一个类型参数逆变:只可以被消费而不可以被生产,我们以Compareble为例:
interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    // 我们可以将 x 赋给类型为 Comparable <Double> 的变量
    val y: Comparable<Double> = x // OK!因为 y可以接受Double或者它的任意父类,即“逆变了”
}

我们再回到篮子和水果的例子,我定义一个水果篮类,用in修饰:

class Basket3<in T> {

    fun set(param: T) {
        println(param)
    }
}

那么我们现在可以逆变了:

var basket3Apple = Basket3<Apple>()
var basket3Fruit = Basket3<Fruit>()
//ok的 符合逆变
basket3Apple = basket3Fruit
结论:
  • 如果泛型T(或其他字母)只出现在该类的返回值中声明,那么该泛型形参即可使用out修饰
  • 如果泛型T(或其他字母)只出现在该类的方法的形参声明中,那么泛型形参可使用int修饰
Kotlin使用处型变:类型投影

声明时型变虽然方便,但它有一个限制:要么该类的所有方法都只用泛型声明返回值类型(此时可用out声明型变):要么所有方法都只用泛型声明形参类型(此时可用in声明型变)。如果一个类中有 的方法使用泛型声明返回值类型,有的方法使泛型声明形参类型,那么该类就不能使用声明处型变。典型的例子就是Kotlin 的Array类,它无法使用声明处型变,该类在T 上既不能协变也是不能逆变的。

class Array<T>(val size: Int) {
    fun get(index: Int): T { …… }
    fun set(index: Int, value: T) { …… }
}

假如写下了如下方法,把一个数组复制到另外一个数组:

fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

尝试着按照这种方式调用:

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 
copy(ints, any)
//   ^ 其类型为 Array<Int> 但此处期望 Array<Any>

再次回到老问题:T是不型变的,因此 Array和Array都不是彼此的子类,如果在from参数中要求Sting,我实际却传入了int就会报ClassCastException,那么我们想避免这样的事情发生我们可以这么做:

fun copy(from: Array<out Any>, to: Array<Any>) { …… }

我们说from不仅仅是一个数组,而且是一个受限制(投影)的数组,我们只可以调用返回类型为T的方法,上面我们就只能调用get()。这便是我们的使用处型变的方法。

再例如:
我定义一个类型为Number的Array:

var numArr: Array<Number> = arrayOf(1,2,3,4,5)
numArr.set(0,2)//1.正常
var intArr:Array<Int> = arrayOf(1,2,3)//2.正常
numArr = intArr//3.报错 不支持声明处型变

那么我现在在Number上面加上一个out

var numArr: Array<out Number> = arrayOf(1,2,3,4,5)
numArr.set(0,2)//1.报错
var intArr:Array<Int> = arrayOf(1,2,3)//2.正常
numArr = intArr//3.正常

我用out修饰了Number,意味着它可以接受协变,代价就是只能出不能添加。

上面的例子中 out 的定义就叫类型投影

依然以Array为例:

我写一个填充到数组的方法,指定类型为String

fun fill(dest: Array<String>,value:String){
    if(dest.size>0){
        dest[0] = value
    }
}

此时我这么调用就会报错:

var arr1:Array<CharSequence> = arrayOf("a","b",StringBuilder("test"))
fill(arr1,"test") //报错
println(arr1.contentToString())

此时我在声明处添加 in ,表示可以接受String的任何父类,就可以编译通过了:

fun fill(dest: Array<in String>,value:String){
。。。。。。
}

再例如,刚刚上面的的例子:

var intArr:Array<Int> = arrayOf(1,2,3)
var number:Array<Number> = arrayOf(1,2,3)
intArr = number //报错:不支持声明时逆变

我们加上这个限制以后,就能逆变了:

var intArr:Array<in Int> = arrayOf(1,2,3)
星投影

表示不知道类型实参的任何信息

var list:Array<*> =  arrayOf("test","kotiln",1,2)
list[0]="1"//报错 无法被写入

所以:

  • 星号投影不能写入,只能读取
  • <*>等价于java中的<?>
设定类型形参上限
单个形参

kotlin不仅允许在使用通配符时设定形参上限,而且可以在定义类型形参时设定上限,用于表示给该类型的实际类型要么是该上限类型,要么是它的子类。

回顾一下上面篮子的例子

class Basket2<out T>(private val content: T) {

    fun get(): T {
        return content
    }
}

我们改一改,让它只能放水果:

class Basket2<T:Fruit>(private val content: T) {

    fun get(): T {
        return content
    }
}

乍一看,它们好像没有什么区别。。。。

var basket2Fruit: Basket2<Fruit> = Basket2(Fruit())
var basket2Apple: Basket2<Apple> = Basket2(Apple())

以上两行代码在两种修饰符下都可以执行,但是,我们知道,out是可以让泛型协变的,即:

basket2Fruit = basket2Apple //out ok ,设定形参上限报错

用out是ok的,代价是只能作为生产者输出了,而用形参上限,我们却可以跟它提供一个set方法:

class Basket2<T : Fruit>(private var content: T) {

    fun set(fruit: T) {
        content = fruit
    }

    fun get(): T {
        return content
    }
}

这样我们就能保证这个篮子中只能放入Fruit和它的子类了,也能从里面取出Fruit,但是此时,它不能型变。

多个形参

kotlin允许为类型设定多个形参上限,在尖括号外用 where语句:
先定义两个接口或者父类:

interface Eatable {
    fun eat()
}

interface Color

如果想限定参数的必须实现上面两个接口可以这么写:

class Basket<T> where T : Eatable, T : Color {
。。。。。。
}

  • 对于泛型的使用如果我们没有型变需要,有存有取,可以优先使用形参上限来限制参数。
具体化类型参数

kotlin允许在内联函数(inline修饰)使用refied修饰泛型参数,这样可将泛型参数变成一个具体的类型参数,很适用于我们需要用Class做参数的情形:
例如,我们要从某个List找某个指定类型的元素:

fun<T> findData(clazz:Class<T>):T?{
    .....
}
//使用
findData(Integer:class.java)

那么这么写就能省略class参数了:

fun inline <refied T> findData():T{
    .....
}
//使用
findData<Int>()

是不是优雅许多?

参考资料:

https://www.kotlincn.net/docs/reference/generics.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值