泛型类 & 泛型办法
泛型,指的是详细的类型泛化,多用在调集中(如List
、Map
),编码时运用符号代替,在运用时再确定详细类型。
泛型一般用于类和办法中,称为泛型类、泛型办法,运用示例:
/**
* 泛型类
*/
abstract class BaseBook<T> {
private var books: ArrayList<T> = ArrayList()
/**
* 泛型办法
*/
fun <E : T> add(item: E) {
books.add(item)
println("list:$books, size:${books.size}")
}
}
/**
* 子类承继BaseBook并传入泛型参数MathBook
*/
class BookImpl : BaseBook<MathBook>()
fun main() {
BookImpl().apply {
add(MathBook("数学"))
}
}
执行main()
办法,输出:
list:[MathBook(math=数学)], size: 1
Java泛型通配符
? extends E 界说上界
Java
中的泛型是不型变的,举个比如:Integer
是Object
的子类,可是List<Integer>
并不是List<Object>
的子类,由于List
是不型变的。如:

List<Integer>
成为List<Object>
的子类,能够经过上界操作符 ? extends E
来操作。
? extends E
表示此办法承受 E
或许 E
的 一些子类型对象的调集,而不只是 E
自身。extends
操作符能够约束上界通配符类型,使得通配符类型是协变的。注意,经过协变之后,数据是可读不可写的。示例:
//承继联系Child -> Parent
class Parent{
protected String name = "Parent";
}
class Child extends Parent {
protected String name = "Child";
}
界说实体类,承继联系:Child -> Parent
class CList<E> {
//经过<? extends E>来界说上界
public void addAll(List<? extends E> list) {
//...
}
}
/**
* <? extends E>来界说上界,能够确保协变性
*/
public void GExtends() {
//1、Child是Parent的子类
Parent parent = new Child();
//2、协变,泛型参数是Parent
CList<Parent> objs = new CList<>();
List<Child> strs = new ArrayList<>(); //声明字符串List
strs.add(new Child());
objs.addAll(strs); //addAll()办法中的入参有必要为List<? extends E>,然后确保了List<Child>是List<Parent>的子类。
}
addAll()
办法中的入参有必要为List<? extends E>
,然后确保了List<Child>
是List<Parent>
的子类。假如addAll()
中的入参改为List<E>
,则编译器会直接报错,由于List<Child>
并不是List<Parent>
的子类,如下:

? super E 界说下界
? super E
能够看作一个E
或许E
的父类的“不知道类型”
,这里的父类包含直接和直接父类。super
界说泛型的下界,使得通配符类型是逆变的。经过逆变之后,数据是可写不可读的,如: List<? super Child>
是 List<Parent>
的一个超类。示例:
class CList<E> {
//经过<? super E>来界说下界
public void popAll(List<? super E> dest) {
//...
}
/**
* 逆变性
*/
public void GSuper(){
CList<Child> objs = new CList<>();
List<Parent> parents = new ArrayList<>(); //声明字符串List
parents.add(new Parent());
objs.popAll(parents); //逆变
}
能够看到popAll()
的入参有必要声明为List<? super E>
,假如改为List<E>
,编译器会直接报错:

Kotlin泛型
和 Java
相同,Kolin
泛型自身也是不能型变的。
- 运用关键字
out
来支持协变,等同于Java
中的上界通配符? extends T
。 - 运用关键字
in
来支持逆变,等同于Java
中的下界通配符? super T
。
声明处型变
协变< out T>
interface GenericsP<T> {
fun get(): T //读取并回来T,能够以为只能读取T的对象是生产者
}
如上声明了GenericsP< T>
接口,假如其内部只能读取并回来T
,能够以为GenericsP
实例对象为生产者(回来T
)。
open class Book(val name: String)
data class EnglishBook(val english: String) : Book(english)
data class MathBook(val math: String) : Book(math)
已知EnglishBook、MathBook
为Book
的子类,可是假如将Book、EnglishBook
当成泛型放入GenericsP
,他们之间的联系还建立吗?即:

EnglishBook
是Book
的子类,可是GenericsP<EnglishBook>
并不是GenericsP<Book>
的子类,假如想让这个联系也建立,Kotlin
供给了out
修饰符,out
修饰符能够确保:
1、T只能用于函数回来中,不能用于参数输入中; 2、GenericsP< EnglishBook>能够安全的作为GenericsP< Book>的子类
示例如下:
interface GenericsP<out T> {
fun get(): T //读取并回来T,能够以为只能读取T的对象是生产者
// fun put(item: T) //错误,不允许在输入参数中运用
}
经过如上的改动后,能够看到GenericsP<EnglishBook>
能够正确赋值给GenericsP<Book>
了:

逆变< in T>
interface GenericsC<T> {
fun put(item: T) //写入T,能够以为只能写入T的对象是顾客
}
如上声明了GenericsC<T>
接口,假如其内部只能写入T
,能够以为GenericsC
实例对象为顾客(消费T
)。为了确保T
只能出现在参数输入位置,而不能出现在函数回来位置上,Kotlin
能够运用in
进行操控:
interface GenericsC<in T> {
fun put(item: T) //写入T,能够以为只能写入T的对象是顾客
//fun get(): T //错误,不允许在回来中运用
}
继续编写如下函数:
/**
* 称为GenericsC在Book上是逆变的。
* 跟体系源码中的Comparable类似
*/
private fun consume(to: GenericsC<Book>) {
//GenericsC<Book>实例赋值给了GenericsC<EnglishBook>
val target: GenericsC<EnglishBook> = to
target.put(EnglishBook("英语"))
}
能够看到GenericsC
中的泛型参数声明为in
后,GenericsC<Book>
实例能够直接赋值给了GenericsC<EnglishBook>
,称为GenericsC
在Book
上是逆变的。在体系源码中我们常常运用的一个比如便是Comparable
:
//Comparable.kt
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}
运用途型变(类型投影)
上一节中in、out
都是写在类的声明处,然后操控泛型参数的运用场景,可是假如泛型参数既可能出现在函数入参中,又可能出现在函数回来中,典型的类便是Array
:
class Array<T>(val size: Int) {
fun get(index: Int): T { …… }
fun set(index: Int, value: T) { …… }
}
这时候就不能在声明处做任何协变/逆变的操作了,如下函数中运用Array
:
fun copy(from: Array<Any>, to: Array<Any>) {
if (from.size != to.size) return
for (i in from.indices)
to[i] = from[i]
}
调用方:
val strs: Array<String> = arrayOf("1", "2")
val any = Array<Any>(2) {}
copy(strs, any) //编译器报错 strs其类型为 Array<String> 但此处希望 Array<Any>
错误原因便是由于Array<String>
并不是Array<Any>
的子类,即不是协变的,这里是为了确保数据的安全性。假如能够确保Array< String>
传入copy()
函数之后不能被写入,那么就确保了安全性,已然我们在声明Array
时不能约束泛型参数,那么完全能够在运用途进行约束,如下:
fun copy(from: Array<out Any>, to: Array<Any>) {
if (from.size != to.size) return
for (i in from.indices)
to[i] = from[i]
}
能够看到对from
添加了out
约束,这种被称为运用途型变。即不允许from
进行写入操作,那么就能够确保了from
的安全性,再进行上面的调用时,copy(strs, any)
就能够正确的执行了。
星投影< *>
当不运用协变、逆变时,某些场景下能够运用<*>
来完成泛型,如:
- 关于
GenericsP<out T: Book>
,GenericsP< *>
相当于GenericsP<out Book>
,当T
不知道时,能够安全的从GenericsP<*>
中读取Book
值; - 关于
GenericsC<in T>
,GenericsC<*>
相当于GenericsC<in Nothing>
,当T
不知道时,没有安全方法写入GenericsC<*>
; - 关于
Generics<T: Book>
,T
为有上界Book
的不型变参数,当Generics<*>
读取时等价于Generics<out Book>
;写入时等价于Generics<in Nothing>
。
泛型擦除
泛型参数会在编译期间存在,在运转期间会被擦除,例如:Generics<EnglishBook>
与 Generics<MathBook>
的实例都会被擦除为 Generics<*>
。运转时期检测一个泛型类型的实例无法经过is
关键字进行判别,别的运转期间详细的泛型类型判别也无法判别,如: books as List<Book>
,只会对非泛型部分进行检测,形如:books as List<*>
。
假如想详细化泛型参数,能够经过inline + reified
的方法:
/**
* inline + reified 使得类型参数被实化 reified:实体化的
* 注:带reified类型参数的内联函数,Java是无法直接调用的
*/
inline fun <reified T> isAny(value: Any): Boolean {
return value is T
}
参阅
【1】https://www.kotlincn.net/docs/reference/generics.html
【2】https://mp.weixin.qq.com/s/vSwx7fgROJcrQwEOW7Ws8A
【3】https:///post/7042606952311947278