¶主要内容
- 空值处理
- 原生类型和对应的Java类型
- Kotlin的集合以及与Java的关系
Kotlin对空值类型的处理,并不是使用ADT。诸如Option
、Either
,而是使用符号记法?
。对于类型转换,不使用协变逆协变,而是使用as
和Any?
这种语法。
¶Nullability
Nullability 在kotlin的type system是作为feature般的存在,用了避免NullPointerException
错误。所以kotlin并不是要解决编译期或运行期的NPE问题,而是提供兼容手段,减少该异常在运行期出现的可能性。
因此,kotlin中需要讨论空值类型:kotlin通过标记值类型以允许可以为null
,并提供空值类型的工具。
¶Nullable types
首先也是最重要的不同在于,Kotlin和Java类型系统上,kotlin是显式支持 nullable types 。什么意思?它提供了指定变量或属性允许为null
的一种方式。如果一个变量为null
,对其的方法调用是不安全的,因为它会导致NullPointerException
。kotlin不允许这种调用以避免许多可能的异常。且看如下代码:
1 | int strLen(String s) { |
该函数是不安全的,传入的参数如果为null
将会抛出NullPointerException
。你可能需要根据需求在方法体内进行null
检测。
下面是重写为kotlin的写法,
1 | fun strLen(s: String) = s.length |
如果传入null
参数,会直接触发编译错误检查:
1 | >>> strLen(null) |
该参数被声明为String
类型,在Kotlin中意味着它必须总是包含一个String
实例。这是编译器强制性的,你不能传递一个null
的参数。保证了函数strLen
永远不会在运行时抛出NullPointerException
。
如果你希望允许传入null
参数,可以写为:
1 | fun strLenSafe(s: String?) = ... |
将问号置于类型签名之后。譬如:String?
、Int?
、MyCustomType?
,等等。
再次重申,没有用?
标记的变量是不能存储null
引用的。意味着所有常规类型默认都是非空的(non-null),除非显式指定可以为空。
一旦声明了一个nullable type类型, 一系列的操作将变得严格。例如,不能对其调用方法:
1 | >>> fun strLenSafe(s: String?) = s.length() |
不可以将其分配给一个非空类型:
1 | >>> val x: String? = null |
也不可以传给非空参数方法,
1 | >>> strLen(x) |
你需要对其空值的可能性进行处理,
1 | fun strLenSafe(s: String?): Int = if (s != null) s.length else 0 // By adding the check for null, the code now compiles. |
这种写法有点啰嗦,后面会讲到kotlin的更简洁的写法。
¶The meaning of types
kotlin的类型系统上,对于null
的处理本质和Java8之后的Optional
一致,在编译期进行包装和检查,以确保在运行期不会过度的重复检查。
Java的API也相应提供了基于运行期的注解@Nullable
和@NotNull
等,在标准库定义,但不在标准库实现,运行期的检查实现可能在诸如Hibernate或Guava这些类库中。
¶Safe call operator: “?.”
Kotlin其中最有用的军械库是:safe-call
—— ?.
。一分为二操作,允许同时对空值类型检查和后续非空值处理。例如,表达式s?.toUpperCase()
实际上等价于if (s != null) s.toUpperCase() else null
。
换句话就是,如果值不为空,则正常执行方法调用;如果为null
,跳过调用,null
直接作为结果。
safe-call 的写法也可以用在属性访问上,如下:
1 | class Employee(val name: String, val manager: Employee?) |
safe-null 可以进行链式调用,
1 | class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String) |
¶Elvis operator: “?:”
带默认值的空值处理操作符 ?:
称作 Elvis operator 。写法如下,
1 | fun foo(s: String?) { |
类似于Java的三元运算。逻辑如下,
通常和 safe-call 操作一起使用,
1 | fun strLenSafe(s: String?): Int = s?.length ?: 0 |
countryName
函数也可以改为,
1 | fun Person.countryName() = |
有些时候,希望直接抛出异常,
1 | class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String) |
¶Safe casts: “as?”
kotlin的 safe cast 也总是和 safe call 和 elvis operator 一起使用。类似地,作为feature用于处理ClassCastException
的兼容性问题。
as?
操作会尝试转换值的类型,如果不匹配将返回null
。它常和 Elvis operator 一起使用。
1 | class Person(val firstName: String, val lastName: String) { |
¶Not-null assertions: “!!”
not-null assertion 非空断言操作符!!
会在值为null
的时候,直接抛出NullPointerException
,逻辑如下,
1 | fun ignoreNlls(s: String?) { |
出现空值时,在运行时直接抛出NullPointerException
。这种需要断言的情况大多数出现在类似swing ui这些框架中,譬如,
1 | class CopyRowAction(val list: JList<String>): AbstractAction() { |
需要注意的是,如果不希望使用!!
,你需要自定义返回,val value = list.selectedValue ?: return
;否则一个空的list.selectedValue
会被返回。并且需要记住的是,!!
会触发异常,所以下面写法最好不要这样做:
1 | person.company!!.address!!.country // Don't write code like this! |
这种写法,无法知道到底是person
返回了null,还是company
返回了null,除非你不关系。
¶The “let” function
Kotlin的内联函数let
用于处理nullable表达式。和 safe-call 一起使用,属于一种 evaluate 操作,“假如… 就…”。
常见于对方法签名的参数处理,如下,
1 | fun sendEmailTo(email: String) { /*...*/ } |
你不能传递空值类型,
1 | >>> val email: String? = ... |
这是你需要显式检查传入的值是否为null
:
1 | if (email != null) sendEmailTo(email) |
Kotlin中的let
的逻辑如下,
因此,使用let
结合lambda有更简洁的写法:
1 | email?.let { email -> sendEmailTo(email) } |
或者用it
替换,
1 | email?.let {sendEmailTo(it) } |
如果为null,lambda的表达式永远不会执行。
¶Late-initialized properties
某些框架对于一些变量或属性的声明是必须要延迟执行的,譬如JUnit这类。这时候就需要预先使用?
这类 nullable type替代。
1 | class MyService { |
这看起来很丑,特别是如果要访问属性很多次时。为了解决这个问题,你可以将myService
属性声明为 late-initialized 。你需要用到lateinit
修改器,
1 | class MyService { |
所有late-initialized的属性都是 var
的,因为你需要在构造器外部更改它的值,以及val
属性则被编译为final的,必须在构造器内初始化。lateinit
更多被用于DI(dependency injection)之类的框架中,譬如spring等。
¶Extensions for nullable types
空值类型在Kotlin中属于类型系统部分,当然也有extension function,写法上没有差别,只不过接收类型(receiver type)是带有?
标记的,
1 | fun String?.isNullOrBlank(): Boolean = // Extension for a nullable String |
需要注意的是,之前介绍的内联函数let
也是带有接收类型,但它不做null
的检查。所以如果在lambda调用空值类型而不使用safe-call 操作符,lambda内的参数也可能是空值类型的,
1 | >>> val person: Person? = ... |
所以你必须尽早处理空值,person?.let { sendEmailTo(it) }
。
¶Nullability of type parameters
默认地,Kotlin中所有函数的参数类型都是可空的。任何类型,包括空值类型,都可以被替代为参数类型;因此,类型的参数声明是运行为null
,即使参数类型T
没有以?
结尾。考虑如下代码。
1 | fun <T> printHashCode(t: T) { |
在这个例子中,类型参数T
被推断为Any?
。因此,t
是运行为null
的,即使没有带?
。
若要使其类型参数不为null
,需要指定一个非空类型边界,如下,
1 | fun <T: Any> printHashCode(t: T) { // Now "T" can't be nullable. |
¶Primitive and other basic types
Kotlin不区分原生类型和封装类型。
¶Primitive types: Int, Boolean, and more
- 对于变量、属性、参数以及返回值,Kotlin的
Int
类型编译为Java的原生类型int
。 - 对于泛型类,Kotlin的
Int
则编译为Java的包装类型。如集合类。
下面对应于Java的原生类型列表:
- Integer Types :
Byte
、Short
、Int
、Long
- Floating-point number types :
Float
、Double
- Character type :
Char
- Boolean type :
Boolean
¶Nullable primitive types: Int?, Boolean?, and more
Kotlin中的可空原生类型并不能表述为Java的原生类型,因为null
在Java中仅存储为变量的引用。意味着无论kotlin中用的是哪种可空的原生类型,只能编译为Java的包装类型。
以下面例子开始,
1 | data class Person(val name: String, val age: Int? = null) { |
这里并不能比较两个Int?
类型,因为其中一个有可能为null
。相反,你必须两个值都做null
检查。之后再比较它们的真实值。
另外,对于泛型类型,kotlin会使用它的装箱类型替代,如下
1 | val listOfInts = listOf(1, 2, 3) |
会创建一个Integer
的集合变量。
这是因为,泛型的实现是有JVM所控制的。JVM并不支持原生类型作为泛型参数,所以泛型类(不论是Java还是Kotlin)都必须使用装箱类型表述。
¶Number conversions
Kotlin和Java的一个重要区分是对数字的转换处理。Kotlin并不自动将数字类型由一种类型,转换为另一种,
1 | val i = 1 |
相反,你需要显式转换,
1 | val i = 1 |
每个原生类型都定义了相应的转换函数(处理Boolean
):toByte()
、toShort()
、toChar()
等等。函数支持两个方向的转换:小类型到大类型,如Int.toLong()
,大类型到小类型,Long.toInt()
。
Kotlin需要显式进行转换是为了避免出现意外,特别是比较封装类型时。equals
方法会检测封装类型和值。因为,在Java中,new Integer(42).equals(new Long(42))
是返回false
的。
假如Kotlin支持隐式转换,
1 | val x = 1 // Int variable |
这会触发编译错误,kotlin要求你显式地转换指定的类型进行比较,
1 | >>> val x= 1 |
Primitive type literals
Kotlin支持下面几种形式的数字字面量写法:
Long
类型使用L
作为后缀:123L
。Double
类型使用标准浮点数的写法:0.12
、2.0
、2e10
、1.2e-10
。Float
使用f
或F
作为后缀:123.4f
、.456F
、1e3f
。- 十六进制字面量使用
0x
或0X
前缀:0xCAFEBABE
或0xbcdL
。- 二进制字面量使用
0b
或0B
前缀:0b000000101
。注意,下划线的表述写法仅从Kotlin 1.1 开始支持。
对于字符字面量,和Java写法一样。使用单引号:
1
、\t
、\u0009
。
注意,对于演算操作来说是不需要显式指定类型的。
1 | fun foo(l: Long) = println(l) |
¶“Any” and “Any?”: the root types
Kotlin的Any
对应于Java的Object
。不同的是,Java中的Object
是所有类型的超类,但不包括原生类型。Kotlin中,Any
则是所有类型的超类,包括原生类型诸如Int
。
和Java一样,分配原生类型给变量类型为Any
时会自动装箱,
1 | val answer: Any = 42 // The value 42 is boxed, because Any is a reference type. |
Any
是不可为空类型,包含三个方法:toString
、equals
和hashCode
。其它方法诸如wait
和notify
并不可用,除非显式转换为java.lang.Object
。
¶The Unit type: Kotlin’s “void”
Unit
类型是Java中void
的完全实现。它可以作为一个函数的返回类型:
1 | fun f(): Unit { ... } |
语法层面上就是一个没有返回类型的语句块,
1 | fun f() { ... } |
基本上它和Java的void没有任何区别,唯一区分的是它被作为类型参数使用了。因为在语句块中它不需要返回任何值,return
语句也可以省略。
¶The Nothing type: “This function never returns”
为了阐述那种“没有执行完整”的逻辑,譬如无限循环语句、中间抛出异常的语句。Kotlin引入了这种特殊类型Nothing
。
1 | fun fail(message: String): Nothing { |
同样,Nothing
类型没有任何值,仅作为函数的返回类型或类型参数使用。这种返回类型为Nothing
的函数,可以用在 _elvis operator_的表达式上,
1 | val address= company.address ?: fail("No address") |
¶Collections and arrays
Kotlin在Java内建集合的基础上,增加了extension的feature。注意,是作为一种feature实现。
¶Nullability and collections
集合类型对于空值类型参数,需要严格区分List<Int?>
还是List<Int>?
还是List<Int?>?
。
1 | fun readNumbers(reader: BufferedReader): List<Int?> { |
¶Read-only and mutable collections
Kotlin中将对集合数据的访问和修改分离为不同的两个接口实现了。
大多数情况下, 我们只会用的只读的接口api。如果需要更新集合实例,可能需要创建一份copy新的实例。
1 | fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) { |
** read-only collections aren’t necessarily immutable **.
对于只读集合是不能被修改的,这样设计可以保证在多线程环境下获取数据的一致性(强一致)。
¶Kotlin collections and Java
无可否有,Kotlin的集合都有一个与之唯一对应的Java集合接口。集合中的数据不需要转换或封装类型。因为Kotlin中的集合更多是作为feature存在。
下面是常见的集合类型的扩展函数,
collection type | Read-only | Mutable |
---|---|---|
List | listOf |
mutableListOf arrayListOf |
Set | setOf |
mutableSetOf hashSetOf linkedSetOf sortedSetOf |
Map | mapOf |
mutableMapOf hashMapOf linkedMapOf sortedMapOf |
注意,setOf()
和mapOf()
返回Java标准库的类实例(Kotlin 1.0)。Kotlin将来的版本会重新设计实现。
由于接口层实现一致,Java和Kotlin的集合可以混用,不需要关心编译问题。
¶Collections as platform types
¶Arrays of objects and primitive types
而对于对象数组类型,提供了Array
类封装,如下,
1 | fun main(args: Array<String>) { |
访问方式依然通过数组下标访问array[index]
。
要在Kotlin中创建一个数组,有下面几种形式:
arrayOf
函数创建,arryOfNulls
函数创建,包含null
元素。当然,也仅能用于创建可能为空的元素。Array
构造函数创建,参数为元素个数+lambda表达式,如下
1 | >>> val letters = Array<String>(26) { i -> ('a' + i).toString() } |
其中,类型参数可以省略,编译期可以自动推断真实的数组类型。
为了表示Java的原生数据类型,Kotlin提供了对其的包装类型,譬如IntArray
、ByteArray
、CharArray
、BooleanArray
等等。对应于Java的int[]
、byte[]
、char[]
。
1 | val fiveZeros = IntArray(5) |
类似地,也可以调用构造函数创建,
1 | >>> val squares = IntArray(5) { i -> (i + 1) * (i + 1) } |
相应的,Kotlin也提供了内联函数toIntArray
实现对原生数据类型到封装数据类型的转换。
¶Summary
- Kotlin支持运行时的空值类型的探测可能
NullPointerException
错误。 - Kotlin提供工具诸如
?.
( safe call )、?:
( elvis operator )、!!
( not-null assertions ) 以及let
函数等简化空值的处理。 as?
提供简单的类型转换。- Kotlin提供对Java原生类型和封装类型的编译规则。
- 空值原生类型在Kotlin中对应于
Int?
,编译为Java的包装类型java.lang.Integer
。 Any
类是所有类的超类,但wait
和notify
方法是不可用的;Unit
对应于void
的逻辑实现。Nothing
类作为一种返回类型,表示函数没用正常结束的情况。- Kotlin中的集合实际上是对Java集合的增强。因此可以进行混用,无需关系集合的数据类型。
- Kotlin要求区分可变集合、可空(nullability)集合,以避免多线程数据不一致等问题。
- Kotlin中的
Array
类看起来是普通的类,但实际上是编译为Java的数组。 - 原生类型的数组,在Kotlin中有对应的包装类型代替,譬如
IntArray
对应于int[]
,以及提供了相应的内联函数进行转换,譬如toIntArray
。