/ 今日科技快讯 /
华为荣耀将在下周面向欧洲市场发布搭载HMS服务(华为移动服务)的荣耀V30系列手机,这也将是华为旗下首款预装HMS生态的智能手机。荣耀V30去年年底已在国内市场上市,这是荣耀首款5G智能手机。那么问题来了,不能使用Google移动服务的手机在欧洲会不会有市场呢?我们拭目以待。
/ 作者简介 /
本篇文章来自谭嘉俊的投稿,分享了他对Kotlin开发中泛型型变的理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
谭嘉俊的博客地址:
https://juejin.im/user/593f7b33fe88c2006a37eb9b
/ 开始 /
本文章讲解的内容是泛型的型变,我写一个扩展Boolean的示例代码来应用我要讲的内容,示例代码如下:
https://github.com/TanJiaJunBeyond/BooleanExtensionDemo
先看下以下例子,代码如下:
List strings = new ArrayList();
// Java中禁止这样的操作
List
在Java中是禁止这样的操作的,我们看下Kotlin的写法,代码如下:
val strings: List = arrayListOf()
val anys: List = strings
在Kotlin中是允许这样的操作的,这是为什么呢?下面会详细解释。
在List中,List是基础类型,String是类型实参,现有两个List集合,分别是List和List,它们都具有相同的基础类型,但是类型实参不相同,并且String和Any存在父子关系,型变就是指List和List这两者存在什么关系。
/ 参数和实际参数 /
函数中的形参和实参
代码如下:
fun add(firstNumber: Int, secondNumber: Int): Int =
firstNumber + secondNumber
firstNumber和secondNumber就是形式参数,然后去调用这个函数,代码如下:
val first = 1
val second = 2
add(first, second)
first和second就是add函数的实际参数。
泛型中的形参和实参
代码如下:
class Fruit(var item: T)
T就是类型形参,然后使用这个类,代码如下:
val fruit = Fruit(100)
Int就是Fruit的类型实参,因为Kotlin具有类型推导特性,不必明确指明类型,所以其实可以写成如下代码:
val fruit = Fruit(100)
在这种情况下,Int依然是Fruit的类型实参。还有以下情况,请看代码:
// Collections.kt
public interface MutableList : List, MutableCollection {
// 省略部分代码
}
这里的E是List和MutableCollection的类型实参,同时是MutableList的类型形参。
结论
定义在里面就是形式参数,定义在外面就是实际参数。
/ 子类、超类、子类型、超类型 /
子类会继承超类,例如class Apple: Fruit(),Apple就是Fruit的子类,Fruit就是Apple的超类,那什么是子类型和超类型呢?它们的规则比子类和超类更加宽松,如果需要A类型的地方,都可以用B类型来代替,那么B类型就是A类型的子类型,A类型就是B类型的超类型。
例如String和String?,如果一个函数接收的是String?,我们传入的是String的话,编译器是不会报错的,但是如果一个函数接受的是String,我们传入的是String?的话,编译器就会提示我们可能会存在空指针的问题,所以String就是String?的子类型,String?就是String的超类型。
子类型化关系
如果需要A类型的地方,都可以用B类型来代替,那么B类型就是A类型的子类型,B类型到A类型之间的映射关系就是子类型化关系。
举个例子:List是List的子类型,所以List到List之间存在子类型化关系,List是List
协变
协变(convariant)就是保留子类型化关系,保证泛型内部操作该类型时是只读的,在Java中,带extends限定(上界)的通配符类型使得类型是协变的。
因为List是协变,String是Any的子类型,String是String?的子类型,所以List是List的子类型,List是List
out协变点
以下代码是标准的out协变点:
// T被声明为out
interface Producer {
// T作为只读属性的类型
val value: T
// T作为函数返回值的类型
fun produce(): T
// T作为只读属性的类型List泛型的类型实参
val list: List
// T作为函数返回值的类型List泛型的类型实参
fun produceList(): List
}
out协变点基本特征:出现的位置是只读属性的类型或者函数的返回值类型,它作为生产者的角色,请求向外部输出。
源码分析
在源码中,最为代表性就是List,代码如下:
// Collections.kt
// E被声明为out
public interface List : Collection {
override val size: Int
override fun isEmpty(): Boolean
// E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
override fun contains(element: @UnsafeVariance E): Boolean
// E作为函数返回值的类型Iterator泛型的类型实参
override fun iterator(): Iterator
// E作为函数形参的类型Collection泛型的类型实参,而且还加上了@UnsafeVariance注解,下面会解释
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
// E作为函数返回值的类型
public operator fun get(index: Int): E
// E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
public fun indexOf(element: @UnsafeVariance E): Int
// E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
public fun lastIndexOf(element: @UnsafeVariance E): Int
// E作为函数返回值的类型ListIterator泛型的类型实参
public fun listIterator(): ListIterator
// E作为函数返回值的类型ListIterator泛型的类型实参
public fun listIterator(index: Int): ListIterator
// E作为函数返回值的类型List泛型的类型实参
public fun subList(fromIndex: Int, toIndex: Int): List
}
逆变
逆变(contravariance)就是反转子类型化关系,保证泛型内部操作该类型时是只写的,在Java中,带super限定(下界)的通配符类型使得类型是逆变的。
因为Comparable是逆变,String是Any的子类型,String是String?的子类型,所以Comparable是Comparable的子类型,Comparable
in逆变点
以下代码是标准的in逆变点:
// T被声明为in
interface Consumer {
// T作为函数形参类型
fun consume(value: T)
// T作为函数形参的类型List泛型的类型实参
fun consumeList(list: List)
}
in逆变点基本特征:出现的位置是函数形参类型,它作为消费者,请求外部输入。
源码分析
在源码中,最为代表性就是Comparable,代码如下:
// Comparable.kt
// T被声明为in
public interface Comparable {
// T作为函数形参类型
public operator fun compareTo(other: T): Int
}
不型变
不型变就是既不被声明为out,也不被声明为in的泛型。
因为MutableList是不型变,虽然String是Any的子类型,String是String?的子类型,但是MutableList和MutableList之间没有任何关系,MutableList和MutableList
不型变的基本特征:可以出现在任何位置。
源码分析
在源码中,最为代表性就是MutableList,代码如下:
// Collections.kt
public interface MutableList : List, MutableCollection {
// E作为函数形参类型
override fun add(element: E): Boolean
// E作为函数形参类型
override fun remove(element: E): Boolean
// E作为函数形参的类型Collection泛型的类型实参
override fun addAll(elements: Collection): Boolean
// E作为函数形参的类型Collection泛型的类型实参
public fun addAll(index: Int, elements: Collection): Boolean
// E作为函数形参的类型Collection泛型的类型实参
override fun removeAll(elements: Collection): Boolean
// E作为函数形参的类型Collection泛型的类型实参
override fun retainAll(elements: Collection): Boolean
override fun clear(): Unit
// E作为函数形参类型
public operator fun set(index: Int, element: E): E
// E作为函数形参类型
public fun add(index: Int, element: E): Unit
// E作为函数返回值的类型
public fun removeAt(index: Int): E
// E作为函数返回值的类型MutableListIterator泛型的类型实参
override fun listIterator(): MutableListIterator
// E作为函数返回值的类型MutableListIterator泛型的类型实参
override fun listIterator(index: Int): MutableListIterator
// E作为函数返回值的类型MutableList泛型的类型实参
override fun subList(fromIndex: Int, toIndex: Int): MutableList
}
@UnsafeVariance
在上面说的List源码中,我们发现虽然List是协变的,但是有时出现的位置是逆变的位置,这是为什么呢?其实是可以出现在任何位置上,但是要保证以下两点定义:协变保证泛型内部操作类型时是只读的,逆变保证泛型内部操作类型时是只写的,大体上要遵循上面说的那几个out协变点和in逆变点。
我们可以通过加上@UnsafeVariance注解告诉编译器这个地方是合法、安全,让其通过编译,如果不加的话,编译器会认为你这里是不合法,编译不通过。
例如上面说的List源码中,有一个contains函数,这个函数的作用是检查此元素是否包含在此集合中,它的实现方法没有出现写操作,所以这里就可以加上@UnsafeVariance注解,让其通过编译器。
使用处型变和声明处型变
Java是使用使用处型变,有如下接口:
public interface IGeneric {
// 省略部分代码
}
Java是禁止这样的操作的:
private void setData(IGeneric item) {
// Java禁止这样的操作
IGeneric
我们应该写成如下这样:
private void setData(IGeneric item) {
IGeneric extends Object> newItem = item;
}
我们必须把newItem声明为IGeneric extends Object>,类型变得更复杂了,复杂的类型并没有给我们带来任何价值,这种就叫做使用处型变,我们看下Kotlin的写法吧,有如下接口:
// T被声明为out
interface IGeneric {
// 省略部分代码
}
有如下方法:
private fun setData(item: IGeneric) {
// 泛型IGeneric的类型实参是Any
val newItem: IGeneric = item
}
这种就做声明处型变,我们只需要在用out修饰符修饰T即可,语义简单了很多,当然Kotlin也可以使用使用处型变的,我们不再用out修饰符修饰T,代码如下:
interface IGeneric {
// 省略部分代码
}
然后我们在声明类型的时候加上out修饰符,代码如下:
private fun setData(item: IGeneric) {
// 泛型IGeneric的类型实参Any被声明为out
val newItem: IGeneric = item
}
/ 星投影 /
定义
有时候,我们对类型参数一无所知,但是仍然希望以安全的方式使用它,我们可以使用星投影,这个泛型类型的每个具体实例化是这个投影的子类型。
语法
对于Function,T是泛型Function的一个具有上界String的协变类型参数,Function<*>等价于Function,这意味着当T为未知时,我们可以安全地从Function<*>读取String的值。
对于Function,T是泛型Function的一个逆变类型参数,Function<*>等价于Function,这意味着当T为未知时,我们不能安全地写入Function<*>。
对于Function,T是泛型Function的一个不型变类型参数,Function<*>读取值时等价于Function,写入值时等价于Function。
如果一个泛型类型具有多个类型参数,那么它们每个类型参数都可以单独投影,例如:如果类型被声明为Function
Function<*, String>表示Function
Function
Function<*, >表示Function
要注意的是星投影非常像Java的原始类型,但是是安全的。
这里解释一下Nothing,Nothing是所有类型的子类型,源码如下:
public class Nothing private constructor()
应用
我们可以扩展Boolean,让其更具有函数式编程的味道,让链式调用更加顺滑,代码如下:
package com.tanjiajun.booleanextensiondemo
/**
* Created by TanJiaJun on 2020-01-28.
*/
sealed class BooleanExt
class TransferData(val data: T) : BooleanExt()
object Otherwise : BooleanExt()
inline fun Boolean.yes(block: () -> T): BooleanExt =
when {
this -> TransferData(block.invoke())
else -> Otherwise
}
inline fun BooleanExt.otherwise(block: () -> T): T =
when (this) {
is Otherwise -> block()
is TransferData -> data
}
调用地方,代码如下:
package com.tanjiajun.booleanextensiondemo
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
/**
* Created by TanJiaJun on 2020-01-28.
*/
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 第一个例子
val name = "谭嘉俊"
(name == "谭嘉俊")
.yes { Log.i("TanJiaJun", name) }
.otherwise { Log.i("TanJiaJun", "苹果") }
// 第二个例子
val strings = mutableListOf(2, 4, 6, 8, 10)
(strings
.filter { it % 2 == 0 }
.count() == strings.size)
.yes { Log.i("TanJiaJun", "是偶数集合") }
.otherwise { Log.i("TanJiaJun", "不是偶数集合") }
}
}
我们可以看到密封类BooleanExt,它是个泛型,T是一个协变类型参数,为什么要用到协变呢?我们可以观察到T都出现在out协变点,所以T可以被声明为out。
我们还看到对象Otherwise继承密封类BooleanExt,我使用了Nothing,为什么要使用Nothing呢?因为在Boolean的扩展函数yes中返回的是BooleanExt,如果要返回Otherwise,我们就只能使用Nothing,因为Nothing是所有类型的子类型,上面也提及过,所以这样就符合协变的定义了。
/ 题外话 /
PECS原则
PECS原则是指Producer-Extends, Consumer-Super,它是Effective Java提出来的,如果泛型的类型实参是生产者,那么就应该用extends;如果泛型的类型实参是消费者,那么就应该用super。
密封类
在我的示例代码中,我用到了sealed这个修饰符,它可以声明一个密封类,我这里大概说下密封类:
密封类用来表示受限的类继承结构,意思就是当一个值为有限几种的类型,而不能有任何其他类型,其实他们在某种意义上,有点像枚举类的扩展,不过枚举类型的值集合是受限的,每个枚举常量只存在一个实例,而密封类的一个子类可以有包含状态的多个实例。
密封类可以有子类,但是所有子类必须在与密封类自身相同的文件中声明。
密封类自身是抽象的,它不能直接实例化,但是可以有抽象成员。
密封类不允许有非private构造函数,它的构造函数就是private的。
扩展密封类子类的类可以放在任何位置,而不需要放在同一个文件中。
使用密封类还有个好处在于使用when表达式的时候,当我们用when作为表达式的时候,也就像上面示例代码中的otherwise,我是使用了它的结果,而不是作为语句,如果能够验证语句覆盖了所有情况的时候,我们就不需要再为语句添加一个else子句了。
推荐阅读:
Dart语言快速入门
为了KPI,对APK进行极限优化!
Google官方提供的分页加载解决方案
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注