阅读 372

【疯狂Android之Kotlin】 聊聊kotlin中的泛型吧

泛型简介

本文同学我想和大家分享一下泛型的相关知识,欢迎大家一起讨论学习

泛型是什么?

  • 相信很多同学已经对泛型非常熟悉了,在实际项目中类和函数中用到泛型是必不可少的。通常情况下的,每个类和函数都可以使用具体的类型即可完成相应功能,但是有个问题,如果是在集合类的场景下,每种类型都用一个方法去实现,这样代码复用率是较低的,并且抽象也没有做好。
  • 这个时候,jdk5就引入了泛型,简单来说即参数化类型,把原来具体的类型进行参数化,相当于方法的类型参数,在使用的时候传入具体的类型值,这样可以实现代码复用,大大减少了代码冗余。

类型参数是什么?

上面提到了泛型相当于以类型作为变量的类型,作为参数类型,就是所谓的类型参数。是不是有点绕

  • 首先,我们来看下在没有泛型之前,我们集合类是怎么实现的,这里我用java来实现下,由于在java中object是所有的根类,转为其他类型的需要强制类型转换
class RawArrayList {
    public int length = 0;
    private Object[] elements;
    public RawArrayList(int length) {
        this.length = length;
        this.elements = new Object[length];
    }
    public Object get(int index) {
        return elements[index];
    }
    public void add(int index, Object element) {
        elements[index] = element;
    }
    @Override
    public String toString() {
        return "RawArrayList{" +
            "length=" + length +
            ", elements=" + Arrays.toString(elements) +
            '}';
    }
}
复制代码
  • 测试代码如下
public class RawTypeDemo {
    public static void main(String[] args) {
        RawArrayList rawArrayList = new RawArrayList(4);
        rawArrayList.add(0, "a");
        rawArrayList.add(1, "b");
        System.out.println(rawArrayList);
        String a = (String)rawArrayList.get(0);
        System.out.println(a);
        String b = (String)rawArrayList.get(1);
        System.out.println(b);
        rawArrayList.add(2, 200);
        rawArrayList.add(3, 300);
        System.out.println(rawArrayList);
        int c = (int)rawArrayList.get(2);
        int d = (int)rawArrayList.get(3);
        System.out.println(c);
        System.out.println(d);
        // Exception in thread "main" java.lang.ClassCastException: 
          // java.lang.Integer cannot be cast to java.lang.String
        String x = (String)rawArrayList.get(2);
        System.out.println(x);
    }
}
复制代码
  • 思考:这种使用object[]数组形式的实现方式会存在什么问题呢?
  1. 向集合中添加对象元素的时候,没有对元素的类型进行检查,也就是说,我们往集合中添加任意对象,编译器都不会报错。

  2. 当我们从集合中获取一个值的时候,我们不能都使用Object类型,需要进行强制类型转换。而这个转换过程由于在添加元素的时候没有作任何的类型的限制跟检查,所以容易出错。例如上面代码中的:String a = (String)rawArrayList.get(0);,对于这行代码,编译时不会报错,但是运行时会抛出类型转换错误。

  3. 所以我们不能把所有的对象视作Object,然后具体的值再进行强制类型转换,这时候就需要引入类型参数来解决这个问题了

  4. 下面我们可以看看kotlin中map接口的定义

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 MutableMap<K, V> : Map<K, V> {
    public fun put(key: K, value: V): V?
    public fun remove(key: K): V?
    public fun putAll(from: Map<out K, V>): Unit
    ...
}
复制代码
  • 比如,在实例化一个Map时,我们使用这个函数:
fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>
复制代码
  • 类型参数K,V是一个占位符,当泛型类型被实例化和使用时,它将被一个实际的类型参数所替代。
>>> val map = mutableMapOf<Int,String>(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}
>>> map.put(4,"c")
null
>>> map
{1=a, 2=b, 3=c, 4=c}
复制代码
  • mutableMapOf<Int,String>表示参数化类型<K , V>分别是Int 和 String,这是泛型类型集合的实例化,在这里,放置K, V 的位置被具体的Int 和 String 类型所替代。

  • 泛型主要是用来限制集合类持有的对象类型,这样使得类型更加安全。当我们在一个集合类里面放入了错误类型的对象,编译器就会报错:

>>> map.put("5","e")
error: type mismatch: inferred type is String but Int was expected
map.put("5","e")
        ^
复制代码
  • Kotlin中有类型推断的功能,有些类型参数可以直接省略不写。上面的mapOf后面的类型参数可以省掉不写:
>>> val map = mutableMapOf(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}
复制代码
  1. 以上对于Kotin的泛型实现,其实采用了运行时类型擦除的方式。也就是说,在运行时,这些类型参数的信息将会被擦除。Java 和 Kotlin 的泛型对于语法的约束是在编译期

Kotlin的泛型特点

在这里值得一提的是,在java泛型里,在 Java 泛型里,有通配符这种东西,我们要用? extends T指定类型参数的上限,用 ? super T指定类型参数的下限。而Kotlin抛弃了这个东西,引用了生产者和消费者的概念。也就是我们前面讲到的PECS。生产者就是我们去读取数据的对象,消费者则是我们要写入数据的对象。这两个概念理解起来有点绕。

  • 下面还是用代码来帮助理解一下
public static <T> void copy(List<? super T> dest, List<? extends T> src) {  
        ...
        ListIterator<? super T> di = dest.listIterator();   // in T, 写入dest数据
        ListIterator<? extends T> si = src.listIterator();   // out T, 读取src数据
         ...
}
复制代码
  • 思考
  1. List<? super T> dest是消费数据的对象,这些数据会写入到该对象中,这些数据该对象被“吃掉”了(Kotlin中叫in T)。
  2. L2ist<? extends T> src是生产提供数据的对象。这些数据哪里来的呢?就是通过src读取获得的(Kotlin中叫out T)。

out T 与 in T

  • 在Kotlin中,我们把那些只能保证读取数据时类型安全的对象叫做生产者,用 out T标记;
  • 把那些只能保证写入数据安全时类型安全的对象叫做消费者,用 in T标记。
  • 如果觉得太晦涩难懂,同学我的话就是这么记的:
  1. out T 等价于? extends T
  2. in T 等价于 ? super T
  3. 此外, 还有 * 等价于?

声明处型变

Kotlin 泛型中添加了声明处型变。

  • 看下面的例子:
interface Source<out T> {
    fun <T> nextT();
}
复制代码
  1. 我们在接口的声明处用 out T 做了生产者声明以实现安全的类型协变:
fun demo(str: Source<String>) {
    val obj: Source<Any> = str // 合法的类型协变
}
复制代码
  1. Kotlin 中有大量的声明处协变,比如 Iterable 接口的声明:
public interface Iterable<out T> {
    public operator fun iterator(): Iterator<T>
}
复制代码
  • 因为 Collection 接口和 Map 接口都继承了 Iterable 接口,而 Iterable 接口被声明为生产者接口,所以所有的 Collection 和 Map 对象都可以实现安全的类型协变:
  1. 来看看下面的例子
val c: List<Number> = listOf(1, 2, 3)
复制代码
  • 这里的 listOf() 函数返回 List类型,因为 List 接口实现了安全的类型协变,所以可以安全地把 List类型赋给 List 类型变量。

类型投影

将类型参数 T 声明为 out 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回 T。

  1. 一个很好的例子是 Array:
class Array<T>(val size: Int) {
    fun get(index: Int): T {  }
    fun set(index: Int, value: T) {  }
}
复制代码
  • 该类在 T 上既不能是协变的也不能是逆变的。这造成了一些不灵活性。
  1. 考虑下述函数:
fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}
复制代码
  • 这个函数应该将项目从一个数组复制到另一个数组。
  1. 如果我们采用如下方式使用这个函数:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 
copy(ints, any) // 错误:期望 (Array<Any>, Array<Any>)
复制代码
  • 这里我们将遇到同样的问题:Array 在 T 上是不型变的,因此 Array 和 Array 都不是另一个的子类型。
  • 那么,我们唯一要确保的是 copy() 不会做任何坏事。
  1. 我们阻止它写到 from,我们可以:
fun copy(from: Array<out Any>, to: Array<Any>) {}
复制代码
  • 现在这个from是一个受Array限制的(投影的)数组。在Kotlin中,称为类型投影(type projection)。其主要作用是参数作限定,避免不安全操作。
  1. 类似的,我们也可以使用 in 投影一个类型:
fun fill(dest: Array<in String>, value: String) {}
复制代码
  • Array 对应于 Java 的 Array<? super String>,也就是说,我们可以传递一个 CharSequence 数组或一个 Object 数组给 fill() 函数。
  1. 类似Java中的无界类型通配符?, Kotlin 也有对应的星投影语法。

例如,如果类型被声明为 interface Function <in T, out U>,我们有以下星投影:

Function<*, String> 表示 Function<in Nothing, String>;
Function<Int, *> 表示 Function<Int, out Any?>;
Function<*, *> 表示 Function<in Nothing, out Any?>。
*投影跟 Java 的原始类型类似,不过是安全的。
复制代码

关于函数和类

同样的泛型可以用作泛型函数,也可以作为一个泛型类,下面让我们来看看

泛型函数

  • 类可以有类型参数。函数也有。类型参数要放在函数名称之前:
fun <T> singletonList(item: T): List<T> {}
fun <T> T.basicToString() : String {  // 扩展函数
}
复制代码
  • 要调用泛型函数,在函数名后指定类型参数即可:
val l = singletonList<Int>(1)
复制代码
  • 泛型函数与其所在的类是否是泛型没有关系,泛型函数独立于其所在的类。
  • 我们应该尽量使用泛型方法,也就是说如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更明白。

泛型类

泛型类不太好描述,下面我用代码来简单描述一下,这样会变得通俗易懂

  1. 声明一个泛型类
class Box<T>(t: T) {
    var value = t
}
复制代码
  • 通常, 要创建这样一个类的实例, 我们需要指定类型参数:
val box: Box<Int> = Box<Int>(1)
复制代码
  • 但是, 如果类型参数可以通过推断得到, 比如, 通过构造器参数类型, 或通过其他手段推断得到, 此时允许省略类型参数:
val box = Box(1) // 1 的类型为 Int, 因此编译器知道我们创建的实例是 Box<Int> 类型
复制代码

总结

  • 通过简单的聊聊泛型,它是非常有用的东西,尤其是在集合类当中,只有深入理解了这些概念,我们才能更好理解并用好Kotlin的集合类,进而写出高质量的泛型代码。
  • 泛型实现是依赖OOP中的类型多态机制的。
  • 思考:关于kotlin中的协变,逆变也是属于kotlin泛型的特色功能,可以欢迎大家一起探讨学习。
文章分类
Android
文章标签