Kotlin 泛型

一.概念

java里的泛型想必大家都很了解了,是java系统提供的一个特性,便于我们在设计代码时,可以将一部分内容设置为可变的,比如最常见的class List集合类,T可以为任意类型,比如我们想要String的集合就是List,想要Object的集合就用List,这样一来,通过一个类的书写就可以产生"很多"类,但实质上又都是一个类,因为泛型只是编译时用于类型安全检测的,运行时没有泛型类型,也就是我们常说的泛型擦除,也就是说泛型是在编译时确保我们的类型是安全的,不会出ClassCastException这样的类型转换异常。

kotlin自然不用说,也提供了泛型机制,作用和java也是一样,只不过实现有些不同罢了,我们接着往下看。

二.Java协变

如果泛型只是像List这样使用就很简单了,但是泛型在java中会有一些其他的问题,看下面的例子:

public static void paramType(){
    List<String> stringList = new ArrayList<>();
    List<Object> objectList = stringList;//wrong
    testParamType(stringList);//wrong
}

private static void testParamType(List<Object> objectList){}

上面的两行错误代码都表明了List和List并不是子类父类的关系,否则List就可以直接赋值给List,这是java的多态特性,那么为什么这两个类不是子类父类关系呢,我们上面说过,泛型只是编译时的类型检测作用,并不是运行时的一个真正的类,所以他们本质上还是List类;那么毕竟Object是String的父类,为什么java不把这样的泛型也作为子类父类呢,如果也是子类父类的关系的话,那么我们使用起来会更方便,比如看下面的代码:

//假设这是List的addAll方法
public boolean addAll(Collection<T> c){
	c.forEach{this.add(it)};
}
 
public static void paramType(List<String> stringList){
    List<Object> objectList = new ArrayList<>();
	objectList.addAll(stringList);
}

如果上述代码是成立的话,那么我们可以很轻易的就将List全部元素添加到List里了,而且String是Object的子类,取出时也没有问题,一切看似很完美,但是会有问题,比如下面的代码:

public static void paramType() {
    List<String> stringList = new ArrayList<>();
    List<Object> objectList = stringList;//假设这句代码成立
    objectList.add(1);
    String res = stringList.get(0);//ClassCastException!!!
}

我们看,假设第二句代码成立的话,那么,这段代码第四行就会有类型转换的异常抛出,因为既然是List,那么他肯定可以添加任何Object及其子类对象,但是其运行时类型其实是List,虽然在编译时不会报错了,但是从List中取出元素强转为String,自然就出现这个错误了(Integer可不能直接转为String),这也就是说编译时泛型认为了你写的类型是安全的,但却运行时并不如此,这不是打java的脸么。

那咋办呢,总不能像上面的addAll方法不让实现吧,这就太坑了,于是乎,java提供了一个泛型协变的特性,也就是通配符,即通过List<? extends Object>类型来表示所有Object及其子类型的List,看下面代码:

public static void paramType() {
    List<String> stringList = new ArrayList<>();
    List<? extends Object> objectList = stringList;
    objectList.add(1);//wrong
    String res = stringList.get(0);
}

现在List<?extends Object>可以接受List了,但作为代价和规定,List<?extends Xxx>就不能够再add任何东西,因为你不确定到底add进去的元素到底能不能放到List的运行时类型里,但是get没有问题,因为即使是String,List<?extends Object>get取出来作为Object也没有问题,因为String是Object的子类—所以可以这么理解,java用<?extends X>这个"新的"类型(也叫使原类型协变)处理了上述这种情况,让其可以当做子类父类进行赋值,但是不能进行add添加,从而保证了类型的安全;

我们再来看一下真实的addAll方法:

boolean addAll(Collection<? extends E> c);

是不是就明白了;

同理,还有一种情况:? extends X是指X或X的子类,也就是上界,那也可能会有下界的情况,对此java提供了逆协变类型,即?super X,看一个例子即可明白:

List<Object> objectList = new ArrayList<>();
List<? super String> strList = objectList;
strList.add("string");//right
Object res = objectList.get(0);//right
String res1 = strList.get(0);//wrong

当我们用List<? super String>接收List后,规定前者只能addString类型,这是无论其运行时类型是什么,都将是String或其父类的集合,所以add进去没有问题,同时规定前者不能get,或者说get到的都是Object类型,这也是对的,不然你怎么只到get到的具体是String的哪个父类型对象呢?所以逆协变就是相对于协变的一套下界类型而已。

说了这么多,就是解释一下java是如何支持泛型的协变并保证其类型安全的,知道了这些,就可以来看看kotlin是如何针对这些情况做的改变了。

三.Kotlin协变

(一)声明处协变

kotlin对于泛型的一般使用其实和java一样,就不多说了,主要说说和java不同的协变,kotlin将上述java设计协变的原因理解为:

  1. 之所以要上界通配符?extends X,是因为怕"父类型"指向"子类型",可以随意执行符合"父类型"而不符合"子类型"的操作,如上述的对Object集合添加Integer合法,但是对String集合添加Integer不合法
  2. 之所以要下界通配符?super X,是因为怕"子类型"指向"父类型",可以随意执行符合"子类型"而不符合"父类型"的操作,如上述对String集合添加String合法,但是如果指向的是Object集合,String集合取出Object集合里的东西就不一定合法了(因为可能是Object的任意子类,不一定能转换为String)

而且java的通配符方式也有一些不便,如下面的例子:

 class Generic<T> {

    private T value;

    public Generic(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}
 
Generic<String> strGeneric = new Generic<>("string");
//wrong
Generic<Object> objGeneric = strGeneric;
Object res = objGeneric.getValue();
//right
Generic<? extends Object> objGeneric2 = strGeneric;
Object res2 = objGeneric2.getValue();

Generic类的泛型参数是T,这个类的功能仅仅是返回一个T的对象(构造时传入的),所以我们就想,直接用Generic接收Generic也可以,然后get出来的是Object类型也没毛病,并没有涉及到类似add方法的操作,但是你认为没用,java的协变规则不允许,所以我们还是只能用通配符类型?extends X来处理,这显然很麻烦和不必要;

通过类似的情况,kotlin认为java设计协变的原因(上面说的两点)还是不够准确,于是乎自己认为:

  1. 需要上界通配符进行协变的原因并不是泛型通用的问题,虽然类使用了泛型,但如果类内部没有外部可以调用到的,输入泛型类型元素的相关方法(类似add)时,就不会有问题,而有输出(类似get)的相关方法是不会有问题的,因为子类输出为父类类型是没有问题的
  2. 同理下界通配符进行逆协变的原因也不是泛型通用的问题,如果类内部没有外部可以调用到的输出(类似get)的相关方法,也不会有问题,而有输入(类似set)的方法是不会有问题的,因为输入子类拿父类接受是没有问题的

所以综上诉说,kotlin对协变有了自己的一套定义:

  1. 凡是类内部没有供外部调用的,参数为泛型类型的方法(输入方法),而仅有返回值为泛型类型的方法(输出方法)时,就可以做到协变,在声明泛型时以out来表明这个泛型类型可以协变
  2. 凡是类内部没有供外部调用的,返回值为泛型类型的方法(输出方法),而仅有参数为泛型类型的方法(输入方法)时,就可以做到逆协变,在声明泛型时以in来表明这个泛型类型可以逆协变

这样就可以既保证类型安全,又保证了最大限度内的(逆)协变功能,上述例子用kotlin来写:

class Generic<out T>(val value: T)//只有public的get方法
 
val strGeneric: Generic<String> = Generic("string")
val anyGeneric:Generic<Any> = strGeneric//right
val res: Any = anyGeneric.value

是不是方便很多。

(二)使用处协变

上面说的协变是声明处协变,也就是在声明类时就给了泛型类型定义,但是如果类的泛型类型确实不能使用out或者in,那么就没办法让"父类型"指向"子类型"了么,比如下面的情况:

class Generic<T>(var value: T)
 
val strGeneric: Generic<String> = Generic("string")
val anyGeneric:Generic<Any> = strGeneric//wrong

基于这种情况,kotlin也提供了使用处协变,也就是在使用时,将泛型类型声明为out或in来实现这种功能:

class Generic<T>(var value: T)//getter/setter都有

val strGeneric: Generic<String> = Generic("string")
val anyGeneric:Generic<out Any> = strGeneric//right

使用处协变其实就像java的?extends X,可以接受X即X的子类,并且不能调用输入类型的方法(如setter),同理in也如此。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值