Kotlin基础之泛型

泛型

与Java一样,Koltin的类也有类型参数。例如:

class Box<T>(t: T){
    var value = t
}

常规来说,创建这样的类,需要提供具体的类型。例如:

val box: Box<Int> = Box<Int>(1)

当类型可以从构造参数或其他上下文中推断出时,可以忽略类型参数。上面的代码可以简化为:

val box = Box(1)

型变

Java类型系统中最复杂的其中一个部分就是通配符类型(Java泛型FAQ)。而Kotlin没有任何的通配符类型,它使用声明处变型和类型投影两种方式替代。

通配符 - 使用问号表示的类型参数,表示未知类型的类型约束方法。

首先,先思考为什么Java需要这些难以理解的通配符。Effective Java解释了这个问题,第28条:使用受限通配符来增加API灵活性。首先,Java中泛型为不可变类型,意味List不是List的子类型。为什么这样?如果List为可变量,List<>不会比Java的数组更好,并且下面的代码能够成功编译,但在运行时会引起异常。

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // 会引起错误,Java禁止这样使用。
objs.add(1);  // Here we put an Integer into a list of Strings
String s = strs.get(0); // 类转换异常:无法将Integer转换为String

所以Java禁止这样做,目的是保证运行时安全。但会有一些影响。比如:Collection接口的addAll()方法,这个方法的签名是什么?我们直觉上会这样做:

// Java
interface Collection<E> ... {
  void addAll(Collection<E> items);
}

考虑到运行时安全,我们无法做到像下面的简单操作。

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from); // !!! Would not compile with the naive declaration of addAll:
                   //       Collection<String> is not a subtype of Collection<Object>
}

实际上,addAll()的方法签名是:

// Java
interface Collection<E> ... {
  void addAll(Collection<? extends E> items);
}

通配符参数? extends E表明方法接收类型为E的子类集合,而非E本身。意味着可以安全读取集合中为E的值(集合的元素类型为E的子类实例),但无法写入E,因为我们不知道对象是否是E未知的子类。作为交换,我们希望得到这些行为:Collection为Collection

声明处变型

假设现有一个Source泛型接口,没有使用T作为参数的方法,只有一个返回T的方法。例如:

// Java
interface Source<T> {
  T nextT();
}

那么使用Source的变型来存储Source实例引用是类型安全的(因为没有消费者方法)。但是Java仍会禁止这样做,下面的代码是不合法的。

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!! Not allowed in Java
  // ...
}

当然,可以声明Source

abstract class Source<out T> {
    abstract fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is OK, since T is an out-parameter
    // ...
}

泛型规则:当类C的泛型参数T声明为out时,表示T只能出现在C成员的输出位置,作为交换,C是C类型安全的超类。

称类C是参数T的协变量,或T是协变量类型参数。可以认为类C是T的生产者,而不是T的消费者。

out修饰符称为变型注解,因为它提供了类型参数声明点,因此称之为声明点类型。

除了out,kotlin提供了一个补充的变型注解:in。让类型参数变为逆变量:只能消费,从不生产。Comparable就是协变量一个很好的例子。

abstract class Comparable<in T> {
    abstract fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
    // Thus, we can assign x to a variable of type Comparable<Double>
    val y: Comparable<Double> = x // OK!
}

类型投影

使用处变型:类型投影 声明类型参数T为out很方便,避免在使用处子类型化。但一些类实际时无法限制只返回T,Array就是一个很好的例子:

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

Array类既不是T的协变,也不是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 , ant) // 错误:expects(Array<Any>, Array<Any>)

遇到了相同的问题:Array是不变的,T类型的数组,所以Array和Array都不是对方的子类。因为copy可能会坏事,可能会进行写操作,比如像from写入String,而实际上这里传入的是Int数组,运行时就能出现ClassCastException异常。

因此,只需要保证copy不会做坏事,禁止向from写数据,可以这样做:

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

这样做法被称为类型投影(type projection),也是说from不是一个简单数组,而是受限(投影)类型:只能够调用那些返回类型为T的方法,在这种情况意味着只能调用get,这也是使用使用出变型的目的,对应java的Array

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

Array对应Java的Array

星号投影

有时不知道类型参数任何信息,但仍希望安全地使用。此时安全地定义投影的泛型,每个泛型的具体实例都是泛型的子类型。 为此,Kotlin提供称为星号投影的语法。

  • 对于Foo,T为带有上界TUpper的协变量,Foo<>等价于Foo。意味着T类型未知时,可以安全地读取Foo<>中TUpper的值
  • 对于Foo,T为逆变类型参数,Foo<>等价于Foo,意味着当T类型未知时,无法安全写入Foo<>
  • 对于Foo,T为不可变类型参数,带有上界TUpper,Foo<*>等价于Foo用于读取和Foo用于写入值。 如果泛型有多个类型参数,则每个都可以独立投影。比如,如果类型声明为interface Function

泛型函数

不仅类可以有类型参数,函数也可以有。函数的类型参数在函数名之前声明:

fun <T> SingletonList(item: T ): List<T>{
    // ...
}

fun <T> T.basicToString() : String { // 扩展函数
    // ...
}

调用泛型函数,在调用的函数名之后指定具体类型参数。

val l = SingletonList<Int>(1)

泛型约束

所有可以被指定类型参数替代的类型,都可以使用泛型约束进行限制。

上界

最常见的泛型约束就是上界,对应java的extends关键字。

fun <T : Comparable<T>> sort(list: List<T>){
    // ...
}

在冒号之后指定的类型就是上界,只有Comparable子类才能替换T。如:

sort(listOf(1, 2, 3)) // 可以。Int是Comparable<Int>的子类
sort(listOf(HashMap<Int, String>()))  // 错误。HashMap<Int, String>不是Comparable<HashMap<Int, String>>的子类

默认上界类型为Any?。尖括号中只允许指定一个上界。可使用where条件语句指定超过一个的上界。

fun <T> cloneWhenGreater(list: List<T> , threshold: T): List<T> 
    where T : Comparable ,
          T : Cloneable {
    return list.filter{it -> threshold }.map { it.clone()}              
}

附:里氏替换原则协变与逆变泛型中的协变和逆变

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Go语言中的Array、Slice、Map和Set使用详解

Array(数组) 内部机制 在 Go 语言中数组是固定长度的数据类型,它包含相同类型的连续的元素,这些元素可以是内建类型,像数字和字符串,也可以是结构类型,元...

6505
来自专栏程序生活

Leetcode-Easy 20. Valid Parentheses

20. Valid Parentheses 描述: 判断括号是否匹配 (),{},[] ? 思路: 遍历括号字符串,添加到一个数组中,匹配...

3435
来自专栏王翔的专栏

ES6基础 数组的扩展

对于那些没有部署 Iterator 接口的类似数组的对象(如普通object),扩展运算符就无法将其转为真正的数组。

642
来自专栏Golang语言社区

Go语言中的Array、Slice、Map和Set使用详解

Array(数组) 内部机制 在 Go 语言中数组是固定长度的数据类型,它包含相同类型的连续的元素,这些元素可以是内建类型,像数字和字符串,也可以是结构类型,元...

3499
来自专栏程序员互动联盟

【编程基础】C++比C牛逼的七个点

1. 函数检测增强 ? 在C语言中,重复定义多个同名的全局变量是合法的,在C++中,不允许定义多个同名的全局变量。 C语言中多个同名的全局变量最终会被链接到全局...

3395
来自专栏猿人谷

C++ 模板学习

1. 模板的概念。 我们已经学过重载(Overloading),对重载函数而言,C++的检查机制能通过函数参数的不同及所属类的不同。正确的调用重载函数。例如,为...

24210
来自专栏aCloudDeveloper

C++基础题

刚在网上转看到几道对于巩固基础很有帮助的C++基础题,反正闲着也是闲着,就做了下,具体题型如下: 答案是我自己写,不一定对,如果有朋友看到不对的,欢迎指正,万分...

2085
来自专栏Golang语言社区

Go语言中的Array、Slice、Map和Set使用详解

Array(数组) 内部机制 在 Go 语言中数组是固定长度的数据类型,它包含相同类型的连续的元素,这些元素可以是内建类型,像数字和字符串,也可以是结构类型,元...

3258
来自专栏从流域到海域

《笨办法学Python》 第40课手记

《笨办法学Python》 第40课手记 本节课讲述的字典,一种比数组更强大的数据结构,字典(dict)的另一个名称是散列(hash)。 我将在后面具体解释dic...

1817
来自专栏信数据得永生

JavaScript 编程精解 中文第三版 六、对象的秘密

2956

扫码关注云+社区