Kotlin从入门到进阶实战
为何引入泛型
为何引入泛型,最引人注意的一个原因是为了创建容器类。集合类可以说是最常用的类之一,在没有泛型前,集合类是怎样持有对象的呢。在java中Object是所有类的根类。为了集合类的通用性,把元素类型定义为Object,当放入具体类型时,再进行相应的强制类型转换。在使用原生态类型实现的集合类中,使用Object[]数组。这种方式常见的问题有两个:
1.像集合中添加元素的时候没有对元素类型进行检查。
2.从集合中取元素的时候,不能都使用Object类型,需要进行强制类型转换。而转换过程由于添加的时候没有任何限值和检查,所以容易出错。(java.lang.ClassCastException):java.lang.Integer cannot be cast to java.lang.string
对于这样的代码,编译器不会报错,但是运行时可能会发生类型转换错误。
泛型最主要的优点就是让编译器追踪参数类型。执行检查和类型转换。
在类、接口和函数上使用泛型
- 泛型接口
interface MyInterface<T> {//类型参数放在接口名称后面:<T>
operator fun next(): T //接口参数中直接使用类型T
}
使用如下,使用object关键字声明一个MyInterface实现类,并调用next()函数
fun test() {
val inter = object : MyInterface<Int> {
override fun next(): Int {
return Random().nextInt(100)
}
}
println(inter.next())
}
Kotlin中Map和MutableMap接口的定义也是一个典型的泛型接口的例子。
public interface Map<K, out V> {
//.........
public fun containsKey(key: K): Boolean
public fun containsValue(value: @UnsafeVariance V): Boolean
public operator fun get(key: K): V?
@SinceKotlin("1.1")
@PlatformDependent
public fun getOrDefault(key: K, defaultValue: @UnsafeVariance V): V {
// See default implementation in JDK sources
return null as V
}
public val keys: Set<K>
public val values: Collection<V>
public val entries: Set<Map.Entry<K, V>>
public interface Entry<out K, out V> {
/**
* Returns the key of this key/value pair.
*/
public val key: K
/**
* Returns the value of this key/value pair.
*/
public val value: V
}
}
使用mutableMapOf初始化一个Map
mutableMapOf<Int, String>(1 to "A", 2 to "B", 3 to "C")
mutableMapOf函数如下:
public fun <K, V> mutableMapOf(vararg pairs: Pair<K, V>): MutableMap<K, V>
= LinkedHashMap<K, V>(mapCapacity(pairs.size)).apply { putAll(pairs) }
这里的K、V在泛型类型被实例化和使用时,将被实际的类型所替代。
泛型可以用来限制集合类持有的对象类型,这样使得类型更加安全。我们在集合里放入错误的类型编译器会报错。
Kotlin中有类型推断功能,有些类型参数可以省略不写。
mutableMapOf(1 to "A", 2 to "B", 3 to "C")
- 泛型类
直接声明一个带类型参数的Container类
class Container<K, V>(var key: K, var value: V) {
override fun toString(): String {
return "key = $key value = $value"
}
}
测试
fun test() {
val container = Container<Int, String>(1, "A")
println(container)
}
输出如下
key = 1 value = A
- 泛型函数
函数中使用泛型代码如下
fun <T> test(t: T) {
println(t)
}
interface MyInter {
fun <T> go(t: T)
}
fun <T : Comparable<T>> gt(x: T, y: T): Boolean {
return x > y
}
类型上界
在下面这函数中
fun <T : Comparable<T>> gt(x: T, y: T): Boolean {
return x > y
}
T : Comparable ,这里的Comparable是类型T的上界。也就是说类型T代表的都是实现了Comparable接口的类。这样等于告诉编译器它们都是事先了CompareTo方法。如果没有声明上界,就无法使用>操作。
协变与逆变
我们来看一下问题场景。有下面存在子父类关系的类型。
open class Food
open class Fruit : Food()
class Apple : Fruit()
class Banana : Fruit()
class Grape : Fruit()
然后有下面的两个函数
object Text {
fun addFruit(fruit: MutableList<Fruit>) {
}
fun getFruit(fruit: MutableList<Fruit>) {
}
当向里面存放Apple的时候
val apples = mutableListOf(Apple(), Apple(), Apple())
addFruit(apples)//报错 Type mismatch
如果没有协变,我们要在里面添加两个方法
fun addFruit(fruit: MutableList<Fruit>) {
}
fun getFruit(fruit: MutableList<Fruit>) {
}
fun addApple(apple: MutableList<Apple>) {
}
fun getApple(apple: MutableList<Apple>) {
}
这样是重复的样板代码,那么怎么避免这个问题呢,让MutableList成为其父类型。java泛型中引入了类型通配符的概念来解决这种问题。java泛型通配符大概有两种方式:
- 子类型上界通配符 ? extends T 指定类型参数的上限,该类型必须为类型T或者它的子类型。也就是说 MutableList<?extends Fruit> 是MutableList的父类型。
- 超类型下界限定符 ?superT 指定类型参数的下限。该类型必须是T或者它的父类型,也就是说MutableList<?super Fruit>是MutableList的父类型。
这里的 ? ,称之为类型通配符。它们为一个泛型类所指定的类型集合提供了一个有用的类型范围。
- Number类型为F Integer类型是 C ,F 是 C 的父类型,我们把这种父子类型关系记为C=>F C继承F ;而List代表的泛型类型信息为f(F) 、 f©。我们可以这样描述协变和逆变。
- 当C=>F时,如果有f© => f©,那么 f 叫做协变
- 当C=>F时,如果f(F) => f(F),那么 f 叫做逆变。如果这两种关系都不成立,则叫做不变。
- 协变和逆变都是类型安全的。
Kotlin中用 out 和 in 代表 extends 和 super 所以上面的代码可以这么写 编译器就不会报错了。
object Text {
fun addFruit(fruit: MutableList<out Fruit>) {
}
fun getFruit(fruit: MutableList<out Fruit>) {
}
@JvmStatic
fun main(args: Array<String>) {
val apples = mutableListOf(Apple(), Apple(), Apple())
addFruit(apples)
}
}
协变
- va中数组是协变的,下面代码可以正确运行
public static void main(String[] args) {
Integer[] integers = new Integer[3];
integers[0] = 0;
integers[1] = 1;
integers[2] = 2;
Number[] numbers = new Number[3];
numbers = integers;
for (Number number :numbers){
System.out.println(number);
}
}
在java中Integer是Number子类型,数组Integer[]也是Number[]子类型,因此任何需要Number[]值的地方都可以提供一个Integer[]值。所以说 java中数组是协变的。
- java中泛型是非协变的,也就是说List< Integer >不是List< Number > 的子类型
我们使用通配符写法如下:
同样是报错的。为什么Number对象可以由Integer实例化。而List< Number >不能由List< Integer >实例化呢?
List<? extends Number> integers =new ArrayList<>();
这里的类型C是 Number或者其子类(Number Integer Float等)。父类F就是上界通配符?extends Number。但是这里不能像 integers 里面添加除了 null 以外的对象。
List<? extends Number> integers = new ArrayList<Integer>();
integers.add(null);//成功
integers.add(new Integer(1));//报错
如果能添加List< Number>的子类,也可以添加List< Integer>的子类,那么集合中的元素类型会比较混乱。我们无法判断哪个类型是Integer 哪个类型是Float,为了保持类型一致,java禁止像List<?extends Number>中添加任意Number类型的对象。不过可以添加null。
逆变
List<? super Number> numbers = new ArrayList<Object>();
这里的子类型C是“?super Number”,父类型F是Number的父类型,逆变的意义如下
public static void main(String[] args) {
List<? super Number> list = new ArrayList<Number>();
List<? super Number> list1 = new ArrayList<Object>();
list.add(new Integer(3));
list1.add(new Integer(4));
}
在逆变类型中,我们可以向其中添加元素。例如我们可以向List< ? super Number> list中添加Number以及子类对象
PECS(Producer-Extends,Consumer-Super)
什么时候使用 extends 什么时候使用 super 呢,在java.util.Collections的copy方法中诠释了PECS。
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next()T
di.set(si.next());
}
}
}
上界<? extends T>不能往里存,只能往外取
下界<? super T>可以往里存,但往外取只能放在Object对象里
out T 、in T
- Kotlin抛弃了通配符?,直接实现了PECS的规则
- out T 等价于 ?extends T
- in T等价于 ? super T
类型擦除
- java和Kotlin的泛型实现,都是采用了运行时类型擦除的方式。也就是说在运行时,这些类型的参数信息会被擦除。
泛型是在编译器层次上实现的,生成的class字节码是不包含泛型中的类型信息的。例如在代码中定义的List< String> JVM看到的只是 List,由泛型添加的信息 JVM是不可见的。 - 泛型很多特性都与这个类型擦除有关,比如List< String>.class 不存在,只有List.class ,对应的Kotlin中也只有 MutableList::class。
- 类型擦除的过程
(1) 首先找到用来替换类型参数的具体类。一般是Object。如果指定了类型上界的话就使用这个上界。
(2) 其次,把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明。即去掉< >的内容。
(3) 最后根据需要生成一些桥接方法。这是由于擦除了类型之后的类可能缺少某些必须的方法。这个时候由编译器来动态生成。