由Java泛型看Kotlin的泛型

参考链接:

《深入理解Java泛型》
《Kotlin 泛型》
《Kotlin中文文档》
《无界通配符》
《Kotin实战》泛型章节
《Effective Java》泛型章节

什么是泛型

泛型的目的是让我们写一套代码可以支持不同的数据类型,下面我们实现一个简单的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyList{

private int size;
private Object[] elements = new Object[10];

public void add(Object item) {
elements[size++]=item;
}

public Object get(int index) {
return elements[index];
}
}

我们实现的集合可以添加任意类型的数据(因为Java中Object是所有类的超类)

1
2
3
4
5
MyList myList = new MyList();
myList.add("A");
myList.add(1);
System.out.println(myList.get(0));
System.out.println((String)myList.get(1));

但是这样的代码有着很大的缺陷,因为需要类型的强制转换,这种转换错误是在编译期间发现不了的,在Java 1.5之前是没有泛型的,就是这样做的。泛型同样可以实现这个需求,但是通过面向对象的多态特性避免了类型强转,而且在编译期间就能发现类型问题。

1
2
3
4
5
6
7
8
9
10
11
class DataHolder<T>{
T item;

public void setData(T t) {
this.item=t;
}

public T getData() {
return this.item;
}
}

泛型类定义时只需要在类名后面加上类型参数即可,当然你也可以添加多个参数,类似于<K,V>,<T,E,K>等。这样我们就可以在类里面使用定义的类型参数。

Java的泛型

泛型方法

泛型不仅仅可以像上面一样作用于整个类,还可以作用于方法,泛型方法既可以存在于泛型类中,也可以存在于普通的类中。

如果使用泛型方法可以解决问题,那么应该尽量使用泛型方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TestClass {

/**
* 泛型方法
* @param e
*/
public <E> void printerInfo(E e) {
System.out.println(e);
}

/**
* 泛型方法
* @param id
* @param <E>
* @return
*/
public <E> E findViewById(E id){
return id;
}

public static void main(String[] args){
TestClass tc = new TestClass();
tc.printerInfo(1);
tc.printerInfo("AAAAA");
tc.printerInfo("0.13145");
}
}

我们看到我们是在一个泛型类里面定义了一个泛型方法printInfo。通过传入不同的数据类型,我们都可以打印出来。

与泛型类的定义一样,此处E可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。

泛型接口

Java泛型接口的定义和Java泛型类基本相同,下面是一个例子:

1
2
3
4
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
  • 泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。
1
2
3
4
5
6
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
  • 如果泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型。
1
2
3
4
5
6
class DataHolder implements Generator<String>{
@Override
public String next() {
return null;
}
}

Java泛型擦除

泛型类型不能显式地运用在运行时类型的操作当中,例如:转型、instanceof 和 new。因为在运行时,所有参数的类型信息都丢失了。类似下面的代码都是无法通过编译的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
//编译不通过
if (arg instanceof T) {
}
//编译不通过
T var = new T();
//编译不通过
T[] array = new T[SIZE];
//编译不通过
T[] array = (T) new Object[SIZE];
}
}

编译器虽然会在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。

泛型参数将会被擦除到它的第一个边界(边界可以有多个,重用 extends 关键字,通过它能给与参数类型添加一个边界)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。下面的例子中,可以把泛型参数T当作HasF类型来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface HasF {
void f();
}

public class Manipulator<T extends HasF> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}

extend关键字后后面的类型信息决定了泛型参数能保留的信息。Java类型擦除只会擦除到HasF类型。

由于Java泛型的类型擦除特点,我们难以判断参数的实际类型,可以通过一些手段来解决泛型的类型判断、类型转型、instanceof 和 new等问题。

类型判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 方法一:记录类型参数的Class对象,然后通过这个Class对象进行类型判断
* @param <T>
*/
class GenericType<T>{
Class<?> classType;

public GenericType(Class<?> type) {
classType=type;
}

//判断方法
public boolean isInstance(Object object) {
return classType.isInstance(object);
}
}

实例创建(new T())

泛型代码中不能new T()的原因有两个,一是因为擦除,不能确定类型;而是无法确定T是否包含无参构造函数。
为了避免这两个问题,我们使用显式的工厂模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 使用工厂方法来创建实例
*
* @param <T>
*/
interface Factory<T>{
T create();
}

class Creater<T>{
T instance;
public <F extends Factory<T>> T newInstance(F f) {
instance=f.create();
return instance;
}
}

class IntegerFactory implements Factory<Integer>{
@Override
public Integer create() {
Integer integer=new Integer(9);
return integer;
}
}

我们通过工厂模式+泛型方法来创建实例对象,上面代码中我们创建了一个IntegerFactory工厂,用来创建Integer实例,以后代码有变动的话,我们可以添加新的工厂类型即可。

调用代码如下:

1
2
Creater<Integer> creater=new Creater<>();
System.out.println(creater.newInstance(new IntegerFactory()));

创建泛型数组

一般不建议创建泛型数组。尽量使用ArrayList来代替泛型数组。但是在这里还是给出一种创建泛型数组的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GenericArrayWithTypeToken<T> {
private T[] array;

@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}

public void put(int index, T item) {
array[index] = item;
}

public T[] rep() {
return array;
}

public static void main(String[] args) {

}
}

这里我们使用的还是传参数类型,利用类型的newInstance方法创建实例的方式。

Java泛型通配符

我们前面提到过List和List之间没有继承关系,也就是说List不是List的子类型,这个和我们的直觉相违背。

例如我们定义了一个堆栈类(类似于集合,来存储数据)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Stack<E>{

private static final int DEFAULT_INITIAL_CAPACITY = 16;

private E[] elements;
private int size = 0;

public Stack(){
//假设这里创建出了E[]实例elements
}

public void push(E e){
//...
}

public E pop(){
//...
}

public boolean isEmpty(){
//...
}

}

此时我们给Stack类增加一个添加新集合进来的方法(pushAll)

1
2
3
4
5
public void pushAll(Iterable<E> src){
for(E e : src){
push(e);
}
}

到这个时候编译时通过的,但是使用起来却有很大的问题。

1
2
3
Stack<Number> stacks = new Stack<Number>();
Interable<Integer> itg = ....;
stacks.pushAll(itg);

此时你就会发现报错了,那是因为上面我们提到过void push(Number e)和void push(Integer e)是两个不同的泛型。

解决上面这个问题就需要使用到通配符。

上界通配符<? extends T>

1
2
3
4
5
public void pushAll(Iterable<? extends E> src){
for(E e : src){
push(e);
}
}

这样修改后上面代码就可以正确编译和执行了。

下界通配符<? super T>

接下来我们来编写一个popAll方法取出Stack集合中的多个元素,和上面的pushAll相对应。

1
2
3
4
5
public void popAll(Collection[E] dst){
while(!isEmpty()){
dst.add(pop());
}
}

同样的,上面的方法能正确得到编译,但是使用起来同样有问题。

1
2
3
Stack<Number> stacks = new Stack<Number>();
Collection<Object> oob = ....;
stacks.popAll(oob);

会发现,此时编译会报错,因为Collection不是Collection的子类型。此时popAll的输入参数类型不应该是“E的集合”而应该是“E的超类的集合”,修改如下:

1
2
3
4
5
public void popAll(Collection[? super E] dst){
while(!isEmpty()){
dst.add(pop());
}
}

总结上界和下界通配符何时用:

PECS:表示producer-exnteds, consumer-super

意思就是说如果参数类型表示一个T的生产者就使用<? exntends T>,如果表示一个T的消费者就使用<? super T>

无限通配符<?>

无界通配符 意味着可以使用任何对象,因此使用它类似于使用原生类型。但它是有作用的,原生类型可以持有任何类型,而无界通配符修饰的容器持有的是某种具体的类型。

1
2
List list1;
List<?> list2;

编译器很少关心使用的是原生类型还是<?>,而在这种情况下,<?>可以被认为是一种装饰,但是它仍旧是很有价值的,因为,实际上,它是在声明:“想用Java的泛型来编写代码,但是我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。”

下面的示例展示了无界通配符的一个重要的应用。当你处理多个泛型参数的时,有时允许一个参数可以是任何的类型,同时为其他参数确定某种特定类型的能力会显得尤为重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static Map map1;
static Map<?, ?> map2;
static Map<String, ?> map3;

static void assign1(Map map) {
map1 = map;
}

static void assign2(Map<?, ?> map) {
map2 = map;
}

static void assign3(Map<String, ?> map) {
map3 = map;
}

assign1(new HashMap());
assign2(new HashMap());
// warning
// Unchecked assignment: 'java.util.HashMap' to 'java.util.Map<java.lang.String,?>'
assign3(new HashMap());
assign1(new HashMap<String, Integer>());
assign2(new HashMap<String, Integer>());
assign3(new HashMap<String, Integer>());

Kotlin的泛型

Kotlin的泛型语法和Java类似,在Kotlin中不存在通配符,但提供了两种方法:声明处类型变异(declaration-sitevariance), 以及类型投射(type projection)。

类型变异(协变)

我们上面提到下面的代码要正确执行,必须将pushAll的形参定义为Iterable<? extends E>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

class Stack<E>{

//...省略,请参考上面

}

public void pushAll(Iterable<E> src){
for(E e : src){
push(e);
}
}

Stack<Number> stacks = new Stack<Number>();
Interable<Integer> itg = ....;
stacks.pushAll(itg);

在Kotlin中我们有办法将这种情况告诉编译器. 这种技术称为声明处的类型变异(declaration-sitevariance)

我们可以对Stack的类型参数E添加注解, 来确保 Stack的成员函数只会返回E类型, 而绝不会消费E类型. 为了实现这个目的, 我们可以对E添加out修饰符:

1
2
3
4
5
6
7
8
9
10
class Stack<out E>{

//...省略,请参考上面
}

fun pushAll(src: Iterable<E>){
for(e in src){
push(e)
}
}

一般规则是: 当S类的类型参数E声明为out时, 那么在S的成员函数中,E类型只允许出现在输出位置, 这样的限制带来的回报就是, S

可以安全地用作I的父类型。

类型投射(逆变)

和类型变异类似的是反向类型变异(contravariant): 这个类型将只能被消费, 而不能被生产。

1
2
3
4
5
6
7
8
9
10
class Stack<in E>{

//...省略,请参考上面
}

fun popAll(dst: Collection[E]){
while(!isEmpty){
dst.add(pop())
}
}

声明点变型

但是我们上面的Stack类中我们既想要pushAll方法又要popAll方法,那么他既协变又得逆变,这个时候我们就得使用声明点变型。

声明点变型:在类声明的时候,指定变型修饰符,这些修饰符会应用到所有类被使用的地方;

1
2
3
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}

Kotlin中支持使用点变型,允许在类型参数出现的具体位置指定变型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// (不变型) 从一个集合copy到另一个集合
fun <T> copyData(source: MutableList<T>, destination: MutableList<T>) {
for(item in source) {
destination += item
}
}

// 特定类型
fun <T : R, R> copyData2(source: MutableList<T>, destination: MutableList<R>) {
for (item in source) {
destination += item
}
}

// 使用点变型:给类型参数加上 变型修饰符 (out 投影)
fun <T> copyData3(source: MutableList<out T>, destination: MutableList<T>) {
for (item in source) {
destination += item
}
}

*星号投影

星号投影,用来表示不知道关于泛型实参的任何信息,跟Java的?问号通配符类似。

注意:MutableList<*> 和 MutableList<Any?> 不一样

  • MutableList<Any?>包含的是任何类型的元素。
  • MutableList<*>是包含某种特定类型元素,但不知是哪个类型,所以不能写入但可读取。