• 作者:老汪软件技巧
  • 发表时间:2024-08-27 00:03
  • 浏览量:

面试官的题库,让一部分人先拿Offer!

本文主要学习Kotlin的泛型(Generics),包括泛型基础、使用处型变、声明处型变

泛型,这个概念在很多编程语言里面都存在。在中大型软件开发当中,我们对泛型的使用也十分频繁,因为它可以让我们在不同类型之间复用相似的逻辑代码。然而想要学好泛型却不是一件容易的事情,这是因为,泛型实在太抽象了我们都知道,程序其实是对真实世界的抽象,可是泛型,它是对程序的抽象

我们就拿“遥控器”这个生活中常见的物件儿,来聊聊它跟“泛型”之间,都能产 生哪些联系。

掌握泛型基础

在现实生活中,我们能看到各式各样的电视机遥控器,比如小米就有 1S、2S、3S、4S 电视遥控器。那么,如果我们将遥控器的概念迁移到程序的世界,我们就需要定义各种各样的“遥控器类”, 比如

如果我们为每一个型号的电视机都创建一个对应的遥控器类,我们的工作量会很大,而且没有意义。这个时候,我们其实需要一个万能遥控器,而借助 Kotlin 的泛型,我们就可以很容易地实现 了

我们定义了一个“万能遥控器类”Controller,它当中的字母 T 代表了, 这个遥控器可以控制很多种型号的电视,至于我们到底想要控制哪种型号,在使用的时候,只需要把 T 替换成实际的电视机型号即可可见,使用泛型的好处就在于,我们可以复用程序代码的逻辑,借助这个特性,我们可以在程序的基础上再做一次抽象。这样,通过这个Controller,不管将来有多少型号的电视机,我们都可以用这一个类来搞定

型变(Variance)

首先,型变是什么呢?简单来说,它就是为了解决泛型的不变性问题。事实上,型变讨论的是:在已知 Cat 是 Animal 的子类的情况下,MutableList与MutableList之间是什么关系。 在正常情况下,编译器会认为它们两者是没有任何关系的。换句话,也就是说,泛型是不变 的。在某些特定场景下,编译器这种行为还是会给我们带来麻烦的。而这个时候,就需要泛型的逆变与协变了。具体是什么特定场景呢?

逆变(Contravariant)

让我们继续以前面的遥控器为例

在这里,我们有一个电视机的父类,叫做 TV,另外还有一个子类,叫做 XiaoMiTV1。它们两 者是继承关系。由于它们是父子的关系,当函数的参数需要 TV 这个父类的时候,我们是可以 传入子类作为参数的。这很好理解

现在问题来了,Controller和Controller之间是什么关系呢?让我们 来设想一个买遥控器的场景:

在上面的代码中,我们的函数需要一个“小米电视 1 的遥控器”,在函数的内部,我们需要打开一台小米电视机。那么,当我们需要打开一台小米电视机的时候,我们是否可以用一个“万能的遥控器”呢?当然可以!

在这段代码中,由于我们传入的泛型实参是 TV,它是所有电视机的父类。因此,Controller 内部将会处理所有电视机型号的开机、关机。这时候,它就相当于一个万能遥控器,万能遥控器当然也可以打开小米电视 1。

从道理上来讲,我们的推理是没有错的,不过 Kotlin 编译器会报错,报错的内容是说“类型不匹配”,需要的是小米遥控器Controller,你却买了个万能遥控器Controller。在默认情况下,Kotlin 编译器就是这么认死理。所以,为了让我们的代码通过编译,我们需要主动告诉编译器一些额外的信息,具体的做法有两种

第一种做法,是修改泛型参数的使用处代码,它叫做使用处型变。具体做法就是修改 buy 函数的声明,在 XiaoMiTV1 的前面增加一个in关键字

第二种做法,是修改 Controller 的源代码,这叫声明处型变。具体做法就是,在泛型形参 T 的前面增加一个关键字 in:

我们使用以上任意一种方式修改后,代码就能够通过 Kotlin 编译了。这样修改之后,我们就可以使用Controller来替代Controller,也就是说,Controller是Controller的子类

协变逆变_协变逆变java_

:在这个场景下,遥控器与电视机之间的父子关系颠倒了。“小米电视”是“电视”的子类,但是,“万能遥控”成了“小米遥控”的子类。这种父子关系颠倒的现象,我们就叫做“泛型的逆变”。

协变(Covariant)

这次,我们仍然以一个生活中的场景来做分析。现在,请你想象一个点外卖的场景为了模拟这个场景,我们需要用代码来描述其中的几个角色:普通的食物、肯德基的食物,它们两者之间是父子关系

除此之外呢,我们还有一个饭店的角色:

在上面的 Restaurant 泛型参数处,我们传入不同的食物类型,就代表了不同类型的饭店。接下来,就是我们的点外卖方法了

如果我们直接运行上面的代码,会发现编译器提示最后一行代码报错,报错的原因同样是:“类型不匹配”,我们需要的是一家随便类型的饭店Restaurant,而传入的是肯德基Restaurant,不匹配。

是不是觉得很荒谬?既然随便找一家饭店就能点外卖,为什么肯德基不可以呢?

不过,有了上次的经验,这次我们就轻车熟路了,由于编译器认死理,我们必须额外提供一些信息给编译器,让它知道我们是在特殊场景使用泛型。具体的做法呢,还是有两种

第一种做法,还是修改泛型参数的使用处,也就是使用处型变。具体的做法就是修改orderFood() 函数的声明,在 Food 的前面增加一个** out** 关键字

第二种做法,是修改 Restaurant 的源代码,也就是声明处型变。具体做法就是,在它泛型形参 T 的前面增加一个关键字 out:

在做完以上任意一种修改以后,代码就可以通过编译了。这也就意味着,在这种情况下,我们可以使用Restaurant替代Restaurant,也就意味着Restaurant可以看作是Restaurant的子类

到了这时候,你会发现,食物与饭店它们之间的父子关系一致了。这种现象,我们称之为“泛型的协变”。上面两种修改的方式,就分别叫做使用处协变和声明处协变

虽然 Java 当中也有型变的概念,但是呢,Java 当中是没有声明处型变的。Java 里面只有使用处型变,下面是它们的语法对比

总结

在学完型变以后,也许你会有点迷惑:**到底什么时候用逆变,什么时候用协变?**如果你看过Kotlin 的官方文档,你会看到一句这样的话:Consumer in, Producer out !直译的话,大概意思就是:消费者 in,生产者 out。

们继续根据前面的遥控器、点外卖两个场景,来做个说明

我们模拟的是买遥控器的场景。请注意注释①的地方,我们的泛型 T,它最终会以函数的参数的形式,被传入函数的里面,这往往是一种写入行为,这时候,我们使用关键字 in。

我们模拟的是点外卖的场景。请注意注释②的地方,我们的泛型 T,它最终会以返回值的形式,被传出函数的外面,这往往是一种读取行为,这时候,我们使用关键字 out。

所以,如果要以更加通俗的语言来解释逆变与协变的使用场景的话,我们可以将其总结为:传入in,传出out。或者,我们也可以说:泛型作为参数的时候,用 in,泛型作为返回值的时候,用 out

最后,让我们来做一个总结吧。

当中很多源代码也都是借助泛型来实现的。

子关系和原来一致。

数和返回值的泛型参数,我们可以用 @UnsafeVariance来解决型变冲突。