Kotlin 泛型中的 in 和 out

协变

在 Java 的泛型系统中. 泛型默认是不支持协变(covariant). 也就是说在 Java 中. 即使 A 是 B 的子类, List<A> 也不是 List<B> 的子类.
协变在某些情况下会造成一些问题, 比如在 Java 中, 数组是支持协变的. 下面的代码可以正常通过编译, 只有在运行时才会报错.

// snippet 1
// 数组是协变的, String[] 是 Object 的子类
Object [] objects = new String[10]
objects[1] = 1

虽然协变会引起一些不能在编译时就发现的明显错误, 但是在 Java 一直到 Java 5 才引入了泛型. 如果数组不支持协变, 很多方法就没有办法实现了. 比如 Arrays.sort(Ojbect[] array)这个方法. 不知持协变的情况下只能对每一个不同对象都实现一个方法.
在有了泛型的支持后, 数组协变这个特性就变成了一个彻彻底底的设计缺陷. 但是为了保持向下的兼容, Java 还是一直保留这个设计.
Kotlin 并没有类似 Java 的兼容包袱, 所以 Kotlin 中的 Array 是不支持协变的.下面的代码不能通过编译.

// snippet 2
val objects: Array<Object> = Array<String>(1) { "" } // Error

协变的作用

在一些情况下, 协变依然很有用. 比如我们实现一个List 的 copy 的函数如下:

// snippet 3
// 从 src 复制元素到 
static <T> void copy(List<T> dest, List<T> src)

当需要将一个类型为 List<Integer> 的 src 复制到 List<Number> dest 我们可能会写出如下的代码:

// snippet 4
List<Number> dest = ... // 初始化 dest
List<Integer> src = ...// 初始化 src
copy(dest, src)

但是 List<Integer> 并不是 List<Number> 的子类. 所以上面的代码并不能通过编译.
Java 提供了有限通配符类型 (bounded wildcard type) 来处理这种情况.上面的代码可以被改写成下面的形式:

// snippet 5
static <T> void copy(List<T> dest, List<? extend T> src)

这样改写后, src 的类型就变成 T 的某一个子类的列表. 也就是说 src 是支持协变的.

逆变

逆变(contravariant) 跟协变相反. 如果 List 支持逆变, 且 A 是 B 的子类, 那么 List<B> 是 List<A> 是子类.
逆变在函数中比较常见, 比如在 Kotlin 中 (Any) -> () 是 (String) -> () 的子类. Java 也通过 wildcard type 的方式支持了逆变. 声明方式如下:

List<? super Number> a;

变量 a 可以接受 List<Integer>, List<Double> 等类型.

PECS 法则

在函数的接口里使用通配符类型可以提高接口的灵活度. 但是参数究竟要使用 super 还是 extend 呢. Effective Java 中给出了一个 PECS 法则.

PSCS stands for producers-extends, consumer-super

也就是说, 如果一个参数是起的是生产者的作用, 那应该用 extend, 如果起的是消费者的作用, 就应该使用 super. 一个最好的例子就是 JDK 里 Collections 中的 copy 方法. 函数定义如下:

public static <T> void copy(List<? super T> dest, List<? extend T> src)

该函数里 src 提供需要复制的内容, 是一个生产者. 所以使用了 extend. dest 接受复制过来的元素, 是一个消费者. 所以使用的是 super. 判读一个参数是生产者还是消费者需要一定的编码的经验. 下面是我的两条经验:

  • 如果是参数是一个容器类型, 比如 List. 那么如果你在函数中是只是读取容器内容, 那么该参数就是一个生产者. 反正, 如果只是往容器里添加和删除, 而没有读取那么这个参数就是消费者.
  • 如果参数是一个 function Object, 可以将其改成 Lambda 函数. 正常在 Lambda 里当作参数的类型参数要求是逆变的, 返回值是协变的 比如 Comparator<T> 改写成 Lambda 形式应该是 (T, T) ->Int, 这里的 T 是只出现在参数中, 应该是逆变的. 当函数需要 Comparator 作为参数时通常使用会使用 super. 比如 Collections 中 sort 函数的定义为:
public static <T> void sort(List<T> list, Comparator<? super T> c)

Kotlin 中的 in 和 out

Kotlin 中可以声明泛型类型是协变还是逆变的. out 修饰类型参数是协变的, in 修饰的类型参数支持逆变.
比如 Collections 的 copy 方法的可以定义为:

public <T: Any> fun copy(dest: List<in T>, src: List<out T>)

除了在参数中使用外. 还可以直接在类型中使用. 比如 Kotlin 中的 List 是不可变的. 所以 List 定义里直接声明为协变的.

public interface List<out E>: Collection<E>

in 和 out 两个关键字应该是取自: Consumer in, Producer out!

参考资料:

Kotlin Generics: https://kotlinlang.org/docs/reference/generics.html

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

推荐阅读更多精彩内容

  • 前言 泛型(Generics)的型变是Java中比较难以理解和使用的部分,“神秘”的通配符,让我看了几遍《Java...
    珞泽珈群阅读 7,574评论 12 51
  • 简评:在 Kotlin 中使用泛型你会注意到其中引入了 in 和 out,对于不熟悉的开发者来说可能有点难以理解。...
    极小光阅读 20,876评论 7 53
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,062评论 9 118
  • 泛型 泛型(Generic Type)简介 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么...
    Tenderness4阅读 1,385评论 4 2
  • 文 六孑箫 此刻 独坐在天井湖畔 任凭秋风瑟瑟 吹得波澜 冰凉的石凳 沾满了水草味 对岸灯红酒绿 夜景似...
    六孑箫阅读 174评论 2 2