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

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中也有型变,但是只有使用处没有声明处

KotlinJava
逆变ChargerCharger<? super XiaoMi>
协变ShopShop<? 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()
}

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

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

作者:无糖可乐爱好者

%s 个评论

要回复文章请先登录注册