¶主要内容
- 空值处理
- 原生类型和对应的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。