Kotlin 知识梳理(7) - Kotlin 的类型系统

Kotlin 知识梳理系列文章

Kotlin 知识梳理(1) - Kotlin 基础
Kotlin 知识梳理(2) - 函数的定义与调用
Kotlin 知识梳理(3) - 类、对象和接口
Kotlin 知识梳理(4) - 数据类、类委托 及 object 关键字
Kotlin 知识梳理(5) - lambda 表达式和成员引用
Kotlin 知识梳理(6) - Kotlin 的可空性
Kotlin 知识梳理(7) - Kotlin 的类型系统
Kotlin 知识梳理(8) - 运算符重载及其他约定
Kotlin 知识梳理(9) - 委托属性
Kotlin 知识梳理(10) - 高阶函数:Lambda 作为形参或返回值
Kotlin 知识梳理(11) - 内联函数
Kotlin 知识梳理(12) - 泛型类型参数


一、本文概要

本文是对<<Kotlin in Action>>的学习笔记,如果需要运行相应的代码可以访问在线环境 try.kotlinlang.org,这部分的思维导图为:

二、基本数据类型和其它基本类型

2.1 基本类型:Int、Boolean 及其它

Java把基本数据类型和引用类型做了区分:

  • 基本数据类型,例如int的变量直接存储了它的值,我们不能对这些值调用方法,或者把它们放到集合中。
  • 引用类型的变量存储的是指向包含该对象的内存地址的引用。

Kotlin不区分基本数据类型和引用类型,它使用的永远是一个类型(例如Int),此外,你还能对一个数字类型的值调用方法。

在运行时,数字类型会尽可能地使用最高效的方式来表示,大多数情况下,对于变量、属性、参数和返回类型,KotlinInt类型会被编译成Java基本数据类型int。唯一不可行的例外是泛型类,例如集合,用作泛型类型参数的基本数据类型会被编译成对象的Java包类型。

对应到Java基本数据类型的类型完整列表如下:

  • 整数类型:ByteShortIntLong
  • 浮点数类型:FloatDouble
  • 字符类型:Char
  • 布尔类型:Boolean

Int这样的Kotlin类型在底层可以轻易地编译成对应的Java基本数据类型。而在Kotlin中使用Java声明时,Java基本数据类型会变成非空类型,因为它们不能持有null值。

2.2 可空的基本数据类型:Int?、Boolean? 及其它

Kotlin中的可空类型不能用Java的基本数据类型表示,因为null只能被存储在Java的引用类型的变量中。任何时候,只要使用了基本数据类型的可空版本,它就会被编译成对应的包装类型,并且不能比较两个可空基本数据类型的大小,因为它们之中任何一个都可能为null

除此之外,泛型类是包装类型应用的另一种情况,如果你 用基本数据类型作为泛型类的类型参数,那么 Kotlin 会使用该类型的包装形式,例如下面这段代码,就会创建一个Integer包装类的列表,尽管你从来没有指定过可空类型或者用过null值:

val listOfInts = listOf(1, 2, 3)

这是由Java虚拟机实现泛型的方式决定的,JVM不支持用基本数据类型作为类型参数,所以泛型类必须始终使用类型的包装表示。

2.3 数字转换

KotlinJava之间一条重要的区别就是处理数字转换的方式,Kotlin不会自动地把数字从一种类型转换成另一种,即便是转换成范围更大的类型,我们必须 显示地转换,对每一种基本数据类型都定义有转换函数:toByte()toShort()toChar()等,这些函数支持双向转换:


在比较装箱值的时候,比较两个装箱值的equals不仅会检查它们存储的值,还要比较装箱类型,也就是说new Integer(42).equals(new Long(42))会返回false

基本数据类型字面值

Kotlin除了支持简单的十进制数字之外,还支持下面这些在代码中书签数字字面值的方式:

  • 使用后缀L表示Long123L
  • 使用标准浮点数表示Double0.121.2e101.2e-10
  • 使用后缀F表示Float123.4f.456F1e3f
  • 使用前缀0x或者0X表示十六进制:0xbcdL
  • 使用前缀0b或者0B表示二进制字面值:0b0001

当你使用数字字面值去初始化一个类型已知的变量时,又或是把字面值作为实参传给函数时,必要的转换会自动地发生


此外,算术运算符也被重载了,它们可以接收所有适当的数字类型。

2.4 根类型:Any 和 Any?

Any类型是Kotlin所有非空类型的超类型,包括像Int这样的基本数据类型,和Java一样,把基本数据类型的值赋给Any类型的变量会自动装箱。

Kotlin中,如果你需要可以持有任何可能值的变量,包括null在内,必须使用Any?类型。

在底层,Any类型对应java.lang.ObjectKotlinJava方法参数和返回类型中用到的Object类型看作Any,当Kotlin函数函数中使用Any时,它会被编译成Java字节码中的Object

所有的Kotlin类都包含下面三个方法:toStringequalshashCode,这些方法都继承自AnyAny不能使用其它Object的方法(例如waitnotify),但是可以通过手动把值转换成java.lang.Object来调用这些方法。

2.5 Unit 类型:Kotlin 的 void

Kotlin中的Unit类型完成了Java中的void一样的功能,当函数没有有意思的结果要返回时,它可以用作函数的返回类型:

fun f() : Unit { .. }

Unit是一个完备的类型,可以作为类型参数,而void却不行。只存在一个值是Unit类型,这个值也叫做Unit,并且(在函数中)会被隐式地返回,当你在重写返回泛型参数的函数时这非常有用,只需要让方法返回Unit类型的值:


运行结果为:

2.6 这个函数永不返回:Nothing

对于某些Kotlin函数来说,“返回类型”的概念没有任何意义,因为它们从来不会成功地结束,Kotlin使用一种特殊的返回类型Nothing来表示:


运行结果为:

Nothing类型没有任何值,只有被当作函数返回值使用,或者被当作泛型函数返回值的类型参数使用才会有意义,在其它情况下,声明一个不能存储任何值的变量没有任何意义。

返回Nothing的函数可以放在Elvis运算符的右边来做先决条件检查:


运行结果为:

三、集合和数组

3.1 可空性和集合

Kotlin 知识梳理(6) - Kotlin 的可空性 中,我们讨论了可空类型的概念,但仅仅简略地谈到类型参数的可空性,其实集合也可以持有null元素,和变量可以持有null一样,类型在被当作类型参数时也可以用同样的方式来标记。

下面我们创建一个包含可空值的集合,之后遍历该集合,打印出有效的数字之和以及为null的集合元素个数:


运行结果为:

3.2 只读集合与可变集合

Kotlin的集合设计与Java不同的另一项重要特质是:它把访问集合数据的接口和修改集合数据的结构分开了:

  • kotlin.collections.Collection:使用这个接口,可以遍历集合中的元素、获取集合大小、判断集合中是否包含某个元素,执行其他从该集合中读取数据的操作。
  • kotlin.collections.MutableCollection:修改集合中的数据。

一般的原则是:在代码的任何地方都应该使用只读接口,只在代码需要修改集合的地方使用可变接口的变体

下面的例子演示了如何使用只读集合和可变集合:



运行结果为:


3.3 Kotlin 集合和 Java

每一个Kotlin接口都是其对应Java集合接口的一个实例,在KotlinJava之间转移并不需要转换;不需要包装器也不需要拷贝数据。

每一种Java集合接口在Kotlin中都有两种表示:一种是只读的,另一种是可变的。在下图当中,可以看出Kotlin集合接口的层级结构,JavaArrayListHashSet都继承了Kotlin可变接口。


Kotlin中只读接口和可变接口的基本结构与java.util中的Java集合接口的结构是平行的。可变接口直接对应java.util包中的接口,而它们的只读版本缺少了所有产生改变的方法。

上图中包含了Java类中的ArrayListHashSet,在Kotlin看来,它们分别继承自MutableListMutableSet接口,这样既得到了兼容性,也得到了可变接口和只读接口之间清晰的分离。

除了集合之外,KotlinMap类也被表示成了两种不同的版本:MapMutableMap。我们之前见到的listOf/setOf/mapOf所返回的都是只读版本。

当你有一个使用java.util.Collection做形参的Java方法,可以把任意CollectionMutableCollection的值作为实参传递给这个形参。Java并不会区分只读集合和可变集合,也就是说即使Kotlin中把集合声明成只读的,Java代码也可以修改这个集合,例如下面的代码,虽然我们将printInUppercase接收的list参数声明为只读的,但是仍然可以通过Java代码修改它。

//CollectionUtils.java
public class CollectionUtils {
    public static List<String> uppercaseAll(List<String> items) {
        for (int i = 0; i < items.size(); i++) {
            items.set(i, items.get(i).toUpperCase());
        }
        return items;
    }
}

//collections.kt
fun printInUppercase(list : List<String>) {
    println(CollectionUtils.uppercaseAll(list));
    println(list.first())
}

3.4 作为平台类型的集合

前面我们介绍过,Kotlin把那些定义在Java代码中的类型看成 平台类型Kotlin没有任何关于平台类型的可空性信息,所以编译器允许Kotlin代码将其视为可空或者非空,同样,Java中声明的集合类型的变量也被视为平台类型。

当我们需要重写或者实现签名中有集合类型的Java方法时,这些差异才变得重要,我们需要决定使用哪一种Kotlin类型来表示这个Java类型,它们会反映在产生的Kotlin参数类型中:

  • 集合是否为空?
  • 集合中的元素是否为空?
  • 你的方法会不会修改集合?

例如下面这个使用集合参数的Java接口:

interface DataParser<T> {
    void parseData(String input, List<T> output, List<String> errors);
}

我们的选择为:

  • List<String>将是非空的,因为调用者总是需要接收错误信息。
  • 列表中的元素将是可空的,因为不是每个输出列表中的条目都有关联的错误信息。
  • List<String>将是可变的,因为实现代码需要向其中添加元素。

那么Kotlin的实现如下:

class PersonParser : DataParser<Person> {
    override fun parseData(input : String, output : MutableList<Person>, 
        errors : MutableList<String?>)
}

3.5 对象和基本数据类型的数组

Kotlin中的一个数组是一个带有类型参数的类,其元素类型被指定为相应的类型参数,要在Kotlin中创建数组,有下面这些方法供你选择:

  • arrayOf函数创建一个数组,它包含的元素是指定为该函数的实参
  • arrayOfNulls创建一个给定大小的数组,包含的是null元素,当然,它只能用来创建包含元素类型可空的数组
  • Array构造方法接收数组的大小和一个lambda表达式,调用lambda表达式来创建每一个数组元素,这就是使用非空元素类型来初始化数组,但不用显示地传递每个元素的方式

运行结果为:


创建没有装箱的基本数据类型的数组

数组类型的类型参数始终会变成对象类型,因此,如果你声明了一个Array<Int>,它将会是一个包含装箱整型的数组,如果你需要创建没有装箱的基本数据类型的数组,必须使用一个基本数据类型数组的特殊类。

Kotlin提供了若干个独立的类,每一种基本数据类型对应一个,例如Int类型值的数组叫作IntArray,要创建一个基本数据类型的数组,有如下的选择:

  • 该类型的构造方法接收size参数并返回一个使用对应基本数据类型默认值初始化好的数组。
  • 工厂函数(例如IntArrayintArrayOf,以及其他数组类型的函数)接收变长参数的值并创建和存储这些值的数组。
  • 另一种构造方法,接收一个大小和一个用来初始化每个元素的lambda

运行结果为:



更多文章,欢迎访问我的 Android 知识梳理系列:

推荐阅读更多精彩内容