由Java泛型看Kotlin的泛型

参考链接:

什么是泛型

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

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是所有类的超类)

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之前是没有泛型的,就是这样做的。泛型同样可以实现这个需求,但是通过面向对象的多态特性避免了类型强转,而且在编译期间就能发现类型问题。

class DataHolder<T>{
    T item;
    
    public void setData(T t) {
    	this.item=t;
    }
    
    public T getData() {
    	return this.item;
    }
}

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

Java的泛型

泛型方法

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

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

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泛型类基本相同,下面是一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}
  • 泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。

    class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
    }
    
  • 如果泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型。

    class DataHolder implements Generator<String>{
    @Override
    public String next() {
    	return null;
    }
    }
    

Java泛型擦除

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

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类型来使用。

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等问题。

类型判断

/**
 * 方法一:记录类型参数的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是否包含无参构造函数。 为了避免这两个问题,我们使用显式的工厂模式:

/**
 * 使用工厂方法来创建实例
 *
 * @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实例,以后代码有变动的话,我们可以添加新的工厂类型即可。

调用代码如下:

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

创建泛型数组

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

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<String>List<Object> 之间没有继承关系,也就是说 List<String> 不是 List<Object> 的子类型,这个和我们的直觉相违背。

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

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)

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

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

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

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

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

上界通配符 <? extends T>

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

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

下界通配符 <? super T>

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

public void popAll(Collection[E] dst){
    while(!isEmpty()){
        dst.add(pop());
    }
}

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

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

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

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

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

PECS:表示producer-exnteds, consumer-super

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

无限通配符 <?>

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

List list1;
List<?> list2;

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

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

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>


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类型. 为了实现这个目的, 我们可以对E添加out修饰符:

class Stack<out E>{

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

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

一般规则是: 当S类的类型参数E声明为out时, 那么在S的成员函数中,E类型只允许出现在输出位置, 这样的限制带来的回报就是, S<Base> 可以安全地用作I的父类型。

类型投射(逆变)

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

class Stack<in E>{

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

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

声明点变型

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

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

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

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

// (不变型) 从一个集合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<*> 是包含某种特定类型元素,但不知是哪个类型,所以不能写入但可读取。