本文章已授权微信公众号郭霖(guolin_blog)转载。
本文章讲解的内容是泛型的型变,我写一个扩展Boolean的示例代码来应用我要讲的内容,示例代码如下:
先看下以下例子,代码如下:
List<String> strings = new ArrayList<String>();
// Java中禁止这样的操作
List<Object> objects = strings;
在Java中是禁止这样的操作的,我们看下Kotlin的写法,代码如下:
val strings: List<String> = arrayListOf()
val anys: List<Any> = strings
在Kotlin中是允许这样的操作的,这是为什么呢?下面会详细解释。
在List<String>中,List是基础类型,String是类型实参,现有两个List集合,分别是List<String>和List<Any>,它们都具有相同的基础类型,但是类型实参不相同,并且String和Any存在父子关系,型变就是指List<String>和List<Any>这两者存在什么关系。
形式参数和实际参数
函数中的形参和实参
代码如下:
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<T>(var item: T)
T就是类型形参,然后使用这个类,代码如下:
val fruit = Fruit<Int>(100)
Int就是Fruit的类型实参,因为Kotlin具有类型推导特性,不必明确指明类型,所以其实可以写成如下代码:
val fruit = Fruit(100)
在这种情况下,Int依然是Fruit的类型实参。
还有以下情况,请看代码:
// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> {
// 省略部分代码
}
这里的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<String>是List<Any>的子类型,所以List<String>到List<Any>之间存在子类型化关系,List<String>是List<String?>的子类型,所以List<String>到List<String?>之间存在子类型化关系,MutableList<String>和MutableList<Any>之间就没有关系,这个会在下面解释。
协变
协变(convariant)就是保留子类型化关系,保证泛型内部操作该类型时是只读的,在Java中,带extends限定(上界)的通配符类型使得类型是协变的。
因为List<out E>是协变,String是Any的子类型,String是String?的子类型,所以List<String>是List<Any>的子类型,List<String>是List<String?>的子类型。
out协变点
以下代码是标准的out协变点:
// T被声明为out
interface Producer<out T> {
// T作为只读属性的类型
val value: T
// T作为函数返回值的类型
fun produce(): T
// T作为只读属性的类型List泛型的类型实参
val list: List<T>
// T作为函数返回值的类型List泛型的类型实参
fun produceList(): List<T>
}
out协变点基本特征:出现的位置是只读属性的类型或者函数的返回值类型,它作为生产者的角色,请求向外部输出。
源码分析
在源码中,最为代表性就是List<out E>,代码如下:
// Collections.kt
// E被声明为out
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
// E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
override fun contains(element: @UnsafeVariance E): Boolean
// E作为函数返回值的类型Iterator泛型的类型实参
override fun iterator(): Iterator<E>
// 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>
// E作为函数返回值的类型ListIterator泛型的类型实参
public fun listIterator(index: Int): ListIterator<E>
// E作为函数返回值的类型List泛型的类型实参
public fun subList(fromIndex: Int, toIndex: Int): List<E>
}
逆变
逆变(contravariance)就是反转子类型化关系,保证泛型内部操作该类型时是只写的,在Java中,带super限定(下界)的通配符类型使得类型是逆变的。
因为Comparable<in T>是逆变,String是Any的子类型,String是String?的子类型,所以Comparable<Any>是Comparable<String>的子类型,Comparable<String?>是Comparable<String>的子类型。
in逆变点
以下代码是标准的in逆变点:
// T被声明为in
interface Consumer<in T> {
// T作为函数形参类型
fun consume(value: T)
// T作为函数形参的类型List泛型的类型实参
fun consumeList(list: List<T>)
}
in逆变点基本特征:出现的位置是函数形参类型,它作为消费者,请求外部输入。
源码分析
在源码中,最为代表性就是Comparable<in T>,代码如下:
// Comparable.kt
// T被声明为in
public interface Comparable<in T> {
// T作为函数形参类型
public operator fun compareTo(other: T): Int
}
不型变
不型变就是既不被声明为out,也不被声明为in的泛型。
因为MutableList<E>是不型变,虽然String是Any的子类型,String是String?的子类型,但是MutableList<String>和MutableList<Any>之间没有任何关系,MutableList<String>和MutableList<String?>之前没有任何关系。
不型变的基本特征:可以出现在任何位置。
源码分析
在源码中,最为代表性就是MutableList<E>,代码如下:
// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> {
// E作为函数形参类型
override fun add(element: E): Boolean
// E作为函数形参类型
override fun remove(element: E): Boolean
// E作为函数形参的类型Collection泛型的类型实参
override fun addAll(elements: Collection<E>): Boolean
// E作为函数形参的类型Collection泛型的类型实参
public fun addAll(index: Int, elements: Collection<E>): Boolean
// E作为函数形参的类型Collection泛型的类型实参
override fun removeAll(elements: Collection<E>): Boolean
// E作为函数形参的类型Collection泛型的类型实参
override fun retainAll(elements: Collection<E>): 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>
// E作为函数返回值的类型MutableListIterator泛型的类型实参
override fun listIterator(index: Int): MutableListIterator<E>
// E作为函数返回值的类型MutableList泛型的类型实参
override fun subList(fromIndex: Int, toIndex: Int): MutableList<E>
}
@UnsafeVariance
在上面说的List<out E>源码中,我们发现虽然List<out E>是协变的,但是有时出现的位置是逆变的位置,这是为什么呢?其实是可以出现在任何位置上,但是要保证以下两点定义:协变保证泛型内部操作类型时是只读的,逆变保证泛型内部操作类型时是只写的,大体上要遵循上面说的那几个out协变点和in逆变点。
我们可以通过加上@UnsafeVariance注解告诉编译器这个地方是合法、安全,让其通过编译,如果不加的话,编译器会认为你这里是不合法,编译不通过。
例如上面说的List<out E>源码中,有一个contains函数,这个函数的作用是检查此元素是否包含在此集合中,它的实现方法没有出现写操作,所以这里就可以加上@UnsafeVariance注解,让其通过编译器。
使用处型变和声明处型变
Java是使用使用处型变,有如下接口:
public interface IGeneric<T> {
// 省略部分代码
}
Java是禁止这样的操作的:
private void setData(IGeneric<String> item) {
// Java禁止这样的操作
IGeneric<Object> newItem = item;
}
我们应该写成如下这样:
private void setData(IGeneric<String> item) {
IGeneric<? extends Object> newItem = item;
}
我们必须把newItem声明为IGeneric<? extends Object>,类型变得更复杂了,复杂的类型并没有给我们带来任何价值,这种就叫做使用处型变,我们看下Kotlin的写法吧,有如下接口:
// T被声明为out
interface IGeneric<out T> {
// 省略部分代码
}
有如下方法:
private fun setData(item: IGeneric<String>) {
// 泛型IGeneric的类型实参是Any
val newItem: IGeneric<Any> = item
}
这种就做声明处型变,我们只需要在用out修饰符修饰T即可,语义简单了很多,当然Kotlin也可以使用使用处型变的,我们不再用out修饰符修饰T,代码如下:
interface IGeneric<T> {
// 省略部分代码
}
然后我们在声明类型的时候加上out修饰符,代码如下:
private fun setData(item: IGeneric<String>) {
// 泛型IGeneric的类型实参Any被声明为out
val newItem: IGeneric<out Any> = item
}
星投影
定义
有时候,我们对类型参数一无所知,但是仍然希望以安全的方式使用它,我们可以使用星投影,这个泛型类型的每个具体实例化是这个投影的子类型。
语法
- 对于Function<out T : String>,T是泛型Function的一个具有上界String的协变类型参数,Function<>等价于Function<out String>,这意味着当T为未知时,我们可以安全地从Function<>读取String**的值。
- 对于Function<in T>,T是泛型Function的一个逆变类型参数,Function<>等价于Function<in Nothing>,这意味着当T为未知时,我们不能安全地写入Function<>**。
- 对于Function<T : String>,T是泛型Function的一个不型变类型参数,Function<>读取值时等价于Function<out String>,写入值时等价于Function<in Nothing>*。
如果一个泛型类型具有多个类型参数,那么它们每个类型参数都可以单独投影,例如:如果类型被声明为Function<in T, out U>,那么它的星投影就如下:
- Function<, String>表示Function<in Nothing, String>*
- Function<String, >表示Function<String, out Any?>*
- Function<, >表示Function<in Nothing, out Any?>
要注意的是星投影非常像Java的原始类型,但是是安全的。
这里解释一下Nothing,Nothing是所有类型的子类型,源码如下:
public class Nothing private constructor()
应用
我们可以扩展Boolean,让其更具有函数式编程的味道,让链式调用更加顺滑,代码如下:
package com.tanjiajun.booleanextensiondemo
/**
* Created by TanJiaJun on 2020-01-28.
*/
sealed class BooleanExt<out T>
class TransferData<T>(val data: T) : BooleanExt<T>()
object Otherwise : BooleanExt<Nothing>()
inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> =
when {
this -> TransferData(block.invoke())
else -> Otherwise
}
inline fun <T> BooleanExt<T>.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<T>,如果要返回Otherwise,我们就只能使用Nothing,因为Nothing是所有类型的子类型,上面也提及过,所以这样就符合协变的定义了。
题外话
PECS原则
PECS原则是指Producer-Extends, Consumer-Super,它是Effective Java提出来的,如果泛型的类型实参是生产者,那么就应该用extends;如果泛型的类型实参是消费者,那么就应该用super。
密封类
在我的示例代码中,我用到了sealed这个修饰符,它可以声明一个密封类,我这里大概说下密封类:
-
密封类用来表示受限的类继承结构,意思就是当一个值为有限几种的类型,而不能有任何其他类型,其实他们在某种意义上,有点像枚举类的扩展,不过枚举类型的值集合是受限的,每个枚举常量只存在一个实例,而密封类的一个子类可以有包含状态的多个实例。
-
密封类可以有子类,但是所有子类必须在与密封类自身相同的文件中声明。
-
密封类自身是抽象的,它不能直接实例化,但是可以有抽象成员。
-
密封类不允许有非private构造函数,它的构造函数就是private的。
-
扩展密封类子类的类可以放在任何位置,而不需要放在同一个文件中。
-
使用密封类还有个好处在于使用when表达式的时候,当我们用when作为表达式的时候,也就像上面示例代码中的otherwise,我是使用了它的结果,而不是作为语句,如果能够验证语句覆盖了所有情况的时候,我们就不需要再为语句添加一个else子句了。
我的GitHub:TanJiaJunBeyond
Android通用框架:Android通用框架
我的掘金:谭嘉俊
我的简书:谭嘉俊
我的CSDN:谭嘉俊
网友评论