Kotlin系列四-----泛型

概述:在java中泛型的方便和灵活之处,相信广大程序员深有体会,泛型的使用为代码的封装提供了无限可能,好的东西自然要保存下去,在Kotlin中同样提供了泛型的使用,而且扩展了其功能,并简化了使用方式,本篇文章就从自己学习的角度,对java和kotlin中的泛型进行简单的总结,以便于更好的理解泛型的使用,下面开始学习吧,与 Java 类似,Kotlin 中的类也可以有类型参数,这可能是泛型最基本的使用了吧:

class Box<T>(t: T) {
    var value = t
}
创建一个类Box声明其接受T类型的参数,此时T为泛型,在使用时再具体指定传入参数的类型,现在来创建对象:

val box: Box<Int> = Box<Int>(1)
这样的创建方式,大家都见过这里不再过多叙述,有一点在前面创建对象时提到过的,就是在kotlin中,若程序能能推断出参数的类型,允许省略参数类型:

val box = Box(1)
上面的简单例子让大家了解下kotlin中泛型的基本使用,下面我们通过与java中的对比进行更细致的学习。

一、协变

Java 中的泛型是不型变的,举个例子:String 是Object的子类,但 List<String> 并不是List<Object> 的子类型,我们进一步的尝试java中哪些情况下的是型变的
//创建object的集合
ArrayList< Object> objects = new ArrayList<>();
objects.add(str);
//那创建List<String> 
ArrayList< String> strings = new ArrayList<>();
objects=strings;
分别创建了object 和 strings 的集合,并把string的类型赋值给object此时会报错,这意味着 List<String> 并不是List<Object> 的子类型,把我们调用addAll()把string加入到object中,编译通过,查看addAll的源码:

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}
这里参数并不是传入E而是传入? extends  E,通配符类型参数 ? extends E 表示此方法接受 E 或者 E 的 一些子类型对象的集合,而不只是 E 自身。 这意味着我们可以安全地从其中(该集合中的元素是 E 的子类的实例)读取 E,但不能写入, 因为我们不知道什么对象符合那个未知的 E 的子类型。 反过来,该限制可以让Collection<String>表示为Collection<? extends Object>的子类型。 简而言之,带 extends 限定(上界)的通配符类型使得类型是协变的(covariant)。

现在我们修改上面的object集合:

ArrayList<? extends Object> objects = new ArrayList<>();
objects=strings; 编译通过 OK
此时在向其添加集合就会编译错误,即只允许读取其中的object属性,不允许写入数据:

//此时以下写入的代码报错:
objects.addAll(integers);
objects.addAll(strings);
objects.add(str);
//表示 无法添加 集合,
可以读取 Object o  = objects.get(0);
因为从中可以读取到object的对象,但不知道究竟是object的那个子类故无法添加对象,以上是利用java中已经存在的集合,对型变的通配符的简单介绍,下面我们看看泛型在创建类的时候的使用:
public class TypeClass<T > {
}
接下来我们同样创建对象:
TypeClass<String> stringTypeClass = new TypeClass<>();
TypeClass<Object> objectTypeClass = stringTypeClass;//报错

TypeClass<String> stringTypeClass = new TypeClass<>();
TypeClass<? extends String> objectTypeClass = stringTypeClass;//通过

Kotlin 中的协变:

声明处协变:
   为了修正这一点,我们必须声明对象的类型为 <? extends Object>,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 类的类型参数 T 来确保它仅从 <T> 成员中返回(生产),并从不被消费。 为此,我们提供 out 修饰符:
class TypeClass<out T> {
}

var string = TypeClass<String>()
var any : TypeClass<Any> = string
标记为out 的泛型T,在创建的对象为Any时,可以接受其子类的类型。 另外除了  out ,Kotlin 又补充了一个型变注释: in 。它使得一个类型参数 逆变 :只可以被消费而不可以 被生产:
java中的逆协变:
TypeClass<Object> stringTypeClass = new TypeClass<>();
TypeClass<? super String> typeClass = stringTypeClass;
typeClass可以接受其父类的对象
kotlin中的逆协变:
class TypeClass<in T> {
}

var any = TypeClass<Any>()
var String : TypeClass<String> = any
类型协变:
将类型参数 T 声明为 out 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回 T! 
接着上面的例子。考虑一种情况:在Java中
private void action(){
    ArrayList<String> from = new ArrayList<>();
    ArrayList<Object> to = new ArrayList<>();
    copy(from,to);//报错:因为from中的Object 不协变
}

private void copy(ArrayList<Object> from ,ArrayList<Object> to){

}
此时,并不能将ArrayList中使用声明处协变,所以解决的办法是在使用的时候,在copy方法中协变,修改如下:
private void copy(ArrayList<? extends Object> from ,ArrayList<Object> to){
}
此时from协变可以接受其子类型,在Kotlin中的情况基本一致:
fun action(){
    var from : Array<Int> = arrayOf(3)
    var to : Array<Any> = arrayOf("B")
    copy(from,to)
}

//此处使用out 声明使用时协变
fun copy(from : Array<out Any> ,to :Array< Any>){
 }
Array <T>     T   上是 不型变的 ,因此   Array <Int>     Array <Any>   都不是另一个的子类型。为什么? 再次重复,因为 copy   可能 做坏事,也就是说,例如它可能尝试 一个 String 到   from , 并且如果我们实际上传递一个   Int   的数组,一段时间后将会抛出一个   ClassCastException   异常。
那么,我们唯一要确保的是   copy()   不会做任何坏事。我们想阻止它   from,使用:from : Array<out Any> ,这里发生的事情称为类型投影:我们说from不仅仅是一个数组,而是一个受限制的(投影的)数组:我们只可以调用返回类型为类型参数 T 的方法,如上,这意味着我们只能调用 get()。这就是我们的使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>、 但使用更简单些的方式。
你也可以使用   in   投影一个类型:
fun fill(dest: Array<in String>, value: String) {
// ……
}
Array<in String>   对应于 Java 的   Array<? super String> ,也就是说,你可以传递一个   CharSequence   数组或一个   Object   数组给   fill()   函数。
星投影

有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。

Kotlin 为此提供了所谓的星投影语法:

  • 对于 Foo <out T>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。
  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 这意味着当 T未知时,没有什么可以以安全的方式写入 Foo <*>
  • 对于 Foo <T>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>,我们可以想象以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

注意:星投影非常像 Java 的原始类型,但是安全。

泛型函数:
泛型函数在java中也是普遍的应用,泛型函数只定义其执行的方法和逻辑,同时可以取处理不同类型的数据,使用形式也比较简单:
private <T> void test(T t){
t.toString();
}
此时传入任何对象都会调用其toString的方法。
kotlin中的使用和java中基本一致,都是在修饰次之后。方法名之前声明参数类型:
fun <T> test(t : T){
    t.toString()
}
泛型约束:
虽然泛型是可以传入不同的类型,但任何事情都没有绝对的自由,所以泛型的泛也是在一定范围内的泛型,那这个范围的控制就是泛型约束,在java中的约束采用 extends 约束上限,只允许传入其本身和其子类,super限定传入其父类,在kotlin中的范围限制:
fun <T : Comparable<T>> sort(list: List<T>) {
    // ……
}
冒号之后指定的类型是上界:只有 Comparable<T> 的子类型可以替代 T。默认的上界(如果没有声明)是 Any?。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,我们需要一个单独的 where-子句:
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}
类型擦除:
在运行和编译过程中,所有泛型只会被认为是同样的对象,不再区分具体的类型数据,简单的说虽然你传入的时object的不同子类,但对于系统来说他们都只是object,擦去其自己的类型。
Kotlin 为泛型声明用法执行的类型安全检测仅在编译期进行。 运行时泛型类型的实例不保留关于其类型实参的任何信息。 其类型信息称为被擦除。例如,Foo<Bar> 与 Foo<Baz?> 的实例都会被擦除为 Foo<*>。





猜你喜欢

转载自blog.csdn.net/alexwll/article/details/79267153