目录
10 | 泛型:逆变or协变,傻傻分不清?
泛型基础
使用泛型可以复用程序代码的逻辑。
- 在
类名的后面加上<T>,可以为类增加泛型支持。
class TV
class MiTV : TV
class Controller1<T> // T 代表泛型的形参,【形参】的意思是:可以随便取一个名字作为类型,比如这里的 T
class Controller2<T : TV> // 指定泛型的上界:泛型实参必须是 TV 或其子类
fun main() {
Controller1<String>() // String 代表泛型的实参,【实参】的意思是:一个具体的类型,比如这里的 String
Controller1<MiTV>()
Controller2<MiTV>()
}
- 直接在
fun关键字的后面加上<T>,可以为函数增加泛型支持。
fun <T> turnOn(tv: T) = println(tv.toString())
fun <T : TV> turnOff(tv: T) = println(tv.toString())
泛型的不变性
如果 Cat、Dog 是 Animal 的子类,那么编译器会认为:
- 对象
Home<Cat>与对象Home<Animal>两者是 没有任何关系 - 集合
MutableList<Cat>与集合MutableList<Animal>两者是 没有任何关系
这就是 泛型的不变性。
泛型类的不变性
open class Animal
class Cat : Animal()
class Home<T>
fun foo1(home: Home<Animal>) = println("foo1")
fun foo2(home: Home<Cat>) = println("foo2")
fun main() {
foo1(Home<Animal>())
foo2(Home<Cat>())
//foo1(Home<Cat>()) // 需要父类泛型,传入子类泛型,会编译失败
//foo2(Home<Animal>()) // 需要子类泛型,传入父类泛型,会编译失败
}
- 需要携带泛型为 Animal 的对象时,如果对象携带泛型为 Cat,会编译失败
- 需要携带泛型为 Cat 的对象时,如果对象携带泛型为 Animal,会编译失败
泛型集合的不变性
open class Animal
class Dog : Animal()
class Cat : Animal()
fun foo1(list: MutableList<Animal>) { // 需要父类集合
list.add(Dog()) // 假如可以传入 Cat 集合,因为 Cat 集合不能存 Dog,那么这里就会出问题
}
fun foo2(list: MutableList<Cat>) { // 需要子类集合
val cat: Cat = list[0] // 假如可以传入 Animal 集合,因为 Animal 集合中可能有 Dog,那么这里就会出问题
}
fun main() {
val cats: MutableList<Cat> = mutableListOf(Cat())
val animals: MutableList<Animal> = mutableListOf(Dog()) // 创建一个 Animal 集合,并且有一个 Dog
foo1(animals)
foo2(cats)
//foo1(cats) // 需要父类集合,传入子类集合,会编译失败
//foo2(animals) // 需要子类集合,传入父类集合,会编译失败
}
- 需要 Animal 集合时,如果传入的是 Cat 集合,会编译失败
- 需要 Cat 集合时,如果传入的是 Animal 集合,会编译失败
泛型的型变 Variance
在某些场景下,泛型的不变性 会给我们带来麻烦,泛型的 型变,就是为了解决泛型的不变性问题。
-
逆变:
in-作为参数传入-<? super T>- 可以写入,不可以读取(只能以Any?读取) -
协变:
out-作为返回值传出-<? extends T>- 可以读取,不可以写入(只能写入Nothing)
总结
- 逆变 Covariant:泛型 T 最终会以函数的
参数的形式,被传入函数的里面,这往往是一种写入行为,这时候,使用关键字in - 协变 Contravariant:泛型 T,最终会以
返回值的形式,被传出函数的外面,这往往是一种读取行为,这时候,使用关键字out - Consumer in, Producer out :消费者使用
in,生产者使用out。 - 传入
in,传出out - 泛型作为参数用
in,泛型作为返回值用out - 函数传入参数的时候,并不一定就意味着写入
- 某些情况下,
val或private var,可以用out,因为其也满足可以读取不可以写入等特性 - 正常情况下,同时作为参数和返回值的泛型参数,无法直接使用
in或者out来修饰泛型 - 特殊场景下,同时作为参数和返回值的泛型参数,可以用
@UnsafeVariance解决型变冲突
声明处型变
声明处型变,就是修改泛型参数声明处的代码,即在泛型形参前加 in/out
- 逆变和协变,都属于型变
- Java 中没有
声明处型变,只有使用处型变 - 所谓的型变,对应到 Java 中,是指有
?的泛型- 类似
<? super Animal>或<? extends Animal>这种属于型变 - 类似
<T super Animal>或<T extends Animal>这种不属于型变
- 类似
声明处逆变
open class Animal
class Cat : Animal()
class Home<in T> // 声明处逆变,在泛型形参前加 in。Java 中没有声明处型变,因为不能在这里出现 <? super T>
fun foo2(home: Home<Cat>) = println("foo2")
fun main() {
foo2(Home<Cat>())
foo2(Home<Animal>())
}
- 修改后,当需要
Home<Cat>时,就可以传入Home<Animal>了 - 此时,可以认为
Home<Cat>是Home<Animal>的父类,这种父子关系颠倒的现象,就叫做 泛型的逆变
声明处协变
open class Animal
class Cat : Animal()
class Home<out T> // 声明处协变,在泛型形参前加 out。Java 中没有声明处型变,因为不能在这里出现 <? extends T>
fun foo1(home: Home<Animal>) = println("foo1")
fun main() {
foo1(Home<Animal>())
foo1(Home<Cat>())
}
- 修改后,当需要
Home<Animal>时,就可以传入Home<Cat>了 - 此时,仍可认为
Home<Animal>是Home<Cat>的父类,这种父子关系一致的现象,就叫做 泛型的协变
使用处型变
使用处型变,就是修改泛型参数使用处的代码,即在泛型实参声明前加 in/out
Java 中,在构造泛型对象时也支持
使用处型变,例如List<? extends Number> list=new ArrayList<>();
泛型类
open class Animal
class Cat : Animal()
class Home<T>
fun foo1(home: Home<out Animal>) = println("foo1") // 使用处协变,在泛型实参前加 out
fun foo2(home: Home<in Cat>) = println("foo2") // 使用处逆变,在泛型实参前加 in
fun main() {
foo1(Home<Animal>())
foo2(Home<Cat>())
foo1(Home<Cat>())
foo2(Home<Animal>())
}
泛型集合 -- 更常见的案例
-
逆变:
in-作为参数传入-<? super T>- 可以写入,不可以读取(只能以Any?读取) -
协变:
out-作为返回值传出-<? extends T>- 可以读取,不可以写入(只能写入Nothing)
open class Animal
class Dog : Animal()
class Cat : Animal()
fun foo1(list: MutableList<out Animal>) { // 使用处协变,在泛型实参前加 out
var animal: Animal = list[0] //协变,可以读取
//list.add(Dog()) // Type mismatch. Required Nothing, Found Dog
list.add(throw Exception()) // 协变,不可以写入(只能写入 Nothing)
}
fun foo2(list: MutableList<in Cat>) { // 使用处逆变,在泛型实参前加 in
//val cat: Cat = list[0] // Type mismatch. Required Cat, Found Any?
list.add(Cat()) //逆变,可以写入
val cat: Any? = list[0] // 逆变,不可以读取(只能以 Any? 读取)
}
fun main() {
val cats: MutableList<Cat> = mutableListOf(Cat())
val animals: MutableList<Animal> = mutableListOf(Dog())
foo1(animals)
foo2(cats)
foo1(cats)
foo2(animals)
}
星投影 Star-Projections
所谓的星投影就是,当我们不关心实参到底是什么的时候,可以用星号作为泛型的实参。
使用案例
class Home<T> {
fun getT(): T = // 返回一个 T 的实例,这里用到泛型实例的语法,暂时先不管他
}
fun getHome(): Home<*> = // 不关心实参到底是什么,就可以用星号 * 作为泛型的实参
fun main() {
val home: Home<*> = getHome() // 返回值类型是 Home<*>,无法确定其中泛型的实参是什么类型
val t: Any? = home.getT() // 无法确定返回值的类型,因此,这里【只能】看作是【Any?】类型
}
- 在 getHome() 中,我们没有传递任何具体的类型给 Home,而是使用了
星号作为 Home 的泛型实参,因此,我们就无法知道 Home 到底是什么类型。 - 相应的,当我们调用 home.getT() 的时候,就无法确定它的
返回值到底是什么类型。这时候,变量 t 的实际类型可能是任意的,比如 String、Int、Animal、Cat,甚至可能是 null,因此,我们只能将其看作是Any?类型。
增加上界
上面的代码中,如果我们为 Home 的泛型类型加上边界的话,t 的类型就可以更精确一些。
open class Animal
class Home<T : Animal> { // 为 泛型增加了上界 Animal
fun getT(): T =
}
fun getHome(): Home<*> =
fun main() {
val home: Home<*> = getHome()
val animal: Animal = home.getT() // 返回值是 Animal 或其子类
}
为 Home 泛型类型增加了上界 Animal 以后,即使使用了星投影,仍然可以通过调用 home.getT() 拿到 Animal 类型的变量。
小结
2016-05-14