聊一聊Kotlin的泛型

Posted 思忆(GeorgeQin)

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊一聊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:

意味着从泛型中取出(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).

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

我们可以往漏斗中放入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

以上是关于聊一聊Kotlin的泛型的主要内容,如果未能解决你的问题,请参考以下文章

java泛型中的下限

JAVA-初步认识-常用对象API(集合框架-泛型-泛型限定-下限)

java 泛型的上限与下限

java泛型上限下限,通配符

java泛型上限下限,通配符

泛型的上限和下限的Demo