Kotlin学习(十一)——泛型

Kotlin和泛型一样都有类型参数
泛型类:

class One <T>(var i:T){ //泛型类
    fun foo(i:T){
        println(i)
    }
}

创建泛型类的对象:

fun main(args: Array<String>) {
    var one1 = One<Int>(1)
    var one2 = One(1) //如果可以推断出类型,允许省略类型参数
}

泛型方法:

fun <T> showNumber(t:T){
    println(t)
}
fun main(args: Array<String>) {
    showNumber(2)
}

泛型接口:

interface Sub<T>{
    fun add(a : T, b : T) : T
}

fun foo(){
    println( object :Sub<Int>{
        override fun add(a: Int, b: Int): Int = a + b
    })
}

型变

小编理解的型变:类型变化,类型不是一定的
在Java中:
Java 类型系统中最棘⼿的部分之⼀是通配符类型。⽽ Kotlin 中没有。相反,它有两个其他的东西:声明处型变(declarationsite variance)与类型投影(type projections)

⾸先,让我们思考为什么 Java 需要那些神秘的通配符。在 Effective Java 解释了该问题⸺第28条:利⽤有限制通配符来提升 API 的灵活性。⾸先,Java中的泛型是不型变的,这意味着 List<String> 并不是 List<Object> 的⼦类型。为什么这样?如果 List 不是不型变的,它就没⽐ Java 的数组好到哪去,因为如下代码会通过编译然后导致运⾏时异常:

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!即将来临的问题的原因就在这⾥。Java 禁⽌这样!
objs.add(1); // 这⾥我们把⼀个整数放⼊⼀个字符串列表
String s = strs.get(0); // !!! ClassCastException:⽆法将整数转换为字符串

上面的第二行代码小编在IDEA和Eclipse中测试过,不能编译通过。上面的代码就说明什么是型变。

因此,Java 禁止这样的事情以保证运⾏时的安全。但这样会有一些影响。例如,考虑 Collection 接口中的 addAll() 方法法。该方法的签名应该是什么?直觉上,我们会这样:

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

但随后,我们将无法做到以下简单的事情(这是完全安全):

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
    to.addAll(from); // !!!对于这种简单声明的 addAll 将不能编译:
    // 因为Collection<String> 不是 Collection<Object> 的⼦类型
}

(在 Java 中,我们艰难地学到了这个教训,参⻅Effective Java,第25条:列表优先于数组)
在Java中Collection接口中addAll()方法声明是想下面这样的:

//Java
boolean addAll(Collection<? extends E> c); //为了加入集合时,我们能加入其子类类型化的集合

通配符类型参数 ? extends E 表⽰此⽅法接受 E 或者 E 的 ⼀些⼦类型对象的集合,⽽不只是 E ⾃⾝。这意味着我们可以安全地从其中(该集合中的元素是 E 的⼦类的实例)读取 E ,但不能写⼊,因为我们不知道什么对象符合那个未知的 E 的⼦类型。反过来,该限制可以让 Collection<String> 表⽰为 Collection<? extends Object> 的⼦类型。简⽽⾔之,带 extends 限定(上界)的通配符类型使得类型是协变的(covariant)

注意以上的最后一句:带 extends 限定(上界)的通配符类型使得类型是协变的(covariant)
其实小编理解就是子类类型变为更为大的“父”类(向“根”转变),比如

    ArrayList<Number> l1 = new ArrayList<>();
    ArrayList<Integer> l2 = new ArrayList<>();
    l2.add(1);
    l1.add(4.53);
    l1.add(6.3f);
    l1.addAll(l2); //这时候l2就型变为Number类了,这就是协变,变得“更大”(向“根”变化)了
    System.out.println(l1);

理解为什么这个技巧能够⼯作的关键相当简单:如果只能从集合中获取项⽬,那么使⽤ String 的集合,并且从其中读取 Object 也没问题 。反过来,如果只能向集合中 放⼊ 项⽬,就可以⽤ Object 集合并向其中放⼊ String :在 Java 中有 List<? super String>List<Object> 的⼀个超类。

后者称为逆变性(contravariance),并且对于 List <? super String> 你只能调⽤接受 String 作为参数的⽅法(例如,你可以调⽤add(String) 或者 set(int, String) ),当然如果调⽤函数返回 List<T> 中的 T ,你得到的并⾮⼀个 String ⽽是⼀个 Object
注意:带 super 限定(下界)的通配符类型使得类型是逆变的(contravariance)
小编的理解:超类向子类变化称之为逆变(向“枝”变化)
然后总结一下:协变是向上变化,逆变是向下变化

Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写⼊的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输⼊参数上使⽤通配符类型”,并提出了以下助记符:
注意:生产者只能读,消费者只能写。
助记符 :PECS 代表⽣产者-Extens,消费者-Supe(r Producer-Extends, Consumer-Super)

注意:如果你使⽤⼀个⽣产者对象,如 List

//Java
public void test(List<? extends Integer> list){
        list.add(new Integer(13)); //编译错误
        list.set(0,13); //编译错误
    }

声明处型变

假设有⼀个泛型接口Source ,该接⼝中不存在任何以 T 作为参数的⽅法,只是⽅法返回 T 类型值:

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

那么,在 Source 类型的变量中存储 Source 实例的引⽤是极为安全的⸺没有消费者-⽅法可以调⽤。但是 Java 并不知道这⼀点,并且仍然禁⽌这样操作:

// Java
void demo(Source<String> strs) {
    Source<Object> objects = strs; // !!!在 Java 中不允许
}

为了修正这⼀点,我们必须声明对象的类型为 Source<? extends Object> ,这是毫⽆意义的,因为我们可以像以前⼀样在该对象上调⽤所有相同的⽅法,所以更复杂的类型并没有带来价值。但编译器并不知道。

在 Kotlin 中,有⼀种⽅法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source 的类型参数 T 来确保它仅从 Source 成员中返回(⽣产),并从不被消费(不能写入,如调用add()或者set())。为此,我们提供 out 修饰符

abstract class Source<out T> {
    abstract fun nextT(): T
} 
fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 这个没问题,因为 T 是⼀个 out-参数
    //Source<Any>安全的作为Source<String>的超类
}

⼀般原则是:当⼀个类Source的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出-位置,但回报是 Source<Any> 可以安全地作为Source<String> 的超类。

简⽽⾔之,他们说类 Source 是在参数 T 上是协变(能向上变化)的,或者说 T 是⼀个协变的类型参数。你可以认为 SourceT 的⽣产者,⽽不是 T 的消费者。out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以我们讲声明处型变。这与 Java 的使⽤处型变相反,其类型⽤途通配符使得类型协变。另外除了 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 拥有类型 Double,它是 Number 的⼦类型
    // 因此,我们可以将 x 赋给类型为 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) { ///* …… */ }
}

该类在 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<Any>, Array<Any>)

这⾥我们遇到同样熟悉的问题:Array 在 T 上是不型变的,因此 Array 和 Array 都不是另⼀个的⼦类型。为什么?再次重复,因为 copy 可能做坏事,也就是说,例如它可能尝试写⼀个 String 到 from ,并且如果我们实际上传递⼀个 Int 的数组,⼀段时间后将会抛出⼀个ClassCastException 异常。
那么,我们唯⼀要确保的是 copy() 不会做任何坏事。我们想阻⽌它写到 from ,我们可以:

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

这⾥发⽣的事情称为类型投影:我们说 from 不仅仅是⼀个数组,⽽是⼀个受限制的(投影的)数组:我们只可以调⽤返回类型为类型参数 T 的⽅法,如上,这意味着我们只能调⽤ get() 。这就是我们的使⽤处型变的⽤法,并且是对应于 Java 的 Array

fun fill(dest: Array<in String>, value: String) {
    //dest只能写入
}

Array<in String> 对应于 Java 的 Array<? super String> ,也就是说,你可以传递⼀个 CharSequence 数组或⼀个 Object 数组给fill() 函数。
下面小编写了一些代码,用到了型变和投影

class Source<out T> {

    fun nextT(list: ArrayList<in T>,index: Int):T {//这里用了out声明处形变,还有in投影
        var array  = list.toArray()
        //return array[index+1] as T
        return list[index+1] as T
    }
}

fun main(args: Array<String>) {
    var s = Source<Int>()
    var arr  = arrayListOf(1,2,3,4,5,5)
    println(s.nextT(arr,3))

}

星投影

有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。这⾥的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。
Kotlin 为此提供了所谓的星投影语法:
1. 对于 Foo <out T> ,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper> 。这意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。
2. 对于 Foo <in T> ,其中 T 是⼀个逆变类型参数,Foo <*> 等价于 Foo <in Nothing> 。这意味着当 T 未知时,没有什么可以以安全的⽅式写⼊ Foo <*>
3. 对于 Foo <T> ,其中 T 是⼀个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> ⽽对于写值时等价于Foo<in Nothing>

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。例如,如果类型被声明为 interface Function <in T, out U> ,我们可以想象以下星投影:
1. Function<*, String> 表⽰ Function<in Nothing, String>
2. Function<Int, *> 表⽰ Function<Int, out Any?>
3. Function<*, *> 表⽰ Function<in Nothing, out Any?>
注意:星投影⾮常像 Java 的原始类型,但是安全。

上界

最常⻅的约束类型是与 Java 的 extends 关键字对应的 上界

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

冒号之后指定的类型是上界:只有 Comparable 的⼦类型可以替代 T 。

sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的⼦类型
sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的⼦
类型

下面是小编写的代码

fun <T:Number> add(a:T,b:T):T{
    return (a.toDouble() + b.toDouble()) as T 
    //T的上界就是Number
}

默认的上界(如果没有声明)是 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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值