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
),此外,你还能对一个数字类型的值调用方法。
在运行时,数字类型会尽可能地使用最高效的方式来表示,大多数情况下,对于变量、属性、参数和返回类型,Kotlin
的Int
类型会被编译成Java
基本数据类型int
。唯一不可行的例外是泛型类,例如集合,用作泛型类型参数的基本数据类型会被编译成对象的Java
包类型。
对应到Java
基本数据类型的类型完整列表如下:
- 整数类型:
Byte
、Short
、Int
、Long
- 浮点数类型:
Float
、Double
- 字符类型:
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 数字转换
Kotlin
和Java
之间一条重要的区别就是处理数字转换的方式,Kotlin
不会自动地把数字从一种类型转换成另一种,即便是转换成范围更大的类型,我们必须 显示地转换,对每一种基本数据类型都定义有转换函数:toByte()
、toShort()
、toChar()
等,这些函数支持双向转换:
在比较装箱值的时候,比较两个装箱值的
equals
不仅会检查它们存储的值,还要比较装箱类型,也就是说new Integer(42).equals(new Long(42))
会返回false
。
基本数据类型字面值
Kotlin
除了支持简单的十进制数字之外,还支持下面这些在代码中书签数字字面值的方式:
- 使用后缀
L
表示Long
:123L
- 使用标准浮点数表示
Double
:0.12
、1.2e10
和1.2e-10
。 - 使用后缀
F
表示Float
:123.4f
、.456F
和1e3f
。 - 使用前缀
0x
或者0X
表示十六进制:0xbcdL
。 - 使用前缀
0b
或者0B
表示二进制字面值:0b0001
。
当你使用数字字面值去初始化一个类型已知的变量时,又或是把字面值作为实参传给函数时,必要的转换会自动地发生。
此外,算术运算符也被重载了,它们可以接收所有适当的数字类型。
2.4 根类型:Any 和 Any?
Any
类型是Kotlin
所有非空类型的超类型,包括像Int
这样的基本数据类型,和Java
一样,把基本数据类型的值赋给Any
类型的变量会自动装箱。
在Kotlin
中,如果你需要可以持有任何可能值的变量,包括null
在内,必须使用Any?
类型。
在底层,Any
类型对应java.lang.Object
,Kotlin
把Java
方法参数和返回类型中用到的Object
类型看作Any
,当Kotlin
函数函数中使用Any
时,它会被编译成Java
字节码中的Object
。
所有的Kotlin
类都包含下面三个方法:toString
、equals
和hashCode
,这些方法都继承自Any
。Any
不能使用其它Object
的方法(例如wait
和notify
),但是可以通过手动把值转换成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
集合接口的一个实例,在Kotlin
和Java
之间转移并不需要转换;不需要包装器也不需要拷贝数据。
每一种Java
集合接口在Kotlin
中都有两种表示:一种是只读的,另一种是可变的。在下图当中,可以看出Kotlin
集合接口的层级结构,Java
类ArrayList
和HashSet
都继承了Kotlin
可变接口。
Kotlin
中只读接口和可变接口的基本结构与java.util
中的Java
集合接口的结构是平行的。可变接口直接对应java.util
包中的接口,而它们的只读版本缺少了所有产生改变的方法。
上图中包含了Java
类中的ArrayList
和HashSet
,在Kotlin
看来,它们分别继承自MutableList
和MutableSet
接口,这样既得到了兼容性,也得到了可变接口和只读接口之间清晰的分离。
除了集合之外,Kotlin
中Map
类也被表示成了两种不同的版本:Map
和MutableMap
。我们之前见到的listOf/setOf/mapOf
所返回的都是只读版本。
当你有一个使用java.util.Collection
做形参的Java
方法,可以把任意Collection
或MutableCollection
的值作为实参传递给这个形参。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
参数并返回一个使用对应基本数据类型默认值初始化好的数组。 - 工厂函数(例如
IntArray
的intArrayOf
,以及其他数组类型的函数)接收变长参数的值并创建和存储这些值的数组。 - 另一种构造方法,接收一个大小和一个用来初始化每个元素的
lambda
。
运行结果为:
更多文章,欢迎访问我的 Android 知识梳理系列:
- Android 知识梳理目录:http://www.jianshu.com/p/fd82d18994ce
- 个人主页:http://lizejun.cn
- 个人知识总结目录:http://lizejun.cn/categories/