Kotlin面向对象总结之泛型(部分,涉及泛型类、泛型参数使用)

泛型的优势:

1.类型检查,能在编译时就能帮你检查出错误。

2.更加语义化,比如声明一个List<String>,可以知道存储的是String对象,而不是其他对象。

3.自动类型转换,获取数据时不需要进行类型强制转换。

4.能写出更加通用化的代码。

在Kotlin中使用泛型

1.声明一个泛型类和泛型函数

//泛型类可以继承另一个类
class PaySmartList<T> : ArrayList<T>() {

    fun find(t: T): T? {
        val index = super.indexOf(t)
        return if (index >= 0) super.get(index) else null
    }
}

fun main() {
    val smartList = PaySmartList<String>()
    smartList.add("one")
    println(smartList.find("one"))
    println(smartList.find("two").isNullOrEmpty())
}

one
true

 扩展函数也支持泛型。

fun <T> ArrayList<T>.find(t: T): T? {
    val index = this.indexOf(t)
    return if (index >= 0) this.get(index) else null
}

fun main() {
    val arrayList = ArrayList<String>()
    arrayList.add("one")
    println(arrayList.find("one"))
    println(arrayList.find("two").isNullOrEmpty())
}
one
true

在Koltin 中使用泛型时是否需要主动指定类型?

在Kotlin中以下方式的写法是不允许的。

//该写法是错误的
val payArrayList = ArrayList()

 因为在Java中,泛型时Java1.5版本才引入的,而集合类在Java早起早期版本中就已经有了。所以为了保证兼容老版本代码,Java允许声明没有具体类型参数的泛型类。而Kotlin时基于Java6版本的,一开始就有泛型,不存在兼容老版本的问题。所以当声明一个空列表时,Kotlin需要显式声明具体的类型参数。

但是Kotlin具有类型推导的能力,下面的写法也是可行的:

val payArrayList = arrayListOf("one", "two")

类型约束:设置类型上界

Kotlin中使用":",进行泛型约束,称为上界约束。

例子:

class Plate<T>(val t: T)

open class Fruit(val weight: Double)

class Apple(weight: Double) : Fruit(weight)

class Banana(weight: Double) : Fruit(weight)

//T只能是Fruit类及其子类的类型,其他人类型是不允许的
class FruitPlate<T : Fruit>(val t: T)

因为Kotlin支持可空类型,所以只需要在参数类型后面加一个“?”。

class FruitPlate<T : Fruit?>(val t: T)

 上面代码展示的类型约束都是单个条件的约束。

如何实现多个条件的约束?

通过where关键字来实现这个需求。它可以实现对泛型参数类型添加多个约束条件。

interface Ground {

}

class Waternelon(weight: Double) : Fruit(weight), Ground

fun <T> cut(t: T) where T : Fruit, T : Ground {
    println("You can cut me")
}

fun main() {
    cut(Waternelon(3.30))
}

泛型的背后:类型消除

1.Java为什么不能声明一个泛型数组?

例子:Apple是Fruit的子类,观察Apple[]和Fruit[],以及List<Apple>和List<Fruit>的关系

public static void main(String[] args) {
    Apple[] appleArry = new Apple[10];
    Fruit[] fruitArray = appleArry;//允许

    fruitArray[0] = new Banana(0.5);//编译通过,运行时报java.lang.ArrayStoreException

    List<Apple> appleList = new ArrayList<Apple>();
    List<Fruit> fruitList = appleList;//报错,不允许
}

 从上面的代码可以发现:Apple[]类型的值可以赋值给Fruit[]类型的值,而且可以将一个Banana对象添加到fruitArray,编译可以通过。作为对比,List<Fruit>类型到值在一开始就禁止被赋值为List<Apple>类型的值。

原因:数组是协变的,而List是不变的。也就是说Object[]是所有对象数组的父类,而List<Object>却不是List<T>的父类。

Java中的泛型是类型擦除的,可以看作伪泛型,就是无法在运行时获取到一个对象的具体类型。

public static void main(String[] args) {
    Apple[] appleArry = new Apple[10];
    Fruit[] fruitArray = appleArry;//允许
    List<Apple> appleList = new ArrayList<Apple>();
    System.out.println(appleArry.getClass());

    System.out.println(appleList.getClass());
}


class [Lcom.example.kotlindemo.genericdemo.Apple;
class java.util.ArrayList

 从上面的代码可以发现:数组在运行时是可以获取到自身类型的,而List<Apple>在运行时只知道自己是一个List,而无法获取到泛型参数的类型。Java数组是协变的,也就是任意的类A和B,若A是B的父类,则A[]也是B[]的父类。但是假如给数组加入泛型后,将无法满足数组协变的原则,因为运行时无法知道数组的类型了。

Kotlin中的泛型机制与Java相同,所以上面的特性在Kotlin中也存在。

fun main() {
    val appleList = ArrayList<Apple>()
    println(appleList.javaClass)
}
class java.util.ArrayList

但是不同的是:Kotlin中的数组是支持泛型的,所以也不再协变,就是不能将任意一个对象的数组赋值给Array<Any>或者Array<Any?>。

val appleArry = arrayOf<Apple>()
println(appleArry.javaClass)
class [Lcom.example.kotlindemo.genericdemo.Apple;
val appleArry = arrayOfNulls<Apple>(4)
val arrayAny:Array<Any?> = appleArry //不允许

 所以在Kotlin和Java中泛型是通过类型擦除来实现的。

为什么要类型擦除?向后兼容

向后兼容是Java的一大特性,就是老版本的Java文件编译后可以运行在新版本的JVM上。在Java1.5 之前,是没有泛型的。会有大量下面的代码:

List list = new ArrayList();

 为什么使用类型擦除可以解决新老代码兼容的问题?

Java代码

public static void main(String[] args) {
    ArrayList list = new ArrayList();
    ArrayList<String> strList = new ArrayList<String>();
}

show byte code

0: new           #2                  // class java/util/ArrayList
3: dup
4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
7: astore_1
8: new           #2                  // class java/util/ArrayList
11: dup

可以发现在Java中两种方式声明的ArrayList在编译后的字节码是完全一样的,这也说明了低版本编译的class文件在高版本的JVM上运行不会出现问题。既然泛型在编译后是会擦除类型的,那为什么可以使用泛型的相关特性:类型检查、类型自动转换?

1.首先类型检查时编译器在编译前帮助我们进行类型检查,所以类型擦除不会影响到它。

2.自动类型转换如何实现?

背后其实也是通过强制类型转换实现的。

public static void main(String[] args) {
    ArrayList<String> strList = new ArrayList<String>();
    strList.add("one");
    String s = strList.get(0);
}

 get方法到源码:

public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    return (E) elementData[index];//强制类型转换
}

 show byte code

0: new           #2                  // class java/util/ArrayList
3: dup
4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc           #4                  // String one
11: invokevirtual #5                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: aload_1
16: iconst_0
17: invokevirtual #6                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
20: checkcast     #7                  // class java/lang/String 强制类型转换

 虽然Java受限于向后兼容的困扰,使用了类型擦除来实现了泛型,但还是通过(其他方式(强制类型转换))来保证泛型的相关特性。

类型擦除的矛盾

通常情况下使用泛型,并不介意它的类型是否擦除,但在有些场景,我们却需要知道运行时泛型参数的类型,比如序列化/反序列化的时候。

是否可以主动指定参数类型来达到运行时获取泛型参数类型的效果?

open class PayPlate<T>(val t: T, val clazz: Class<T>) {
    fun getType() {
        print(clazz)
    }
}

fun main() {
    val applePlate = PayPlate(Apple(1.0), Apple::class.java)
    applePlate.getType()
}

class com.example.kotlindemo.genericdemo.Apple

 使用上面的方式确实可以达到运行时获取泛型类型参数的效果,但是这种方式也有限制。

下面代码就是无法获取泛型的类型

val listType = ArrayList<String>::class.java //不被允许
val mapType = Map<String, String>::class.java//不被允许

 所以这时候只能通过其他方式获取类型信息?

利用匿名内部类的方式获取:

val payList1 = ArrayList<String>()
val payList2 = object : ArrayList<String>() {} //匿名内部类
println(payList1.javaClass.genericSuperclass)
println(payList2.javaClass.genericSuperclass)

class com.example.kotlindemo.genericdemo.Applejava.util.AbstractList<E>
java.util.ArrayList<java.lang.String>

 为什么使用匿名内部类的方式能够运行时获取泛型参数的类型呢?

其实泛型类型擦除并不是真正的将全部的类型信息都擦除,还是会将类型信息存放在对应class的常量池中。

设计一个能获取所有类型信息的泛型类。

open class GenericsToken<T> {
    var type: Type = Any::class.java

    init {
        val superClass = this.javaClass.genericSuperclass
        type = (superClass as ParameterizedType).actualTypeArguments[0]
    }
}

fun main() {
    val type = object : GenericsToken<Map<String, String>>(){}//使用object创建一个匿名内部类
    println(type.type)
}

java.util.Map<java.lang.String, ? extends java.lang.String>

 匿名内部类在初始化的时候就会绑定父类或父接口的相应信息,这样就能通过获取父类或者父接口的泛型类型信息来实现需求。

使用内联函数获取泛型

Kotlin中的内联函数在编译时,编译器便会将相应函数的字节码插入到调用的地方,也就是说,参数类型也会被插入到字节码中,我们就可以获取参数的类型了。

inline fun <reified T> getType():Type {
    return T::class.java
}
inline fun <reified T> getType():Type {
    return T::class.java
}

fun main() {
    println(getType<Map<String, String>>())
}

interface java.util.Map

 使用内联函数获取泛型的参数类型非常简单,只需要加上reified关键字即可。这里的意思相当于,在编译的时候会将具体的类型插入到相应的字节码中,那么我们就能在运行时获取到对应参数的类型了。

需要注意的是: Java中并不主动指定一个函数是否是内联函数,所以在Kotlin中声明的普通内联函数可以在Java中调用,因为它会被当作一个常规函数;而使用 refied来实例化的参数类型的内联函数则不能在Java中调用,因为它永远需要内联的。

参考Kotlin核心编程

发布了179 篇原创文章 · 获赞 175 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/zhangying1994/article/details/104345243
今日推荐