尚学堂 老师好!

上海:15201841284

广州:020-2989 6995

深圳:0755-23061965

武汉:027-8798 9193

西安:029-8822 8155

Kotlin 泛型 —— 没有Java通配符麻烦

与 Java 类似,Kotlin 中的类也可以有类型参数:

 class Box<T>(t: T) {     var value = t }  更多精彩内容以及学习资料,尚学堂论坛bbs.bjsxt.com免费下载。  

一般来说,要创建这样类的实例,我们需要提供类型参数:

 val box: Box<Int> = Box<Int>(1) 

但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数:

 val box = Box(1) // 1 具有类型 Int,所以编译器知道我们说的是 Box<Int>。 

型变

Java 类型系统中最棘手的部分之一是通配符类型(参见 Java Generics FAQ)。 而 Kotlin 中没有。 相反,它有两个其他的东西:声明处型变(declaration-site variance)与类型投影(type projections)。

首先,让我们思考为什么 Java 需要那些神秘的通配符。在 Effective Java 解释了该问题——第28条:利用有限制通配符来提升 API 的灵活性。 首先,Java 中的泛型是不型变的,这意味着 List<String> 并不是 List<Object>的子类型。 为什么这样? 如果 List 不是不型变的,它就没 比 Java 的数组好到哪去,因为如下代码会通过编译然后导致运行时异常:

  List<String> strs = new ArrayList<String>(); List<Object> objs = strs;  objs.add(1);  String s = strs.get(0);  

因此,Java 禁止这样的事情以保证运行时的安全。但这样会有一些影响。例如,考虑 Collection 接口中的 addAll() 方法。该方法的签名应该是什么?直觉上,我们会这样:

  interface Collection<E> …… {   void addAll(Collection<E> items); } 

但随后,我们将无法做到以下简单的事情(这是完全安全):

  void copyAll(Collection<Object> to, Collection<String> from) {   to.addAll(from);                      } 

(在 Java 中,我们艰难地学到了这个教训,参见Effective Java,第25条:列表优先于数组

这就是为什么 addAll() 的实际签名是以下这样:

  interface Collection<E> …… {   void addAll(Collection<? extends E> items); } 

通配符类型参数 ? extends E 表示此方法接受 E 的 一些子类型对象的集合,而不是 E 本身。 这意味着我们可以安全地从其中(该集合中的元素是 E 的子类的实例)读取 E,但不能写入, 因为我们不知道什么对象符合那个未知的 E 的子类型。 反过来,该限制可以让Collection<String>表示为Collection<? extends Object>的子类型。 简而言之,带 extends 限定(上界)的通配符类型使得类型是协变的(covariant)

理解为什么这个技巧能够工作的关键相当简单:如果只能从集合中获取项目,那么使用 String 的集合, 并且从其中读取 Object 也没问题 。反过来,如果只能向集合中 放入 项目,就可以用 Object 集合并向其中放入 String:在 Java 中有 List<? super String> 是 List<Object> 的一个超类

后者称为逆变性(contravariance),并且对于 List <? super String> 你只能调用接受 String 作为参数的方法 (例如,你可以调用 add(String) 或者 set(int, String)),当然 如果调用函数返回 List<T> 中的 T,你得到的并非一个 String 而是一个 Object

Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符:

PECS 代表生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。

注意:如果你使用一个生产者对象,如 List<? extends Foo>,在该对象上不允许调用 add() 或 set()。但这并不意味着 该对象是不可变的:例如,没有什么阻止你调用 clear()从列表中删除所有项目,因为 clear()根本无需任何参数。通配符(或其他类型的型变)保证的唯一的事情是类型安全。不可变性完全是另一回事。

声明处型变

假设有一个泛型接口 Source<T>,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:

  interface Source<T> {   T nextT(); } 

那么,在 Source <Object> 类型的变量中存储 Source <String> 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

  void demo(Source<String> strs) {   Source<Object> objects = strs;     } 

为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。

在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source 的类型参数 T 来确保它仅从 Source<T> 成员中返回(生产),并从不被消费。 为此,我们提供 out 修饰符:

 abstract class Source<out T> {     abstract fun nextT(): T }  fun demo(strs: Source<String>) {     val objects: Source<Any> = strs       }

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出-位置,但回报是 C<Base> 可以安全地作为 C<Derived>的超类。

简而言之,他们说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 你可以认为 C 是 T 的生产者,而不是 T 的消费者

out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以我们讲声明处型变。 这与 Java 的使用处型变相反,其类型用途通配符使得类型协变。

另外除了 out,Kotlin 又补充了一个型变注释:in。它使得一个类型参数逆变:只可以被消费而不可以 被生产。逆变类的一个很好的例子是 Comparable

 abstract class Comparable<in T> {     abstract fun compareTo(other: T): Int }  fun demo(x: Comparable<Number>) {     x.compareTo(1.0)           val y: Comparable<Double> = x  }

我们相信 in 和 out 两词是自解释的(因为它们已经在 C# 中成功使用很长时间了), 因此上面提到的助记符不是真正需要的,并且可以将其改写为更高的目标:

存在性(The Existential) 转换:消费者 in, 生产者 out! 🙂

类型投影

使用处型变:类型投影

将类型参数 T 声明为 out 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回 T! 一个很好的例子是 Array:

 class Array<T>(val size: Int) {     fun get(index: Int): T {      fun set(index: Int, value: T) {  } 

该类在 T 上既不能是协变的也不能是逆变的。这造成了一些不灵活性。考虑下述函数:

 fun copy(from: Array<Any>, to: Array<Any>) {     assert(from.size == to.size)     for (i in from.indices)         to[i] = from[i] } 

这个函数应该将项目从一个数组复制到另一个数组。让我们尝试在实践中应用它:

 val ints: Array<Int> = arrayOf(1, 2, 3) val any = Array<Any>(3) copy(ints, any) // 错误:期望 (Array<Any>, Array<Any>)

这里我们遇到同样熟悉的问题:Array <T> 在 T 上是不型变的,因此 Array <Int> 和 Array <Any> 都不是 另一个的子类型。为什么? 再次重复,因为 copy 可能做坏事,也就是说,例如它可能尝试一个 String 到 from, 并且如果我们实际上传递一个 Int 的数组,一段时间后将会抛出一个 ClassCastException 异常。

那么,我们唯一要确保的是 copy() 不会做任何坏事。我们想阻止它到 from,我们可以:

 fun copy(from: Array<out Any>, to: Array<Any>) {   } 

这里发生的事情称为类型投影:我们说from不仅仅是一个数组,而是一个受限制的(投影的)数组:我们只可以调用返回类型为类型参数 T 的方法,如上,这意味着我们只能调用 get()。这就是我们的使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>、 但使用更简单些的方式。

你也可以使用 in 投影一个类型:

 fun fill(dest: Array<in String>, value: String) {      } 

Array<in String> 对应于 Java 的 Array<? super String>,也就是说,你可以传递一个 CharSequence 数组或一个 Object 数组给 fill() 函数。

星投影

有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。

Kotlin 为此提供了所谓的星投影语法:

  • 对于 Foo <out T>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。
  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 这意味着当 T未知时,没有什么可以以安全的方式写入 Foo <*>
  • 对于 Foo <T>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>,我们可以想象以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

注意:星投影非常像 Java 的原始类型,但是安全。

泛型函数

不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前:

 fun <T> singletonList(item: T): List<T> {      }  fun <T> T.basicToString() : String {        } 

要调用泛型函数,在调用处函数名之后指定类型参数即可:

 val l = singletonList<Int>(1) 

泛型约束

能够替换给定类型参数的所有可能类型的集合可以由泛型约束限制。

上界

最常见的约束类型是与 Java 的 extends 关键字对应的 上界

 fun <T : Comparable<T>> sort(list: List<T>) {      } 

冒号之后指定的类型是上界:只有 Comparable<T> 的子类型可以替代 T。 例如

 sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型 sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型

默认的上界(如果没有声明)是 Any?。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,我们需要一个单独的 where-子句:

 fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>     where T : Comparable,           T : Cloneable {   return list.filter { it > threshold }.map { it.clone() } }

更多精彩内容以及学习资料,尚学堂论坛bbs.bjsxt.com免费下载。

  • 北京校区
  • 西安校区
  • 山西校区
  • 武汉校区
  • 深圳校区
  • 上海校区
  • 广州校区
  • 保定招生办
  • 黑龙江项目办

北京京南校区:北京亦庄经济开发区科创十四街6号院1号楼 赛蒂国际工业园
咨询电话:400-009-1906 / 010-56233821
面授课程: JavaEE+微服务+大数据     大数据+机器学习+平台架构     Python+数据分析+机器学习  人工智能+模式识别+强化学习   WEB前端+移动端+服务端渲染

地址:陕西省西安市高新区西安软件园西区创新信息大厦A座三层尚学堂

电话:029-88228155 / 18291433445

山西学区地址:山西省晋中市榆次区大学城大学生活广场万科商业A1座702

武汉学区地址:武汉市东湖高新区光谷金融港B22栋11楼
咨询电话:027-87989193

深圳校区地址:深圳市宝安区航城大道U8智造产业园U6栋3楼
咨询电话:0755-23061965 / 18898413781

上海尚学堂校区地址:上海市浦东新区城丰路650号
咨询电话:021-67690939

广州校区地址:广州市天河区车陂街道大岗路5号中侨广场2栋321室(四号线车陂站D出口,或brt车陂站)
咨询电话:18948349646

保定招生办公室

地址:河北省保定市竞秀区朝阳南大街777号鸿悦国际1101室

电话:15132423123

黑龙江项目办
地点:哈尔滨市松北区博文路青年部落孵化器1层
电话:15321415678
Copyright 2006-2021 北京尚学堂科技有限公司  京ICP备13018289号-19  京公网安备11010802015183  
网站维护:北京尚学堂科技有限公司昌平分公司