Kotlin 泛型 VS Java 泛型

建议先阅读我的上一篇文章 -- Java 泛型

和 Java 泛型一样,Kotlin 泛型也是 Kotlin 语言中较难理解的一个部分。Kotlin 泛型的本质也是参数化类型,并且提供了编译时强类型检查,实际上也是伪泛型,和 Java 泛型类型一样。这篇文章将介绍 Kotlin 泛型里中的重要概念,以及与 Java 泛型的对比。

1. 泛型类型与泛型函数

Kotlin 下泛型类型与泛型函数的写法,与 Java 差不多,直接看下面的例子:

// 泛型类型
class Box<T> {
    var t: T? = null
}

// 泛型函数,类型参数在函数名之前
object Util {
    fun <K, V> compare(p1: Pair<K, V>, p2: Pair<K, V>): Boolean {
         return p1.first == p1.first && p2.second == p2.second
    }
}

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

val p1 = Pair(1, "1")
val p2 = Pair(2, "2")
Util.compare(p1, p2)

通过 Tools -> Kotlin -> Show Kotlin Bytecode, 然后点击字节码上面的 Decompile 出 Java 代码可以看出与 Java 泛型的原理是一样的,都进行了类型擦除。

2. 泛型约束

Java 中可以通过有界类型参数来限制参数类型的边界,Kotlin 下泛型约束也可以限制参数类型的上界:

fun <T: Comparable<T>> compare(t1: T, t2: T) = t1.compareTo(t2)

默认的上界是Any?,是可空类型,如果确定为非空类型的话,应该使用<T: Any>

泛型约束中的尖括号中只能指定一个上界,如果需要多个上界,需要一个单独的 where 子句:

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

3. 使用处型变:类型投影

在 Java 泛型的通配符中有一个“Producer Extends, Consumer Super”原则,简称 PECS 原则:只读类型使用上界通配符? extends T,只写类型使用下界通配符? super T。Kotlin 中提供了类似功能的两个操作符outin,分别生产和消费。

先看_Collection.kt中一个扩展函数:

public operator fun <T> Collection<T>.plus(elements: Array<out T>): List<T>

// list 为 ArrayList<Number> 类型
val list = arrayListOf<Number>(1, 2, 3)
// array 为 Array<Float> 类型
val array = arrayOf(1f, 2f)
val list1: List<Number> = list.plus(array)

所以Array<out T>相当于 Java 中的Array<? extends T>,而Array<in T>相当于 Java 中的Array<? super T>,out 表示生产,用于只读类型,in 表示消费,用于只写类型。

类型投影和 Java 的上界通配符和下界通配符一样,只能用于参数、属性、局部变量或返回值的类型,但是不能用于泛型类型和泛型函数声明的类型,所以称之使用处型变。

4. 声明处型变

与 Java 有界通配符不能用于泛型声明时使用不同的是,Kotlin 中outin两个型变注解还可以用于泛型声明时,更加灵活。下面通过 Java 和 Kotlin 中对 Collection 的定义来分析:

// Java 中 Collection 的定义,元素是可读写的
public interface Collection<E> extends Iterable<E> { ... }
public interface List<E> extends Collection<E> { ... }

// Collections 的 copy 方法
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }

// 但是下面声明在 Java 中是不允许的
public interface IllegalList<? extends T> extends Collection<E> { ... }

但是 Kotlin 中可以通过声明处型变(型变的概念在后面会详细解释)定义只读的集合:

// A generic collection of elements. Methods in this interface support only read-only access to the collection
public interface Collection<out E> : Iterable<E>

// A generic collection of elements that supports adding and removing elements.
public interface MutableCollection<E> : Collection<E>, MutableIterable<E>

Collection<out E>使得 Collection 里面的元素是只读的,也使得 Collection<Number>Collection<Int> 的父类,在 Kotlin 中称 Collection 的元素类型是协变的。对于协变的类型,通常不允许泛型类型作为函数的传入参数。

in型变注解可以使得元素类型是逆变的,只能被消费,与协变相反,通常不允许泛型类型作为函数的返回值,看Comparable的定义:

public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    // 1.0 类型为 Double,是 Number 的子类型
    x.compareTo(1.0)
    // 因为 Comparable 只能被消费,所以可以赋值给 Comoparable<Double> 的变量
    val y: Comparable<Double> = x
}

4.1 UnsafeVariance 注解

上面提到过对于协变的类型,通常不允许泛型类型作为函数的传入参数,对于逆变类型,通常不允许泛型类型作为函数的返回值,但是有时我们可以通过@UnsafeVariance 注解告诉 Kotlin 编译器:“我保证不会干坏事”,例如 Collection 的 contains 函数:

public interface Collection<out E> : Iterable<E> {
    ...
    public operator fun contains(element: @UnsafeVariance E): Boolean
    ...
}

5. 星投影

使用泛型的过程中,如果参数类型未知时,在 Java 中可以使用原始类型(Raw Types),但是 Java 的原始类型是类型不安全的:

ArrayList<String> list = new ArrayList<>(5);

ArrayList unkownList = list;
Object first = unkownList.get(0);
unkownList.add(1);  // warning: Unchecked call to 'add(E)'
unkownList.add("1"); // warning: Unchecked call to 'add(E)'

而在 Kotlin 中,在参数类型未知时,可以用星投影来安全的使用泛型:

val list = ArrayList<Int>(5)
val unkownList: ArrayList<*> = list
val first: Any = unkownList[0]
unkownList.add(1)  // error
unkownList.add("1") // error

对于ArrayList<*>来说,因为不知道具体的参数类型,对于add(e E)这种不安全的操作,Kotlin 编译器会直接报错,比 Java 的原始类型更安全。

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?>

6. 型变的概念

在上面提到过使用处型变和声明处型变,那具体型变指什么呢?型变:是否允许对参数类型进行子类型转换。例如在 Java 中List<Integer>List<Number>没有直接的类型关系,就是说 Java 中的泛型是不可以直接型变的。

为了提高代码的灵活性,Java 中可以通配符在使用时实现型变,例如void addNumbers(List<? super Number> list)方法中可以传List<Integer>List<Integer>List<? super Number>的子类型,而 Integer 也是 Number 的子类型,这也称之为协变。另外,List<Number>List<? super Integer>的子类型,和 Integer 与 Number 之间的类型关系相反,称之为逆变。

Kotlin 中outin操作符可以更简洁地实现 Java 的使用处型变,而且还支持声明处型变,这也使得 Kotlin 中的泛型是可以直接型变的。

Kotlin 下协变:interface List<out E>List<Int>是``List<Number>`的子类型。

Kotlin 下逆变:interface Comparable<in T>Comparable<Double>Comparable<Number>的父类型。

7. 具体化的类型参数

Kotlin 与 Java 中泛型都会进行类型擦除,泛型的具体类型在运行时是未知的,例如在解析 json 字符串时:

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { ... } 

还必须传泛型的 Class 类型,不能直接使用T.class获取类型,除非使用反射。

而在 Kotlin 中可以使用reified修饰符将内联函数的泛型类型当作具体的类型来使用,不需要再额外传一个 class 对象:

inline fun <reified T> Gson.fromJson(json: String): T? {
    return fromJson(json, T::class.java)
}

对于具体化的类型参数,可以当做一个普通的类型一样,as!as操作符也可以使用。因为 Kotlin 编译器会把内联函数的代码插入到调用者的地方,所以可以在编译期就确定泛型的类型。需要注意的是,Kotlin 中的reified的内联函数不能被 Java 代码调用。

8. 小结

回顾 Kotlin 和 Java 中的泛型,Kotlin 泛型扩展了 Java 中的泛型,添加了使用处型变和更安全的星投影,还支持具体化的类型参数。我整理了下面表格对比两者:

Java 泛型 Java 中代码示例 Kotlin 中代码示例 Kotlin 泛型
泛型类型 class Box<T> class Box<T> 泛型类型
泛型方法 <K, V> boolean method(Pair<K, V> p) fun <K, V> function(p: Pair<K, V>) 泛型函数
有界类型参数 class Box<T extends Comparable<T> class Box<T : Comparable<T>> 泛型约束
上界通配符 void sumOfList(List<? extends Number> list) fun sumOfList(list: List<out Number>) 使用处协变
下界通配符 void addNumbers(List<? super Integer> list) fun addNumbers(list: List<in Int>) 使用处逆变
interface Collection<out E> : Iterable<E> 声明处协变
interface Comparable<in T> 声明处逆变
原始类型 ArrayList unkownList = new ArrayList<String>(5) val unkownList: ArrayList<*> = ArrayList<Int>(5) 星投影

总的来说,Kotlin 泛型更加简洁安全,但是和 Java 一样都是有类型擦除的,都属于编译时泛型。

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

推荐阅读更多精彩内容