欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-14 18:36:29
...

泛型的优势:

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核心编程