概述:在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 和 strings 的集合,并把string的类型赋值给object此时会报错,这意味着 List<String> 并不是List<Object> 的子类型,把我们调用addAll()把string加入到object中,编译通过,查看addAll的源码:
//创建object的集合 ArrayList< Object> objects = new ArrayList<>(); objects.add(str); //那创建List<String> ArrayList< String> strings = new ArrayList<>(); objects=strings;
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<*>。