Kotlin学习笔记 - 泛型

1. 基本用法

class Box<T> {

    private var element: T? = null

    fun add(element: T) {
        this.element = element
    }

    fun get(): T? = element
}

2. 型变

型变包括 协变、逆变、不变 三种:

  • 协变:泛型类型与实参的继承关系相同
  • 逆变:泛型类型与实参的继承关系相反
  • 不变:泛型类型与实参类型相同

Java的型变

首先明确一点,Java不直接支持型变。通俗地讲,虽然Integer是Number的子类,但是在Java中,List<Integer>和List<Number>是没有关系的(List<Integer>不是List<Number>的子类)所以List<Integer>不能直接赋值给List<Number>。

如果Java支持型变,则会出现以下情况:

// error:以下代码实际上会编译报错
List<Number> numList = new ArrayList<Integer>();
// 假设以上代码能够编译通过,以下代码就会在运行时引发异常
// numList实际上操作的集合元素必须是Integer类型
numList.add("haha"); // ClassCastException

Java采用通配符的方式来处理型变的需要(泛型通配符)

public interface Collection<E> extends Iterable<E> {
    // Java泛型通配符上限(协变),<? extends E> 可用于接收 E 或 E 的子类型
    boolean addAll(Collection<? extends E> c);
    
    // Java泛型通配符下限(逆变),<? super E> 可用于接收 E 或 E 的父类型
    boolean removeIf(Predicate<? super E> filter);
}

以上是泛型通配符在Collection接口源码中的使用

  • 通配符上限
public static void test(List<? extends Number> list) {
    /*
     * 对于“通配符上限”语法而言,从该集合中取出元素是安全的。
     * 集合中的元素是Number或其子类型,但是不能往该集合中存入新的元素,
     * 因为无法预测该集合元素的实际类型是Integer还是Double,又或者是Number的其他子类型
     */
    for (Number num : list) {
        System.out.println(num);
    }
    // list.add() // 不能添加元素,因为不能确定元素的类型
}
  • 通配符下限
public static void test(List<? super Integer> list) {
    /*
     * 对于"通配符下限"语法而言,将对象传给泛型对象是安全的。
     * 该集合中的元素是Integer或其父类型,但是从该集合中取出元素是不安全,
     * 因为无法预测该集合元素的实际类型是Number还是Object
     */
    list.add(666);
    // 以下代码编译不通过,因为在取出元素时无法确认元素的类型,可能是Number类型,也可能是Object类型
    // Number number = list.get(0);
}

泛型的规律

  1. 通配符上限(泛型协变)意味着从中取出<font color=red>(out)</font>对象是安全的,但传入对象是不安全的
  2. 通配符下限(泛型逆变)意味着向其中传入<font color=red>(in)</font>对象是安全的,但取出对象是不安全的

Kotlin的型变

Kotlin处理泛型型变的规则就是根据泛型的规律而设计:

  1. 如果泛型只需要出现在方法的返回值申明中(不出现在形参的声明中),那么该方法就只是取出泛型对象,因此该方法就支持泛型协变(相当于通配符上限);如果一个类的所有方法都支持泛型协变,那么该类的泛型参数可使用out修饰。
  2. 如果泛型只需要出现在方法的形参声明中(不出现在返回值声明中),那么该方法就只是传入泛型对象,因此该方法就支持泛型逆变(相当于通配符下限);如果一个类的所有方法都支持泛型逆变,那么该类的泛型参数可使用in修饰。

声明处型变

  • 如果一个类的所有方法都支持泛型协变,那么该类的泛型参数可使用out修饰
class Box<out T> {
    private var element: T? = null
    
    fun get(): T? = element
}

fun main(args: Array<String>) {
    // 由于泛型使用out修饰,所以 Box<Int> 对象可以直接赋值给 Box<Number>
    var box: Box<Number> = Box<Int>()
}
  • 如果一个类的所有方法都支持泛型逆变,那么该类的泛型参数可使用in修饰
class Box<in T> {
    private var element: T? = null

    fun put(e: T) {
        this.element = element
    }
}

fun main(args: Array<String>) {
    // 由于泛型使用in修饰,所以 Box<Any> 对象可以直接赋值给 Box<Number>
    var box: Box<Number> = Box<Any>()
}

声明处型变的限制:</br>如果一个类中有的方法使用泛型声明返回值类型,有的方法使用泛型声明形参类型,那么该类就不能使用声明处型变。

使用处型变

class Box<T> {
    private var element: T? = null

    fun put(element: T) {
        this.element = element
    }

    fun get(): T? = element
}

fun main(args: Array<String>) {
    // 使用 out 修饰泛型
    var outBox: Box<out Number> = Box<Int>()
    // 不能调用put方法存入数据(编译报错)
    // outBox.put(18)
    val num: Number? = outBox.get()

    // 使用 in 修饰泛型
    var inBox: Box<in Int> = Box<Number>()
    inBox.put(18)
    // 不能确定返回值的类型,只能使用Any类型接收
    val e: Any? = inBox.get()
}

@UnsafeVariance注解

对于协变的类型,通常是不允许将泛型类型作为方法参数的类型,但是在某些情况下,我们需要在协变的情况下将泛型作为方法的参数类型,那么我们可以使用 @UnsafeVariance 注解来修饰泛型。

class Box<out T> {
    private var element: T? = null

    // 如果开发者自己可以保证类型安全,那么可以使用@UnsafeVariance注解让编译器不报错
    fun put(element: @UnsafeVariance T) {
        this.element = element
    }

    fun get(): T? = element
}

星投影

在Java中,当我们不确定泛型的具体类型是,可以使用 ? 来代替具体的泛型,比如

List<?> list = new ArrayList<String>();

在Kotlin也有类似的语法,可以使用 * 来指代相应的泛型映射

// 星投影
val list: List<*> = ArrayList<String>()

以下是官方对于星投影语法的解释:

  • <p>对于 Foo <out T>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <> 读取 TUpper 的值。</p>
  • <p>对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <> 等价于 Foo <in Nothing>。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <>。</p>
  • <p>对于 Foo <T>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>。</p>
简而言之,星投影就是:
  • 当 * 接收可协变的泛型参数 ( out T ) 时,* 映射的类型为 Any?
class Box<out T> {
    private var element: T? = null

    fun get(): T? = element
}

fun main(args: Array<String>) {
    val intBox: Box<Int> = Box()

    val numBox: Box<Number> = intBox
    val num: Number? = numBox.get()

    // 星投影,这里的 <*> 相当于 <out Int>,元素类型映射为 Any?
    val box: Box<*> = intBox
    val element: Any? = box.get()
}
  • 当 * 接收可逆变的泛型参数 ( in T ) 时,* 映射的类型为 Nothing
class Box<in T> {
    private var element: T? = null

    fun put(element: T) {
        this.element = element
    }
}

fun main(args: Array<String>) {
    // 星投影,这里的 <*> 相当于 <in Number>,元素类型映射为 Nothing
    val box: Box<*> = Box<Number>()
    box.put(/*element:Nothing*/) // 没有值可以传入
}
  • 当 * 接收不变的泛型参数 ( T ) 时,* 对于读取值类型映射为 Any?,而写值时类型映射为 Nothing
lass Box<T> {
    private var element: T? = null

    fun put(element: T) {
        this.element = element
    }

    fun get(): T? = element
}

fun main(args: Array<String>) {
    // 星投影
    val box: Box<*> = Box<Number>()
    // 这里的 <*> 在赋值时相当于 <in Int>,元素类型映射为 Nothing
    box.put(/*element:Nothing*/) // 没有值可以传入
    // 这里的 <*> 在取值时相当于 <out Int>,元素类型映射为 Any?
    val element: Any? = box.get()
}

3. 泛型函数

泛型函数就是在函数声明时定义一个或多个泛型,泛型的声明必须在 fun 与函数名之间

fun <T> test(a: T) {
    println(a)
}

fun main(args: Array<String>) {
    test<Int>(1) // <Int>可省略,类型通过参数自动推断
    test(2)
}

泛型函数也可以用于扩展函数

fun <T> T.test(): String {
    return "test(): ${this.toString()}"
}

fun main(args: Array<String>) {
    val num = 666
    // 显示指定泛型为 Int 类型,<Int>可省略
    println(num.test<Int>())

    // 不显示指定泛型的类型,编译器自动推断出泛型为 String 类型
    val str = "haha"
    println(str.test())
}

4. 具体化类型参数

Kotlin允许在内联函数中使用 reified 修饰泛型参数,这样就可以将该泛型参数变成一个具体化的类型参数。具体化类型参数后就可以在函数中将泛型当做一个普通类型来使用,比如可以使用 is、as 运算符。

比如,我们需要在list中找到指定类型的元素,原先的写法如下

fun <T> findData(list: List<*>, clazz: Class<T>): T? {
    for (e in list) {
        if (clazz.isInstance(e)) {
            @Suppress("UNCHECKED_CAST")
            return e as T
        }
    }
    return null
}

fun main(args: Array<String>) {
    val list = listOf(1, 6.6f, "haha")
    println(findData(list, String::class.java)) // haha
    println(findData(list, Float::class.javaObjectType)) // 6.6
}

使用具体化类型参数之后,代码如下:

// 很明显,代码变得更简洁了
inline fun <reified T> findData(list: List<*>): T? {
    for (e in list) {
        if (e is T) {
            return e
        }
    }
    return null
}

fun main(args: Array<String>) {
    val list = listOf(1, 6.6f, "haha")
    println(findData<String>(list))
    println(findData<Float>(list))
}

使用reified修饰的泛型参数,还可以对其使用反射

inline fun <reified T> test() {
    println(T::class.java)
}

fun main(args: Array<String>) {
    test<String>() // class java.lang.String
    test<Double>() // class java.lang.Double
}

5. 泛型边界(上界)

java中可以使用 extends 指定泛型的上界

class Box<T extends Number> {
    // ...
}

Koltin的实现:

class Box<T : Number> {
    // ...
}

如果需要指定多个边界,需要使用where子句(只能有一个父类上界,可以有多个接口上界)

class Box<T> where T : Number, T : Comparable<T> {
    // ...
}

PS:在Kotlin中,如果不指定边界,则默认边界是 Any#

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351