【Kotlin学习】泛型——变型:泛型和子类型化

本文探讨了Kotlin中的泛型变型如何处理类型传递,如协变、逆变和不变型的概念,以及如何通过使用点变型和星号投影解决类型不匹配问题。重点介绍了子类型化、类和接口在类型系统中的作用,以及如何确保类型安全以避免潜在风险。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

变型:泛型和子类型化

变型概念描述了拥有相同基础类型和不同类型实参的(泛型)类型之间是如何关联的

为什么存在变型:给函数传递实参

一个接收List作为实参的参数,可以把List<String>传入,这样是安全的,因为String继承了Any。但当String和Any变成List接口的类型实参后就不一样了

在这里插入图片描述

当期望的是MutableList<Any>的时候把一个MutableList<String>当作实参传递是不安全的。如果函数添加或替换了列表中的元素就是不安全的,因为这样会产生类型不一致的可能性。在kotlin中可以通过列表是否可变选择合适的接口来轻易控制,当它是可读列表可以传递更具体的元素类型的列表,如果是可变的就不行

类、类型和子类型

有时候我们会把类型和类当成同样的概念来使用。在非泛型类中,类的名称可以直接当作类型使用。一个kotlin类都可以用于构造可空和非空类型。在泛型类中,要得到一个合法的类型,需要用一个作为类型实参的具体类型替换(泛型)类的类型实参。List不是一个类型,它是一个类,但List<Int>这些都是合法的类型。每一个泛型类都可能生成潜在的无限数量的类型

子类型:任何时候如果需要的是类型A的值,你都能够使用类型B的值当作A的值,类型B就称为类型A的子类型,所有类型都是它自己的子类型。

超类型是子类型的反义词

简单的情况下,子类型和子类本质上意味着一样的事物,比如Int类是Number的子类,因此Int类型也是Number类型的子类型。如果一个类实现了一个接口,它的类型就是该接口类型的子类型,比如String是CharSequence的子类型

不一样的情况:一个非空类型是它的可空版本的子类型,但它们都对应着同一个类你始终能在可空类型的变量中存储非空类型的值,但反过来不行。由于安全性问题,所以我们不能把MutableList<String>看作是MutableList<Any>的子类型。

不变型

一个泛型类如MutableList,如果对于任意两种类型A和B,MutableList<A>既不是MuableList<B>的子类型也不是它的超类型,它就被称为在该类型参数上是不变型的。java中所有的类都是不变型的

协变:保留子类型化关系

一个协变类是一个泛型类,以Producer为例,对这种类来说,如果A是B的子类型,那么Producer<A>Producer<B>的子类型,我们说子类型化被保留了。比如说Producer<Cat>Producer<Animal>的子类型,因为Cat是Animal的子类型

要声明类在某个类型参数上是可以协变的,在该类型参数的名称前加上out关键字

在这里插入图片描述

将一个类的类型标记为协变的,在该类型实参没有精确匹配到函数中定义的类型实参时,可以让该类的值作为这些函数的实参传递,也可以作为这些函数的返回值。

在这里插入图片描述

如果尝试把猫群传递给feedall函数会得到类型不匹配的错误。因为Herd类中的类型参数T没有用任何变型修饰符,此时猫群不是畜群的子类。我们可以使用显式类型转换解决,但这并不是解决类型不匹配问题的正确方式

使用协变解决类型不匹配问题

在这里插入图片描述

不能把任何类都变成协变的,这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用的可能性。要保证类型安全,它只能用在所谓的out位置意味着这个类只能生产类型T的值而不能消费它们

一个约定:一个泛型类或者泛型接口,它的参数列表是接受数据的地方,称它为in位置,它的返回值是输出数据的地方,称它为out位置

类的类型参数前的out关键字要求所有使用T的方法只能把T放在out位置而不能放在in位置,这个关键字约束了使用T的可能性,但保证了对应子类型关系的安全性

以Herd类为例

在这里插入图片描述

这是一个out位置,可以安全地把类声明成协变的,如果Herd<Animal>类的get方法返回Cat是可以正常工作的,因为此时Cat是Animal的子类型

out的两层含义
1.子类型化会被保留(Producer<Cat>Producer<Animal>的子类型)
2.T只能用在out位置

查看List接口

在这里插入图片描述

在这里插入图片描述

List为只读接口所以只有一个返回类型为E的get方法,所以可以是协变的

注意!类型形参不仅可以直接当作类型参数或者返回类型使用,还可以当作另一个类型的类型实参

在这里插入图片描述

但不能把MutableList<T>声明成协变的,因为它的T会出现在in和out两个位置上

构造方法的参数既不在in也不在out位置,即使类型参数声明成了out,仍然可以在构造方法参数的声明中使用它。如果把类的实例当成一个更泛化的实例使用,变型会防止该实例被误用,不能调用存在潜在危险的方法,构造方法不是那种在实例创建后还能调用的方法,因此不会有潜在危险

在这里插入图片描述

若你在构造方法中使用了val和var关键字,同时就会声明一个getter和settet(若属性可变),此时T不能用out标记,因为类包含属性的setter,它在in位置用到了T

位置规则只覆盖了类外部可见的(public protected internal)私有方法的参数既不在in位置也不在out位置,变型规则只会防止外部使用者对类的误用但不会对类自己的实现起作用

在这里插入图片描述

逆变:反转子类型化关系

逆变可看作是协变的镜像:对逆变类来说,它的子类型化关系与用作类型实参的类的子类型化关系是相反的

在这里插入图片描述

在这里插入图片描述

sortedWith函数期望一个Comparator<String>(一个可以比较字符串的比较器),传给它一个能比较更一般的类型的比较器是安全的。如果你要在特定类型的对象上执行比较,可以使用能处理该类型或者它的超类型的比较器。这说明Comparator<Any>Comparator<String>的子类型,其中Any是String的超类型。不同类型之间的子类型关系和这些类型的比较器之间的子类型化关系截然相反

如果B是A的子类型,那么Consumer<A>就是Consumer<B>的子类型。类型参数A和B交换了位置,所以我们说子类型化被反转了,例如Consumer<Animal>就是Consumer<Cat>的子类型

in关键字的意思是,对应类型的值是传递进来给这个类的方法的,并且被这些方法消费。约束类型参数的使用将导致特定的子类型化关系,在类型参数T上的in关键字意味着子类型化被反转了

一个类可以在一个类型参数上协变,同时在另一个类型参数上逆变

kotlin的表示法(P)->R是表达Function<P,R>的另一种更具可读性的形式

在这里插入图片描述
在这里插入图片描述

上图是一高阶函数尝试对所有的猫进行迭代,你可以把一个接收任意动物的lambda传给它,其中Animal是Cat的超类型,Int是Number的子类型

使用点变型:在类型出现的地方指定变型

在类声明的时候就能够指定变型修饰符是很方便的,因为这些修饰符会应用到所有类被使用的地方,这被称作声明点变型在java中每一次使用带类型参数的类型的时候,还可以指定这个类型参数是否可以用他的子类型或者超类型替换,这叫作使用点变型

声明点变型比java通配符带来了更简洁的代码,因为只用指定一次变型修饰符

kotlin也支持点变型,允许在类型参数出现的具体位置指定变型,即使在类型声明时它不能被声明成协变或者逆变的

对于MutableList这样的接口来说,通常情况下既不是协变也不是逆变的。但是在某个特定函数中制备当成其中一种角色使用的情况却比较常见:要么是生产者,要么是消费者

在这里插入图片描述

这个函数从一个集合中把元素拷贝到另一个集合中,尽管两个集合都拥有不变型的类型,来源集合只用于读取,目标集合只用于写入,这种情况下,集合的元素类型不需要精确匹配,比如可以把一个字符串的集合拷贝到可包含任意对象的集合是没有问题的

在这里插入图片描述

在这个函数中,来源元素类型应该是目标列表元素的子类型

kotlin提供更优雅的表达方式,当函数的实现调用了那些类型参数只出现在out/in位置的方法时,可以利用这一点在函数定义中给特定用途的类型参数加上变型修饰符

在这里插入图片描述

可以为类型声明中类型参数任意的用法指定变型修饰符,用法包括:形参类型、局部变量类型、函数返回类型等。这里发生的一切被称作类型投影。我们说source不是一个常规的MutableList,而是一个投影(受限)的MutableList。只能调用返回类型是泛型类型参数的那些方法,严格地讲,只在out位置使用它的方法

kotlin的使用点变型直接对应java的限界通配符:MutableList<out T>对应java中的MutableList<? extends T>,MutableList<in T>对应Java中MutableList<? super T>

星号投影:使用*代替类型参数

星号投影语法可以用来标明你不知道关于泛型实参的任何信息,比如一个包含未知元素的列表:List<*>

需要注意MutableList<*>MutableList<Any?>不一样,前者是包含某种特定类型,后者是包含任意类型,不能向前者写入任何东西,因为你写入的任何值都可能违反调用代码的期望,但是可以读取

编译器会把MutableList<*>当成out投影的类型,MutableList<*>投影成了MutableList<out Any?>。*也对应了java的通配符?

对像Consumer<*>这样的逆变类型参数来说,星号投影等价于<in Nothing>在这种星号投影中无法调用任何签名中含有T的方法,如果类型参数是逆变的,它就只能表现为一个消费者,但我们不能让它消费任何东西

当类型实参的信息不重要时可以用星号投影的语法

在这里插入图片描述

或者引入一个泛型类型参数

例子
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

不能使用类型为FieldVr<*>的验证器来验证字符串,因为当我们尝试把具体类型的值传给未知类型的验证器是不安全的。一种修正方法是把验证器显式转换成需要的类型,不过也是不安全的,因为此时得到的验证器键值类型可能和转换类型不一样。我们可以把其进行封装,保证只有正确的验证器被注册和返回

### Kotlin 实化(Reified Generics)详解 #### 什么是实化? 在 Kotlin 中,默认情况下,由于 JVM 的 **擦除** 特性,在运行时无法获取的具体类信息。然而,通过使用 `inline` 函数以及 `reified` 关键字,可以使参数在运行时保留其具体类信息[^4]。 这种特性被称为 **实化 (reified generics)**,它使得开发者能够在运行时访问的实际类参数。 --- #### 如何使用实化? 要使实化生效,需满足以下条件: 1. 方法必须是一个内联函数 (`inline`)。 2. 参数前需要加上 `reified` 关键字。 下面是一个简单的例: ```kotlin inline fun <reified T> isA(value: Any): Boolean { return value is T } fun main() { println(isA<String>("Hello")) // 输出 true println(isA<Int>("Hello")) // 输出 false } ``` 在这个例中,`isA<T>` 函数能够判断传入的对象是否属于指定的类 `T`。这是因为在运行时,`T` 实际上被替换为了具体的类,而不是像普通那样被擦除了。 --- #### 运行时类的访问 借助实化,我们还可以直接操作类本身的信息。例如,可以通过反射获取类名或其他元数据: ```kotlin inline fun <reified T> getTypeName(): String { return T::class.java.name } fun main() { println(getTypeName<String>()) // 输出 java.lang.String println(getTypeName<Int>()) // 输出 java.lang.Integer } ``` 在这里,`T::class.java` 提供了对实际类的运行时访问能力。 --- #### 使用场景分析 ##### 场景 1:动态类检查 当需要频繁执行类检查或转换时,实化非常有用。例如: ```kotlin inline fun <reified T> List<*>.filterIsInstance(): List<T> = this.filter { it is T }.map { it as T } fun main() { val mixedList = listOf<Any>(1, "two", 3, "four") val stringsOnly = mixedList.filterIsInstance<String>() println(stringsOnly) // 输出 ["two", "four"] } ``` 上述代码展示了如何过滤列表中的特定类元素。 --- ##### 场景 2:简化工厂模式 假设有一个通用的工厂方法用于创建对象实例,可以利用实化减少冗余代码: ```kotlin inline fun <reified T : Any> createInstance(vararg params: Any?): T? { try { val constructor = T::class.constructors.firstOrNull() ?: throw IllegalArgumentException("No suitable constructor found.") return when (params.size) { 0 -> constructor.call() else -> constructor.call(*params) } } catch (e: Exception) { e.printStackTrace() return null } } data class Person(val name: String) fun main() { val person = createInstance<Person>("Alice") println(person?.name) // 输出 Alice } ``` 此示例演示了如何基于实化动态调用不同类的构造函数。 --- ##### 场景 3:日志记录与调试工具 在开发过程中,有时需要打印变量的类以便于调试。此时也可以应用实化技术: ```kotlin inline fun <reified T> logType(variable: T?) { println("${variable?.javaClass?.simpleName} has a value of $variable") } fun main() { logType(42) // 输出 Integer has a value of 42 logType("Kotlin") // 输出 String has a value of Kotlin logType(null) // 输出 null has a value of null } ``` 这段代码实现了自动检测并显示任意变量及其对应类的简单功能。 --- #### 注意事项 尽管实化提供了强大的功能,但也存在一定的局限性: - 只能应用于内联函数(`inline`); - 对性能可能有一定影响,因为编译器会在每次调用位置展开该函数体; - 如果滥用可能导致代码膨胀问题。 因此建议仅在确实需要运行时类信息的情况下才考虑采用这种方式。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值