6.Kotlin的类型系统
问号可以加在任何类型的后面来表示这个类型的变量可以存储null引用。没有问号的类型表示这种类型的变量不能存储null引用。这说明所有常见类型默认都是非空的,除非显式地把它标记为可空。可空和非空的对象在运行时没有什么区别;可空类型并不是非空类型的包装。所有的检查都发生在编译期。这意味着使用Kotlin的可空类型并不会再运行时带来额外的开销。
安全调用符?.
,s?.toUpperCase()
等价于if(s != null) s.toUpperCase() else null
Elvis运算符?:
(或者叫null合并运算符)foo ?: bar
等价于if(foo != null) foo else bar
在Kotlin中有一种场景下Elvis运算符会特别顺手,像return
和throw
这样的操作其实是表达式,因此可以把它们写在Elvis运算符的右边。这种情况下,如果Elvis运算符左边的值为null,函数就会立即返回一个值或者抛出一个异常。如果函数中需要检查先决条件,这个方式特别有用。
安全转换as?
,foo as? Type
等价于if(foo is Type) foo as Type else null
非空断言!!
是kotlin提供给你的嘴简单直率的处理可空类型值的工具。foo!!
等价于if(foo != null) foo else throw NullPointerException("")
如果要将一个可空值作为实参传递给一个只接受非空值的函数时,该怎么办?编译器不允许在没有检查的情况下这么做,因为这样不安全。let
函数可以帮到你。let
函数让处理可空表达式变得更容易,和安全调用运算符一起,它允许你对表达式求值,检查求值结果是否为null,并把结果保存为一个变量。所有这些动作都在同一个简洁的表达式中。
foo?.let{...}
等价于foo != null,在lambda内部it是非空的,否则什么都不会发生
使用lateinit
修饰符来将属性声明成延迟初始化的。延迟初始化的属性都是var
,因为需要在构造方法外修改它的值。
为可空类型定义扩展函数是一种更强大的处理null值的方式。可以允许接收者为null的扩展函数调用,并在该函数中处理null,而不是在确保变量不为null之后再调用它的方法。
kotlin中所有泛型类和泛型函数的类型参数默认都是可空的。
有时候Java包含了可控性信息,这些信息使用注解来表达。当代码中出现了这样的信息时,kotlin就会使用它。因此Java中的@Nullable String
被kotlin当做String?
,而@NotNull String
就是String
。kotlin可以识别多种不同风格的可控性注解,包括JSR-305标注的注解javax.annotaion
包之中,Android的注解android.support.annotaion
和JetBrains工具支持的注解org.jetbrains.annotations
。如果这些注解不存在,Java类型将会变成kotlin中的平台类型。平台类型本质上就是kotlin不知道可控性信息的类型。既可以把它当做可控类型处理也可以当做非空类型处理。
String!
表示法被kotlin编译器用来表示来自Java代码的平台类型,你不能在自己的代码中使用这种语法。
当在kotlin中重写Java的方法时,可以选择把参数和返回类型定义成可空的,也可以选择把他们定义成非空的。
Java把基本数据类型和引用类型做了区分,一个基本数据类型的变量直接存储了它的值,而一个引用类型的变量存储的是指向包含该对象的内存地址的引用。基本数据类型的值能够更高效地存储和传递,但是你不能对这些值调用方法,或是把它们存放在集合中。Java提供了特殊的包装类型,在你需要对象的时候对基本数据类型进行封装。
kotlin并不区分基本数据类型和包装类型,你使用的永远是同一个类型。在运行时,数字类型会尽可能地使用最高效的方式来表示。大多数情况下,对于变量、属性、参数和返回类型,kotlin的Int类型会被编译成Java基本数据类型int。唯一不可行的例外是泛型类,比如集合。用作泛型类型参数的基本数据类型会被编译成对应的Java包装类型。
对应到Java基本数据类型的类型完整列表如下:
- 整数类型——Byte、Short、Int、Long
- 浮点数类型——Float、Double
- 字符类型——Char
- 布尔类型——Boolean
kotlin中的可空类型不能用Java的基本数据类型表示,因为null
只能被存储在Java的引用类型的变量中。这意味着任何时候只要使用了基本数据类型的可空版本,它就会编译成对应的包装类型。
kotlin和Java之间一条重要的区别就是处理数字转换的方式。Kotlin不会自动地把数字从一种类型转换成另外一种,即使是转换成范围更大的类型,相反,必须显式地进行转换。
val i = 1
val l : Long = i.toLong()
和Object
作为Java类层级结构的根差不多,Any
类型是Kotlin所有非空类型的超类型(非空类型的根)。但是在Java中,Object
只是所有引用类型的超类型(引用类型的根),而基本数据类型并不是类层级结构的一部分。这意味着当你需要Object
的时候,不得不使用像Integer
这样的包装类型来表示基本数据的值。而在kotlin中,Any
是所有类型的超类型(所有类型的根),包括像Int
这样的基本数据类型。
和Java一样,把基本数据类型的值赋给Any
类型的变量时会自动装箱:
val answer: Any = 42
注意Any
是非空类型,所以Any
类型的变量不可以持有null
值。在kotlin中如果你需要可以持有任何可能值的变量,包括null
在内,必须使用Any?
类型。
在底层,Any
类型对应java.lang.Object
。kotlin把Java方法参数和返回类型中用到的Object
类型看做平台类型。当kotlin函数使用Any
时,它会被编译成Java字节码中的Object
。
所有kotlin类都包含下面三个方法:toString
、equals
和hashCode
,这些方法都继承自Any
。
kotlin中Unit
类型完成了Java中的void
一样的功能。当函数没什么有意思的结果要返回时,它可以用作函数的返回类型。
kotlin的Unit
和Java中的void
到底有什么不一样呢?Unit
是一个完备的类型,可以用作类型参数,而void
却不行。只存在一个值是Unit
类型,这个值也叫做Unit
,并且在函数中会被隐式地返回。
interface Processor<T> {
fun process() : T
}
class NoResultProcessor : Processor<Unit> {
override fun process() {
// ...
}
}
Java为了解决使用“没有值”作为类型参数一种选择是使用分开的接口定义来分别表示需要和不需要返回值的接口(如Callable
和Runnable
),另一种是用特殊的java.lang.Void
类型作为类型参数,即使采用了这种方式,还是需要加入return null;
语句来返回唯一能匹配这个类型的值,因为只要返回类型不是void
,就必须始终有显式的return
语句。
对某些kotlin函数来说,返回类型的概念没有任何意义,因为他们从来不会成功地结束。当分析调用这样函数的代码时,知道函数永远不会正常终止是很有帮助的。kotlin使用一种特殊的返回类型Nothing
来表示。
kotlin的集合设计和Java不同的一项重要特质是,它把访问集合数据的接口和修改集合数据的接口分开了。
kotlin.collections.Collection
使用这个接口,可以遍历集合中的元素、获取集合大小、判断集合中是否包含某个元素,以及执行其他从该集合中读取数据的操作。但这个接口没有任何添加或移除元素的方法。
kotlin.collections.MutableCollection
接口可以修改集合中的数据,它继承了普通的kotlin.collections.Collection
接口,还提供了方法来添加和移除元素、清空集合等。
集合类型 | 只读 | 可变 |
---|---|---|
List | listOf | mutableListOf、arrayListOf |
Set | setOf | mutableSetOf、hashSetOf、linkedSetOf、sortedSetOf |
Map | mapOf | mutableMapOf、hashMapOf、linkedMapOf、sortedMapOf |
当你需要调用一个Java方法并把集合作为实参传给它时,可以直接这样做,不需要任何额外的步骤。例如,你有一个使用java.util.Collection
做形参的Java方法,可以把任意Collection
或MutableCollection
的值作为实参传递给这个形参。这对集合的可变性有重要影响,因为Java病不会区分只读集合和可变集合,即使kotlin中把集合声明成只读的,Java代码也能够修改这个集合。
Java中声明的集合类型的变量也被视为平台类型,一个平台类型的集合本质上就是可变性未知的集合——kotlin代码将其视为只读的或者可变的。
要在kotlin中创建数组,有下面这些方法供你选择:
arrayOf
函数创建一个数组,它包含的元素是指定为该函数的实参arrayOfNulls
创建一个给定大小的数组,包含的是Null
元素。当然,它只能用来创建包含元素类型可空的数组。Array
构造方法接收数组的大小和一个lambda表达式,调用lambda表达式来创建每一个数组元素。这就是使用非空元素类型来初始化数组,但不用显式地传递每个元素的方式。lambda接收数组元素的下标并返回放在数组下标位置的值。
val letters = Array<String>(26){ i -> ('a' + i).toString()}
kotlin最常见的创建数组的情况之一是需要调用参数为数组的Java方法时,或是调用带有vararg
参数的kotlin函数时。通常数据已经存储在集合中,将其转换为数组可以使用toTypedArray
方法来执行此操作。
val strings = listOf("a", "b", "c")
println("%s/%s/%s".format(*strings.toTypedArray()))
和其他类型一样,数组类型的类型参数始终会变成对象类型。因此,如果你声明了一个Arrayjava.lang.Integer[]
)。
为了表示基本类型的数组,Kotlin提供了若干独立的类,每一种基本数据类型都对应一个。例如,Int
类型值的数组叫做IntArray
。kotlin还提供了ByteArray
、CharArray
、BooleanArray
等给其他类型。所有这些类型都被编译成普通的Java基本数据类型数组,比如int[]
,byte[]
,char[]
要创建一个基本数据类型的数组,你有如下选择:
- 该类型的构造方法接收size参数并返回一个使用对应基本数据类型默认值初始化好的数组
- 工厂函数(
IntArray
的intArrayOf
以及其他数组类型函数)接收边长参数的值并创建存储这些值的数组。 - 另一种构造方法,接收一个大小和一个用来初始化每个元素的lambda
val fiveZeros = IntArray(5)
val fiveZerosTwo = intArrayOf(0, 0, 0, 0, 0)
val squares = IntArray(5) { i -> i*i}
或者假如你有一个持有基本数据类型装箱后的值的数组或者集合,可以用对应的转换函数把它们转换成基本数据类型的数组,比如toIntArray
对数组的操作,除了那些基本操作(获取数组的长度,获取或者设置元素)外,kotlin标准库支持一套和集合相同的用于数组的扩展函数。