泛型类 & 泛型办法

泛型,指的是详细的类型泛化,多用在调集中(如ListMap),编码时运用符号代替,在运用时再确定详细类型。

泛型一般用于类和办法中,称为泛型类、泛型办法,运用示例:

/**
 * 泛型类
 */
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中的泛型是不型变的,举个比如:IntegerObject的子类,可是List<Integer>并不是List<Object>的子类,由于List是不型变的。如:

Kotlin | 理解泛型使用
假如想让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>的子类,如下:

Kotlin | 理解泛型使用

? 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 | 理解泛型使用

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、MathBookBook的子类,可是假如将Book、EnglishBook当成泛型放入GenericsP,他们之间的联系还建立吗?即:

Kotlin | 理解泛型使用
能够看到编译器直接报错,由于尽管EnglishBookBook的子类,可是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>了:

Kotlin | 理解泛型使用

逆变< 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>,称为GenericsCBook上是逆变的。在体系源码中我们常常运用的一个比如便是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

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。