您的位置:首页 > 移动开发 > Android开发

Kotlin学习(十一): 泛型(Generics)

2017-10-08 18:07 387 查看


泛型,即“参数化类型”,顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

接下来学习Kotlin中的泛型,Kotlin的泛型为类型安全提供保证,相比与Java更安全。

泛型

在Java中经常会用到泛型:

class Box<T>{
private T var ;
}
// 使用
Box<String> box = new Box<String>();


与Java一样,Kotlin中也用到了泛型:

class Box<T>(t: T) {
var value = t
}
// 使用
val box: Box<Int> = Box<Int>(1)


与Java不一样的是,Kotlin在可以推断出参数的情况下,如在构造函数参数上推测出等等,可是省去类型,如上面创建的实例传入的1是
Int
类型:

val box = Box(1)


而在Java中是不能这样做。

变异(Variance)

在这里我们先来了解下Java泛型中的通配符,在Effective Java中Item 28中写到: Use bounded wildcards to increase API flexibility,意思是使用通配符为了提高API的使用灵活性。

Java中使用
?
来表示通配符:

public static <T extends Comparable<? super T>> void sort(List<T> list) {
Object[] array = list.toArray();
Arrays.sort(array);
int i = 0;
ListIterator<T> it = list.listIterator();
while (it.hasNext()) {
it.next();
it.set((T) array[i++]);
}
}


在Java中,泛型是不可变的,如
List<String>
不是
List<Object>
的子类:

List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // 这是错误的,类型不匹配


如果上面的操作是正确的,那么会在使用时造成类型不匹配的问题:

objs.add(1); // 添加一个int类型到String的List里面
String s = strs.get(0); // 会报ClassCastException: Cannot cast Integer to String的错误


所以Java的泛型会被设计成不可变的类型,就是为了确保运行时类型安全,但是这样同样会带来一些影响。

举个例子,定义一个泛型接口
Collection
,里面有
addAll()
方法:

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


由于Java的泛型是不可变的,所以下面的代码是做不到的:

void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); // 这是错误的,Collection<String>不是Collection<Object>的子类
}


为了解决上面的问题,Java中使用了类型通配符方式,如
? extends T
表示
T
T
的子类参数都可以使用,所以
Collection
addAll()
方法是这样写的:

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


同样的原理,在上面的代码可以改成这样:

List<String> strs = new ArrayList<String>();
strs.add("0");
objs.add("1");
List<? extends Object> objs = strs; // OK


PECS

PECS stands for Producer-Extends,Consumer-Super,具体的可以参考PECS

- 通配符上界,只能从中读取元素,不能添加元素,称为生产者(Producers),用
<? extends T>
表示。

- 通配符下界,只能添加元素,不能直接读取下界类型的元素,称为消费者(Consumers),用
<? super T>
表示。

通配符上界

<? extends T>
(
T
表示通配符的上界),表示可以接收
T
以及
T
的子类参数,也就是说可以安全的读取到
T
的实例,事实上所有的集合元素都是
T
的子类的实例,但不能向其添加元素,因为没法确定添加的实例类型跟定义的类型是否匹配,举个栗子:

List<String> strs = new ArrayList<String>();
strs.add("0");
strs.add("1");
List<? extends Object> objs = strs;
// 上面说过这样是可以
objs.get(0); // 可以获取


// 但是再添加一个int类型的话
objs.add(1); // 报错




// 再添加一个String类型
objs.add("1"); // 同样会报错




上面的例子说明了
objs
可以读取值,但是再往
objs
里面添加值的时候,就会出错,没法确定添加的实例类型跟定义的类型是否匹配。

这种
wildcard
是通过继承一个范围类(extends-bound),也就是通配符上界(upper bound)来实现类型协变。

通配符下界

那么有通配符上界
<? extends T>
,自然就会有下界,
<? super T>
,其中
T
就表示通配符的下界。

举个栗子:
Collection<? super String>
Collection<String>
的父类型,所以可以直接
add
set
,但是
get
的时候获取到的类型是Object而不是String类型。

List<String> strs = new ArrayList<String>();
strs.add("0");
strs.add("1");
List<? super String> objs = strs;
objs.add("1");
objs.set(0, "2");
Object s = objs.get(0);


在Kotlin中,并没有上面的机制,而是通过
Declaration-site variance
Type projections
来执行的。

声明位置变异(Declaration-site variance)

声明位置变异:通过将参数
T
注解成只能作为返回值,不能作为传入参数;使用
out
关键字标识。

首先我们来看一下,在Java中,

interface Source<T> {
public T nextT();
}
public void demo(Source<String> strs){
Source<Object> objs = strs; // 在Java中是不允许的
// 正确方式为
// Source<? extends Object> objs = strs;
}


在Kotlin中,使用声明位置变异来解决这种问题:

abstract class Source<out T> {
// 使用out的话,T只能作为返回值
abstract fun nextT(): T
// 不能作为传入参数,下面会报错
// abstract fun add(value: T)
}

fun demo(strs: Source<String>) {
val objects: Source<Any> = strs
}


out
就有
in
in
out
互补,它使类型参数逆变
contravariant
,只能作为传入参数,不能作为返回值:

abstract class Source<in T> {
// 使用in的话,只能作为传入参数,不能作为返回值
// abstract fun nextT(): T
abstract fun add(value: T)
}

fun demo(strs: Source<Number>) {
val objects: Source<Double> = strs // Double是Number的子类型
}


总结一下,当一个泛型类
C
,包含
out
关键字的时候,等同于Java的
extends
,将类
C
称为
T
的协变类,
T
只能作为该类中函数的返回类型,不能作为参数传递进来,也可以称类
C
T
的生产者(Producer)。

同理,当包含
in
关键字的时候,等同于Java的
super
,将类
C
称为
T
的逆变类,
T
只能作为该类中函数的参数传递进来,不能作为返回类型,也可以称类
C
为T的消费者(Consumer)。

可以将上面两段话总结成:

Consumer in, Producer out!

fun copy(from: Array<out String>, to: Array<in String>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
// 等同于
public void copy(List<? extends String> from, List<? super String> to) { ... }


Star-projections

在Kotlin中,定义一个该泛型类型的推测
projection
类型,使泛型类型的每一个具体实例应该是推测
projection
类型的子类型,称之为Star-projections,语法为:

- 对于
Foo<out T>
T
是一个协变类型参数,可以知道
Foo<*>
是与之相等的,当
T
不可知时,可以安全的读取到
T
,可以通过
Foo<*>
得到
T


- 对于
Foo<In T>
T
是一个逆变类型参数,可以知道
Foo<*>
等同于
Foo<in Nothing>
,当
T
不可知时,不能在
Foo<*>
里面添加元素。

- 对于
Foo<T>
T
是一个不变类型参数,当从中读取数据时
Foo<*>
同等于
Foo<out T>
;当向其添加数据时,
Foo<*>
等同于
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?>


泛型函数(Generic functions)

Kotlin同样支持泛型函数:

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

fun <T> T.basicToString() : String {  // extension function
// ...
}


使用的时候,在函数名称后面指定具体的类型参数:

val l = singletonList<Int>(1)


泛型约束(Generic constraints)

Upper bounds

Kotlin的泛型约束和类的继承一样,使用
:
代替
extends
对泛型的的类型上界进行约束:

class SwipeRefreshableView<T : View>{}


同时Kotlin支持多个类型的上界约束,,使用
where
关键字:

class SwipeRefreshableView<T>
where T : View,
T : Refreshable {
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android kotlin Generics