java里的泛型想必大家都很了解了,是java系统提供的一个特性,便于我们在设计代码时,可以将一部分内容设置为可变的,比如最常见的class List集合类,T可以为任意类型,比如我们想要String的集合就是List,想要Object的集合就用List,这样一来,通过一个类的书写就可以产生"很多"类,但实质上又都是一个类,因为泛型只是编译时用于类型安全检测的,运行时没有泛型类型,也就是我们常说的泛型擦除,也就是说泛型是在编译时确保我们的类型是安全的,不会出ClassCastException这样的类型转换异常。
kotlin自然不用说,也提供了泛型机制,作用和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对于泛型的一般使用其实和java一样,就不多说了,主要说说和java不同的协变,kotlin将上述java设计协变的原因理解为:
而且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设计协变的原因(上面说的两点)还是不够准确,于是乎自己认为:
所以综上诉说,kotlin对协变有了自己的一套定义:
这样就可以既保证类型安全,又保证了最大限度内的(逆)协变功能,上述例子用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也如此。