Kotlin 泛型边界型变及星投影使用详解

目录
  • 1.泛型
  • 2.型变
  • 3.型变—逆变
  • 4.型变—协变
  • 5.泛型边界
  • 6.星投影

1.泛型

Android项目开发过程中普遍会遇到一个问题:adapter的样式、业务逻辑很相似,但是需要的数据源不是来自一个接口,常规情况下就要定义多个构造函数但是这样就要更改构造函数的传参顺序或者增加传参要么就是将他们统一成一个类。但是用泛型就可以这样解决:

class CommonAdapter<T>(val list: List<T>) : RecyclerView.Adapter<CommonAdapter.ViewHolder>() {
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        TODO("Not yet implemented")
    }
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        TODO("Not yet implemented")
    }
    override fun getItemCount() = list.size
}
//调用CommonAdapter
mBinding.rvTab.adapter = CommonAdapter(listOf("周一","周二","周三"))
mBinding.rvTab.adapter = CommonAdapter(listOf(CommonData("张三"),CommonData("李四"),CommonData("王五")))

可以看到泛型可以很好的解决这个问题。

再举个例子,电子产品的充电器,在以前充电接口没有统一的时候每个设备都需要一个单独的充电器,这样就很麻烦,下面的代码就是对这个问题的一种表示

// 手机
class Phone {
    fun charging() {}
}
// 平板
class Pad {
    fun charging() {}
}
//无线耳机
class Headset {
    fun charging() {}
}
//便携音箱
class Speakers {
    fun charging() {}
}

统一充电器接口后就只需要保留一个充电器即可,这个概念就需要用到泛型了

//统一接口
class UnifiedInterface<T> {
    fun charging(device: T) {}
}
val phone = UnifiedInterface<Phone>()
phone.charging()
val pad = UnifiedInterface<Pad>()
pad.charging()

在统一接口UnifiedInterface中传入要充电的电子设备就可以了。T代表的就是各种电子设备。

这里要注意的一点是在使用泛型的时候还可以加上边界

class UnifiedInterface<T: Phone> {
    fun charging(device: T) {}
}

上面的代码中Phone就是边界,用【:】这个边界的声明就是说只能传入Phone类或者它的子类,传入Pad或者Headset都是不可以的。

2.型变

fun main() {
    func(mutableListOf<Phone>(Phone()))	//报错 这里应该传入Device类型的集合
}
fun func(list: MutableList<Device>) {
    list.add(Pad())
}
open class Device {
}
class Phone : Device() {
}
class Pad {
}

这里定义了一个方法,方法中的传参是Device类型的集合,调用的时候传入的Phone类型的集合,而DevicePhone是继承关系但是却无法传入Phone类型的集合。这是因为在默认情况下MutableList<Device>MutableList<Phone>之间不存在任何继承关系,他们也无法互相替代,这就是泛型的不变性

那么什么是型变?型变就是为了解决泛型的不变性问题。

3.型变—逆变

以手机为例,Android是手机类,还有XioMiHuaWei两个子类,它俩和Android是继承关系,那么Charger<XiaoMi>Charger<HuaWei>Charger<Android>之间有什么关系?

class Charger<T> {
    fun charging(device: T) {}
}
open class Android {
    open fun charging() {}
}
class XiaoMi : Android() {
    override fun charging() {
    }
}
class HuaWei() : Android() {
    override fun charging() {
        super.charging()
    }
}

假设,现在手机都没电了,需要用充电器充电,那么给XiaoMi手机充电就是这样

fun xiaoMiCharger(charger: Charger<XiaoMi>) {
    val xiaomi = XiaoMi()
    charger.charging(xiaomi)
}

但是还有一个HuaWei手机也要充电,是否可以用一个充电器?就像下面这样

fun main() {
    val charger = Charger<Android>()
    xiaoMiCharger(charger)				//报错:类型不匹配
    huaWeiCharger(charger)				//报错:类型不匹配
}

都是Android手机为什么不能充电?这主要是编译器不认为XiaoMi和HuaWei是Android手机,也就是说它们三者之间没有关系,这就是上面讲的不可变性, 此时逆变踩着欢快的脚步到来了。

  • 使用处型变
//				   				 修改处
//								   ↓
fun xiaoMiCharger(charger: Charger<in XiaoMi>) {
    val xiaomi = XiaoMi()
    charger.charging(xiaomi)
}
//				   				 修改处
//								   ↓
fun huaWeiCharger(charger: Charger<in HuaWei>) {
    val huaWei = HuaWei()
    charger.charging(huaWei)
}
class Charger<T> {
    fun charging(device: T) {}
}
fun main() {
    val charger = Charger<Android>()
    xiaoMiCharger(charger)
    huaWeiCharger(charger)
}
  • 声明处型变
//			修改处
//			  ↓
class Charger<in T> {
    fun charging(device: T) {}
}
fun xiaoMiCharger(charger: Charger<XiaoMi>) {
    val xiaomi = XiaoMi()
    charger.charging(xiaomi)
}
fun huaWeiCharger(charger: Charger<HuaWei>) {
    val huaWei = HuaWei()
    charger.charging(huaWei)
}
fun main() {
    val charger = Charger<Android>()
    xiaoMiCharger(charger)
    huaWeiCharger(charger)
}

加上in关键字之后就报错就消失了

为什么被叫做逆变?

上面的代码其实是将父子关系颠倒了,以使用处型变为例,我们把代码放到一起看看

fun main() {
    val charger = Charger<Android>()
    xiaoMiCharger(charger)
    huaWeiCharger(charger)
}
//Charger<Android> → Charger<XiaoMi> 成了颠倒关系
fun xiaoMiCharger(charger: Charger<in XiaoMi>) {
    val xiaomi = XiaoMi()
    charger.charging(xiaomi)
}
fun huaWeiCharger(charger: Charger<in HuaWei>) {
    val huaWei = HuaWei()
    charger.charging(huaWei)
}

这种父子颠倒的关系被称为逆变。

4.型变—协变

假设我要去商场买一个Android手机,它属于Phone类

open class Phone {
}
class Android : Phone() {
}
//商场什么都卖
class Shop<T> {
    fun buy(): T {
        TODO("Not yet implemented")
    }
}
//去商场买手机
fun buy(shop: Shop<Phone>) {
    val phone = shop.buy()
}
fun buy(shop: Shop<Phone>) {
    val phone = shop.buy()
}
fun main() {
    val android = Shop<Android>()
    buy(android)				//报错了,类型不匹配
}

Android是Phone的子类,但是Shop<Android>Shop<Phone>却没有关系,这依旧是Kotlin的不可变性,前面讲过通过in实现逆变, 但是它的父子关系就被颠倒了,那么这里的目的就是维持正确的父子关系——协变。

  • 使用处协变
class Shop<T> {
    fun buy(): T {
        TODO("Not yet implemented")
    }
}
//				   修改处
//					↓
fun buy(shop: Shop<out Phone>) {
    val phone = shop.buy()
}
fun main() {
    val android = Shop<Android>()
    buy(android)				//报错消失
}
  • 声明处协变
//		  修改处
//			↓
class Shop<out T> {
    fun buy(): T {
        TODO("Not yet implemented")
    }
}
fun buy(shop: Shop<Phone>) {
    val phone = shop.buy()
}
fun main() {
    val android = Shop<Android>()
    buy(android)				//报错消失
}

通过out就实现了协变, 父子关系也没有颠倒,关系图如下

Kotlin的型变的逆变、协变到这里就讲完了,Java中也有型变,但是只有使用处没有声明处

Kotlin Java
逆变 Charger Charger<? super XiaoMi>
协变 Shop Shop<? extends Phone>
//RxJava#ObservableAnySingle
public ObservableAnySingle(ObservableSource<T> source, Predicate<? super T> predicate) {
    this.source = source;
    this.predicate = predicate;
}
//String#join
public static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements) {
    Objects.requireNonNull(delimiter);
    Objects.requireNonNull(elements);
    StringJoiner joiner = new StringJoiner(delimiter);
    for (CharSequence cs: elements) {
        joiner.add(cs);
    }
    return joiner.toString();
}

逆变和协变有什么区别?怎么用?

//声明处逆变
class Charger<in T> {
    fun charging(device: T) {}
}
//声明处协变
//		  修改处
//			↓
class Shop<out T> {
    fun buy(): T {
        TODO("Not yet implemented")
    }
}

对比可以发现逆变主要用于传参,协变主要用于返回值,Kotlin的官方文档也有这么一句话: ****消费者 in, 生产者 out!

5.泛型边界

在讲协变的例子时我们要去商场Shop买手机,但是商场什么手机都卖,现在我想买Android手机,怎么保证我买的就是Android手机?加上一个边界就好了

//				变化在这里,加了一个边界,类似于var x: Int = 0
//					↓
class Shop<out T: Android> {
    fun buy(): T {
        TODO("Not yet implemented")
        }
}
fun main() {
    val android = Shop<Android>()
        buy(android)
        val ios = Shop<IOS>()		//报错:类型不匹配
        buy(ios)
    }
}

6.星投影

星投影就是用【】作为泛型的实参,当我们使用【】作为泛型的实参时也就意味着我们对具体的参数是什么并不感兴趣或者说不知道具体的参数是什么。

举例:还是买手机的案例,现在我不挑品牌了,只要能用就好,既然这样那就随便找家店铺好了

//				不指定具体参数
//					 ↓
fun findShop(): Shop<*> {
    TODO("Not yet implemented")
}
fun main(){
    val shop = findShop()
    val product: Any? = shop.buy()
}

这里的product什么手机都可以,甚至是其他物品都行,这里还定义了一个Any?也说明了可能是空手而归。

那么我只想买个手机,怎么才能避免买错成其他物品呢?添加边界

//只找Phone的店铺
//					↓ 这是边界
class Shop<out T: Phone> {
    fun buy(): T {
        TODO("Not yet implemented")
    }
}
fun findShop(): Shop<*> {
    TODO("Not yet implemented")
}
fun main() {
    val shop = findShop()
    //只要返回值是Phone的商品
    val product: Phone = shop.buy()
}

添加边界后就可以达到我只想买个手机的要求了。

泛型这一块比较抽象,一定要多看几遍,思考在项目中这个东西的应用场景在哪里。

以上就是Kotlin 泛型边界型变及星投影使用详解的详细内容,更多关于Kotlin 泛型型变星投影的资料请关注我们其它相关文章!

(0)

相关推荐

  • 一篇文章弄懂Java和Kotlin的泛型难点

    Java 和 Kotlin 的泛型算作是一块挺大的知识难点了,涉及到很多很难理解的概念:泛型型参.泛型实参.类型参数.不变.型变.协变.逆变.内联等等.本篇文章就将 Java 和 Kotlin 结合着一起讲,按照我的个人理解来阐述泛型的各个知识难点,希望对你有所帮助

  • 深入理解Kotlin的泛型系统

    前言 Kotlin 的泛型与 Java 一样,都是一种语法糖,只在源代码里出现,编译时会进行简单的字符串替换. 泛型是静态类型语言中不可缺少的一部分,Kotlin 的泛型定义和使用都类似 Java,但也有一些基于工程实践考虑的优化和改进. 泛型(Generics)其实就是把类型参数化,真正的名字叫做 类型参数,它给强类型编程语言加入了更强的灵活性.在 Java 中,只要是有类型的 API 元素,都可以泛型化,也就是泛型类.泛型接口.泛型方法和泛型属性,泛型类和泛型接口可以统称为泛型类型.其中最重

  • Kotlin泛型的型变之路演变示例详解

    目录 引言 协变 协变的限制 逆变 逆变的限制 Kotlin型变 Kotlin泛型的优化 申明处型变 reified 支持协变的List 获取泛型的具体类型 reified 传入指定Class 匿名内部类 反射 PESC 协变和逆变的使用场景 引言 之前就写过一篇泛型的文章,但是总觉得写得不够系统,所以最近对泛型又作了些研究,算是对这篇文章的补充了. kotlin之泛型 泛型,是为了让「类」.「接口」.「方法」具有更加通用的使用范围而诞生的,举个例子,假如我们不使用泛型,那么一个List中可以装

  • Kotlin 泛型详解及简单实例

     Kotlin 泛型详解 概述 一般类和函数,只能使用具体的类型:要么是基本类型,要么是自定义的类.如果要编写可以应用于多种类型的代码,这种刻板的约束对代码的限制很大.而OOP的多态采用了一种泛化的机制,在SE 5种,Java引用了泛型.泛型,即"参数化类型".一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参.那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用

  • Kotlin 基础教程之泛型

    Kotlin 支持泛型, 语法和 Java 类似. 例如,泛型类: class Hello<T>(val value: T) val box = Box<Int>(1) val box1 = Box(2) 泛型函数: fun <T> foo(item: T): List<T> { // do something } val list = foo<Int>(1) fun <T> T.toString2(): String { // 扩展

  • Kotlin 匿名类实现接口和抽象类的区别详解

    我就废话不多说了,还是上代码吧 接口: interface OnBind { fun onBindChildViewData(holder: String, itemData: Any, position: Int) } lesson.does(object : OnBind { override fun onBindChildViewData(holder: String, itemData: Any, position: Int) { println(holder + itemData +

  • java编程创建型设计模式工厂方法模式示例详解

    目录 1.什么是工厂方法模式? 2.案例实现 3.JDK中的工厂方法模式 1.什么是工厂方法模式? 工厂方法模式设计方案:  将披萨项目的实例化功能抽象成抽象方法,在不同的口味点餐子类中具体实现. 工厂方法模式:  定义了一个创建对象的抽象方法,由子类决定要实例化的类.工厂方法模式将对象的实例化推迟到子类. 何时使用?  不同条件下创建不用实例时.方法是让子类实现工厂接口. 2.案例实现 假如说,我们现在有这样一个需求:客户在点披萨时,可以点不同口味的披萨,比如北京的奶酪pizza.北京的胡椒p

  • Python+OpenCV实现鼠标画瞄准星的方法详解

    目录 函数说明 cv2.circle() cv2.line() 简单的例子 利用鼠标回调函数画瞄准星 所谓瞄准星指的是一个圆圈加一个圆圈内的十字线,就像玩射击游戏狙击枪开镜的样子一样.这里并不是直接在图上画一个瞄准星,而是让这个瞄准星跟着鼠标走.在图像标注任务中,可以利用瞄准星进行一些辅助,特别是回归类的任务,使用该功能可以使得关键点的标注更加精准. 关于鼠标回调函数的说明可以参考:opencv-python的鼠标交互操作 函数说明 import cv2后,可以分别help(cv2.circle

  • Kotlin协程Job生命周期结构化并发详解

    目录 1.Job的生命周期 2.Deffered 3.Job与结构化并发 4.launch和async的使用场景 前面在学习协程启动方式的时候在launch的源码中有一个返回值是Job,async的返回Deferred也是实现了Job,那么而也就是说launch和async在创建一个协程的时候也会创建一个对应的Job对象.还提到过Job是协程的句柄,那么Job到底是什么?它有什么用? 1.Job的生命周期 先看一下Job的源码,这里只保留了跟标题相关的内容 public interface Jo

  • Python实现字符型图片验证码识别完整过程详解

    1摘要 验证码是目前互联网上非常常见也是非常重要的一个事物,充当着很多系统的防火墙功能,但是随时OCR技术的发展,验证码暴露出来的安全问题也越来越严峻.本文介绍了一套字符验证码识别的完整流程,对于验证码安全和OCR识别技术都有一定的借鉴意义. 本文的基于传统的机器学习SVM的源码共享:https://github.com/zhengwh/captcha-svm 2关键词 关键词:安全,字符图片,验证码识别,OCR,Python,SVM,PIL 3免责声明 本文研究所用素材来自于某旧Web框架的网

  • C#中逆变的实际应用场景详解

    目录 前言 协变的应用场景 逆变的应用场景 讨论 总结 前言 早期在学习泛型的协变与逆变时,网上的文章讲解.例子算是能看懂,但关于逆变的具体应用场景这方面的知识,我并没有深刻的认识.本文将在具体的场景下,从泛型接口设计的角度出发,逐步探讨逆变的作用,以及它能帮助我们解决哪方面的问题? 这篇文章算是协变.逆变知识的感悟和分享,开始之前,你应该先了解协变.逆变的基本概念,以及依赖注入,这类文章很多,这里就不再赘述. 协变的应用场景 虽然协变不是今天的主要内容,但在此之前,我还是想提一下关于协变的应用

  • Java泛型<T> T与T的使用方法详解

    泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样. 在集合框架(Collection framework)中泛型的身影随处可见.例如,Map 类允许向一个 Map 类型的实例添加任意类的对象,即使最常见的情况在给定映射(map)中保存一个string键值对. 命名类型参数 对于常见的泛型模式,推荐的泛型类型

  • Kotlin作用域函数之间的区别和使用场景详解

    作用域函数 Kotlin 的作用域函数有五种:let.run.with.apply 以及 also. 这些函数基本上做了同样的事情:在一个对象上执行一个代码块. 下面是作用域函数的典型用法: val adam = Person("Adam").apply { age = 20 city = "London" } println(adam) 如果不使用 apply 来实现,每次给新创建的对象属性赋值时就必须重复其名称. val adam = Person("

  • Kotlin如何捕获上下文中的变量与常量详解

    Lambda表达式或匿名函数可以访问或修改其所在上下文中的变量和常量,这个过程被称为捕获. fun main(args: Array<String>) { //定义一个函数,该函数的返回值类型为()->List<String> fun makeList(ele: String): () -> List<String> { //创建一个不包含任何元素的List var list: MutableList<String> = mutableListO

  • Java使用通配符实现增强泛型详解

    目录 使用通配符增强泛型 1.题目 2.解题思路 3.代码详解 知识点补充 使用通配符增强泛型 1.题目 泛型是JAVA重要的特性,使用泛型编程,可以使代码复用率提高. 实现:在泛型方法中使用通配符 2.解题思路 创建一个类:WildcardsTest. 创建一个方法getMiddle()用于获得给定列表的中间值. 在泛型中,使用“?”作为通配符,通配符的使用与普通的类型参数类似,如通配符可以利用extends关键字来设置取值的上限.如 <? extends Number> 表示Byte,Do

随机推荐