欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

kotlin之泛型的使用

程序员文章站 2022-03-12 22:33:28
...

泛型

我们最先了解到的泛型应该是来自于Java,在Java SE 1.5的时候,首次提出了泛型的概念,泛型的本质是参数化的类型,也就是说传递操作的数据类型被指定为一个参数,泛型可以被应用于类(泛型类)、接口(泛型接口)、方法(泛型方法)。Java引入泛型的好处就是安全简单。在Java SE 1.5之前,没有泛型的情况下,对参数的“任意化”是通过Object的引用来实现的,然而用这种方式去实现参数的任意化的缺点是总是要进行强制类型转换,这种转换是要求开发者对实际参数类型可以预知的情况下进行的,然而随着项目的规模、参与人员的数量慢慢增加,要做到对每个实际参数类型都可以预知几乎不太可能,而强制类型转换错误在编译期不会报错,只有在运行时会抛出类型转换的异常,这让程序变得不稳定且不可控制,所以泛型的引入解决了这些问题。

下面首先通过一段Java代码了解一下泛型:

public interface List<E> extends Collection<E>{
    ...
    boolean add(E e);
    ...
}
复制代码

上面这段代码是来自于Java的集合List的源码,可以看到List是一个泛型接口,一般情况下当我们想要初始化一个list的时候,应该都是这么写: List<Integer> list = new ArrayList<>(); 当想要向集合中添加一个元素的时候,只需要调用list.add()方法就可以了。那么假如我创建的是一个int的集合那么调用add()方法的时候传的参数就应该是int,那如果创建了一个String的集合那么add()的参数就应该是String。那么设想一下,如果没有泛型的存在的话,我们需要写两个参数不同的add()方法,那么对应每一种数据类型就需要多写一遍,显然不是一个很好的实现方法。

但是当我们运用泛型的时候,比如:List<Integer> list = new ArrayList<>(); 通过源码可以看到,我们将集合List的泛型参数E设置为Integer,那么此时对于list来说,add()方法内的参数E就是Integer类型。同理如果创建语句为:List<String> list = new ArrayList<>(); 那么此时E类型就会是String。

Kotlin泛型

上面对泛型做了一个简单的解释,举例说明了泛型最简单的用法,实际上远远不止于此。kotlin中的泛型定义与Java中类似,在类名之后声明:

class Box<T>(t: T){
    val vlaue = t
}

//在创建box对象的时候声明了泛型参数为Int
val box: Box<Int> = Box<Int>(1)

//一般来说,如果类型是能够被推断出来的,我们也可以省去声明泛型参数的步骤
val box = Box(1)
复制代码

对于Java来说,常见的泛型使用中,通配符也是最为常见的一种方式,比如有 ?、? extends Number、? super Integer等等。那么T和通配符之间有什么区别呢?实际上T更多的是表示了一个限定约束,如声明了 “T” 类,那么该泛型类中用 “T”声明的对象就必须是T类型,如上述List中的E;如果是 ? 就表示该类型可以是任意的类型,并不会起到限定约束作用。

kotlin中并没有上述Java中的通配符类型,如Java中用 ? extends Number 表示了参数的上界,只能是Number的子类型,用 ? super Integer 说明了参数的下界,只能是Integer的超类型。这样我们可以在使用通配符的时候也对参数进行一个约束,然而kotlin中抛弃了这一个概念,在kotlin中,类似的概念称之为生产者和消费者。

生产者:只能读取数据的对象

消费者:只能写入数据的对象

之所以会有生产者和消费者概念的引入,和泛型的型变有关,在Java中泛型是没有型变的,例如String是Object的子类,但是List<String> 并不是 List<Object > 的子类。这种设计会让我们的List变的安全,如果是可以型变的,那么将发生一些错误。比如我们看下面的代码:

List<String> strs = new ArrayList<String>();
List<Object> objs = strs; 
objs.add(1); 
String s = strs.get(0);
复制代码

如果Java是可以型变的,那么上述代码将会编译通过,然而我们最后是想得到一个String, 但是却向List内写入了一个int型的数据,这在运行时会发生ClassCastException(类型转换异常)而导致程序crash。

通过Java的上述特性,可以考虑一下集合List的addAll()方法的参数,顺理成章的应该是类似下面这样:

boolean addAll(Collection<E> c);
复制代码

但如果addAll方法是上述那样的话,当编写了如下代码的时候:

void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from); 
}
复制代码

这个操作看上去很安全,但是编译器会报错:Collection<String> 不是 Collection<Object> 的子类型,因此实际上集合的源码如下:

public interface List<E> extends Collection<E>{
    ...
    boolean add(E e);
    boolean addAll(Collection<? extends E> c);
    ...
}
复制代码

我们可以看到,addAll()方法的参数是Collection<? extends E> 而不是Collection<E>, 通过指定通配符参数的上界来使得向Collection<Object>中添加Collection<String>变得合法。

因此当对于一个集合Collection<A> , 从中读取一个元素,他可能是A类型,也可能是A类型的子类。这种情况被称为协变;反之,如果向一个集合Collection<A> 写入元素时,可以写入A类型,也可以写入A类型的超类,这种情况被称为逆变。(举例说明:如果只需要读取的话,那么我们可以从一个String的集合中读取Object,这种操作是安全的; 如果需要写入,那么我们应当向一个Object的集合内写入String,而不是像一个Number的集合内写入Object。)

在kotlin中,用out和in来表示生产者和消费者的行为,一言以蔽之:out T 表示 Java 中的 ? extends T, in T 表示Java中的 ? super T。 * 用来表示Java中的 ?(通配符)

Kotlin声明处型变

Kotlin 对 Java 泛型的一项改动就是添加了声明处型变。看下面的例子:

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

void demo(Source<String> str) { 
// Java 中这种写法是不允许的 
Source<Object> obj = str; 
...
} 

复制代码

因为 Java 泛型是不型变的,Source<String> 不是Source<Object> 的子类型,所以不能把 Source<String> 类型变量赋给 Source<Object>类型变量。

现在用 Kotlin 改写上面的接口声明:

interface Source<out T> { 
T nextT(); 
} 
复制代码

我们在接口的声明处用 out T 做了生产者声明,因为这个接口只有一个读取数据的 nextT() 方法,可以视为生产者。把这个接口的类型参数声明为生产者后,就可以实现安全的类型协变了:

fun demo(Source<String> str) { 

val obj: Source<Any> = str // 合法的类型协变 
} 
复制代码

Kotlin 中有大量的声明处协变,比如 Iterable 接口的声明:

public interface Iterable<out T> { 
public operator fun iterator(): Iterator<T> 
} 
复制代码

因为 Collection 接口和 Map 接口都继承了 Iterable 接口,而 Iterable 接口被声明为生产者接口,所以所有的 Collection 和 Map 对象都可以实现安全的类型协变:**

val c: List<Number> = listOf(1, 2, 3) 
复制代码

这里的 listOf() 函数返回 List<Int> 类型,因为在kotlin中List <out T>接口实现了安全的类型协变,所以可以安全地把 List<Int> 类型赋给 List<Number>类型变量。 在kotlin中List是指可以读不可以写的,因此上述代码是安全的。(可读写的List为MutableList<E>)

使用处型变:类型投影。

考虑之前讲到的在声明处型变,将T设置为生产者out T,可以使其安全的产生型变。然而有些类我们不能够限制它就返回T。比如一个Array:

class Array<T>(val size: Int) {
    fun get(index: Int): T { …… }
    fun set(index: Int, value: T) { …… }
}
复制代码

可以看到其中set方法,并不会返回T,我们无法将其设置为生产者。那么当我们去对两个数组进行copy操作的时候:

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)
复制代码

显然上述代码是错误的,因为它在尝试将一个String的Array赋值给Int的Array。因此可以将copy方法的from数组设置为生产者,如下:

fun copy(from: Array<out Any>, to: Array<Any>)
复制代码

这个时候,我们只能调用from的返回值为T的方法,即get()方法,这时to数组将不会被写入到from中去,这可以被称为是使用处型变,也可以称为类型投影(因为此时的from数组就像是一个受到限制的Array<Any>)。