Kotlin 的类型系统

主要内容

  1. 空值处理
  2. 原生类型和对应的Java类型
  3. Kotlin的集合以及与Java的关系

Kotlin对空值类型的处理,并不是使用ADT。诸如OptionEither,而是使用符号记法?。对于类型转换,不使用协变逆协变,而是使用asAny?这种语法。

Nullability

Nullability 在kotlin的type system是作为feature般的存在,用了避免NullPointerException错误。所以kotlin并不是要解决编译期或运行期的NPE问题,而是提供兼容手段,减少该异常在运行期出现的可能性。

因此,kotlin中需要讨论空值类型:kotlin通过标记值类型以允许可以为null,并提供空值类型的工具。

Nullable types

首先也是最重要的不同在于,Kotlin和Java类型系统上,kotlin是显式支持 nullable types 。什么意思?它提供了指定变量或属性允许为null的一种方式。如果一个变量为null,对其的方法调用是不安全的,因为它会导致NullPointerException。kotlin不允许这种调用以避免许多可能的异常。且看如下代码:

1
2
3
int strLen(String s) {
return s.length();
}

该函数是不安全的,传入的参数如果为null将会抛出NullPointerException。你可能需要根据需求在方法体内进行null检测。

下面是重写为kotlin的写法,

1
fun strLen(s: String) = s.length

如果传入null参数,会直接触发编译错误检查:

1
2
>>> strLen(null)
ERROR: Null can not be a value of a non-null type String

该参数被声明为String类型,在Kotlin中意味着它必须总是包含一个String实例。这是编译器强制性的,你不能传递一个null的参数。保证了函数strLen永远不会在运行时抛出NullPointerException

如果你希望允许传入null参数,可以写为:

1
fun strLenSafe(s: String?) = ...

将问号置于类型签名之后。譬如:String?Int?MyCustomType?,等等。

Figure 6.1

再次重申,没有用?标记的变量是不能存储null引用的。意味着所有常规类型默认都是非空的(non-null),除非显式指定可以为空。

一旦声明了一个nullable type类型, 一系列的操作将变得严格。例如,不能对其调用方法:

1
2
>>> fun strLenSafe(s: String?) = s.length()
ERROR: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type kotlin.String?

不可以将其分配给一个非空类型:

1
2
3
>>> val x: String? = null
>>> var y: String = x
ERROR: Type mismatch: inferred type is String? but String was expected

也不可以传给非空参数方法,

1
2
>>> strLen(x)
ERROR: Type mismatch: inferred type is String? but String was expected

你需要对其空值的可能性进行处理,

1
2
3
4
5
6
7
fun strLenSafe(s: String?): Int = if (s != null) s.length else 0 // By adding the check for null, the code now compiles.

>>> val x: String? = null
>>> println(strLenSafe(x))
0
>>> println(strLenSafe("abc"))
3

这种写法有点啰嗦,后面会讲到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直接作为结果。

Figure 6.2

safe-call 的写法也可以用在属性访问上,如下:

1
2
3
4
5
6
7
8
9
10
class Employee(val name: String, val manager: Employee?)

fun managerName(employee: Employee): String? = employee.manager?.name

>>> val ceo = Employee("Da Boss", null)
>>> val developer = Employee("Bob Smith", ceo)
>>> println(managerName(developer))
Da Boss
>>> println(managerName(ceo))
null

safe-null 可以进行链式调用,

1
2
3
4
5
6
7
8
9
10
11
class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
val country = this.company?.address?.country // Several safe-call operators can be in a chain.
return if (country != null) country else "Unknown"
}
>>> val person = Person("Dmitry", null)
>>> println(person.countryName())
Unknown

Elvis operator: “?:”

带默认值的空值处理操作符 ?: 称作 Elvis operator 。写法如下,

1
2
3
fun foo(s: String?) {
val t: String = s ?: "" // If "s" is null, the result is an empty string.
}

类似于Java的三元运算。逻辑如下,

Figure 6.3

通常和 safe-call 操作一起使用,

1
2
3
4
5
fun strLenSafe(s: String?): Int = s?.length ?: 0
>>> println(strLenSafe("abc"))
3
>>> println(strLenSafe(null))
0

countryName函数也可以改为,

1
2
fun Person.countryName() =
company?.address?.country ?: "Unknown"

有些时候,希望直接抛出异常,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)

fun printShippingLabel(person: Person) {
val address = person.company?.address?:throw IllegalArgumentException("No address") // Throws an exception if the address is absent
with(address) { // "address" is non-null.
println(streetAddress)
println("$zipCode $city, $country")
}
}

>>> val address = Address("Elsestr. 47", 80687, "Munich", "Germany")
>>> val jetbrains = Company("JetBrains", address)
>>> val person = Person("Dmitry", jetbrains)

>>> printlnShippingLabel(person)
Elsestr. 47
80687 Munish, Germany

>>> printShippingLabel(Person("Alexey", null))
java.lang.IllegalArgumentException: No address

Safe casts: “as?”

kotlin的 safe cast 也总是和 safe callelvis operator 一起使用。类似地,作为feature用于处理ClassCastException的兼容性问题。

Figure 6.4

as? 操作会尝试转换值的类型,如果不匹配将返回null。它常和 Elvis operator 一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person(val firstName: String, val lastName: String) {
override fun equals(o: Any?): Boolean {
val otherPerson = o as? Person ?: return false // Checks the type and returns false if no match
return otherPerson.firstName == firstName && otherPerson.lastName == lastName // After the safe cast, the variable otherPerson is smart-cast to the Person type.
}

override fun hashCode(): Int = firstName.hashCode() * 37 + lastName.hashCode()
}

>>> val p1 = Person("Dmitry", "Jemerov")
>>> val p2 = Person("Dmitry", "Jemerov")
>>> println(p1 == p2)
true
>>> println(p1.equals(42))
false

Not-null assertions: “!!”

not-null assertion 非空断言操作符!!会在值为null的时候,直接抛出NullPointerException,逻辑如下,

Figure 6.5

1
2
3
4
5
6
7
fun ignoreNlls(s: String?) {
val sNotNull: String = s!! // The exception points to this line.
println(sNotNUll.length)
}

>>> jgnoreNulls(null)
Exception in thread "main" kotlin.KotlinNullPointerException at <...>.ignoreNulls(...

出现空值时,在运行时直接抛出NullPointerException。这种需要断言的情况大多数出现在类似swing ui这些框架中,譬如,

1
2
3
4
5
class CopyRowAction(val list: JList<String>): AbstractAction() {
override fun isEnabled(): Boolean = list.selectedValue != null
override fun actionPerformed(e: ActionEvent) {
val value = list.selectedValue!! // actionPerformed is called only if isEnabled returns "true"
// copy value to clipboard

需要注意的是,如果不希望使用!!,你需要自定义返回,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
2
3
>>> val email: String? = ...
>>> sendEmailTo(email)
ERROR: Type mismatch: inferred type is String? but String was expected

这是你需要显式检查传入的值是否为null:

1
if (email != null) sendEmailTo(email)

Kotlin中的let的逻辑如下,

Figure 6.6

因此,使用let结合lambda有更简洁的写法:

1
email?.let { email -> sendEmailTo(email) }

或者用it替换,

1
email?.let {sendEmailTo(it) }

如果为null,lambda的表达式永远不会执行。

Late-initialized properties

某些框架对于一些变量或属性的声明是必须要延迟执行的,譬如JUnit这类。这时候就需要预先使用? 这类 nullable type替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyService {
fun performAction(): String = "foo"
}

class MyTest {
private var myService: MyService? = null // Declares a property of a nullable type to initialize it with null

@Before fun setUp() {
myService = MyService() // Provides a real initializer in the setUp method
}

@Test fun testAction() {
Assert.assertEquals("foo", myService!!.performAction()) // You have to take care of nullability: use !! or ?.
}
}

这看起来很丑,特别是如果要访问属性很多次时。为了解决这个问题,你可以将myService属性声明为 late-initialized 。你需要用到lateinit修改器,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyService {
fun performAction(): String = "foo"
}

class MyTest {
private lateinit var myService: MyService // Declares a property of a non-null type without an initializer

@Before fun setUp() {
myService = MyService() // Initializes the property in the setUp method as before
}

@Test fun testAction() {
Assert.assertEquals("foo", myService.performAction()) // Accesses the proerty without extra null checks
}
}

所有late-initialized的属性都是 var的,因为你需要在构造器外部更改它的值,以及val属性则被编译为final的,必须在构造器内初始化。lateinit更多被用于DI(dependency injection)之类的框架中,譬如spring等。

Extensions for nullable types

空值类型在Kotlin中属于类型系统部分,当然也有extension function,写法上没有差别,只不过接收类型(receiver type)是带有?标记的,

1
2
fun String?.isNullOrBlank(): Boolean = // Extension for a nullable String
this == null || this.isBlank() // A smart cast is applied to the second "this".

需要注意的是,之前介绍的内联函数let也是带有接收类型,但它不做null的检查。所以如果在lambda调用空值类型而不使用safe-call 操作符,lambda内的参数也可能是空值类型的,

1
2
3
>>> val person: Person? = ...
>>> person.let { sendEmailTo(it) } // No safe call, so "it" has a nullable type.
ERROR: Type mismatch: inferred type is Person? but Person was expectd

所以你必须尽早处理空值,person?.let { sendEmailTo(it) }

Nullability of type parameters

默认地,Kotlin中所有函数的参数类型都是可空的。任何类型,包括空值类型,都可以被替代为参数类型;因此,类型的参数声明是运行为null,即使参数类型T没有以?结尾。考虑如下代码。

1
2
3
4
5
fun <T> printHashCode(t: T) {
println(t?.hashCode9)) // You have to use a safe call because "t" might be null.
}
>>> printHashCode(null) // "T" is inferred as "Any?".
null

在这个例子中,类型参数T被推断为Any?。因此,t是运行为null的,即使没有带?

若要使其类型参数不为null,需要指定一个非空类型边界,如下,

1
2
3
4
5
6
7
fun <T: Any> printHashCode(t: T) {  // Now "T" can't be nullable.
println(t.hashCode())
}
>>> printHashCode(null) // This code doesn't compile: you can't pass null because a non-null value is expected.
Error: Type parameter bound for `T` is not satisfied
>>> printHashCode(42)
42

Primitive and other basic types

Kotlin不区分原生类型和封装类型。

Primitive types: Int, Boolean, and more

  • 对于变量、属性、参数以及返回值,Kotlin的Int类型编译为Java的原生类型int
  • 对于泛型类,Kotlin的Int则编译为Java的包装类型。如集合类。

下面对应于Java的原生类型列表:

  • Integer Types : ByteShortIntLong
  • Floating-point number types : FloatDouble
  • Character type : Char
  • Boolean type : Boolean

Nullable primitive types: Int?, Boolean?, and more

Kotlin中的可空原生类型并不能表述为Java的原生类型,因为null在Java中仅存储为变量的引用。意味着无论kotlin中用的是哪种可空的原生类型,只能编译为Java的包装类型。

以下面例子开始,

1
2
3
4
5
6
7
8
9
10
11
12
data class Person(val name: String, val age: Int? = null) {
fun isOlderThan(other: Person): Boolean? {
if (age == null || other.age == null)
return null
return age > other.age
}
}

>>> println(Person("Sam", 35).isOlderThan(Person("Amy", 42)))
false
>>> println(Person("Sam", 35).isOlderThan(Person("Jane")))
null

这里并不能比较两个Int?类型,因为其中一个有可能为null。相反,你必须两个值都做null检查。之后再比较它们的真实值。

另外,对于泛型类型,kotlin会使用它的装箱类型替代,如下

1
val listOfInts = listOf(1, 2, 3)

会创建一个Integer的集合变量。

这是因为,泛型的实现是有JVM所控制的。JVM并不支持原生类型作为泛型参数,所以泛型类(不论是Java还是Kotlin)都必须使用装箱类型表述。

Number conversions

Kotlin和Java的一个重要区分是对数字的转换处理。Kotlin并不自动将数字类型由一种类型,转换为另一种,

1
2
val i = 1
val l: Long = i // Error: type mismatch

相反,你需要显式转换,

1
2
val i = 1
val l: Long = i.toLong()

每个原生类型都定义了相应的转换函数(处理Boolean):toByte()toShort()toChar()等等。函数支持两个方向的转换:小类型到大类型,如Int.toLong(),大类型到小类型,Long.toInt()

Kotlin需要显式进行转换是为了避免出现意外,特别是比较封装类型时。equals方法会检测封装类型和值。因为,在Java中,new Integer(42).equals(new Long(42))是返回false的。

假如Kotlin支持隐式转换,

1
2
3
val x = 1  // Int variable
val list = listOf(1L, 2L, 3L) // List of Long values
x in list // False if Kotlin supported implicit conversions

这会触发编译错误,kotlin要求你显式地转换指定的类型进行比较,

1
2
3
>>> val x= 1
>>> println(x.toLong() in listOf(1L, 2L, 3L))
true

Primitive type literals

Kotlin支持下面几种形式的数字字面量写法:

  • Long类型使用L作为后缀:123L
  • Double类型使用标准浮点数的写法:0.122.02e101.2e-10
  • Float使用fF作为后缀:123.4f.456F1e3f
  • 十六进制字面量使用0x0X前缀:0xCAFEBABE0xbcdL
  • 二进制字面量使用0b0B前缀:0b000000101

注意,下划线的表述写法仅从Kotlin 1.1 开始支持。

对于字符字面量,和Java写法一样。使用单引号:1\t\u0009

注意,对于演算操作来说是不需要显式指定类型的。

1
2
3
4
5
6
fun foo(l: Long) = println(l)

>>> val b: Byte = 1 // Constant value gets the correct type
>>> val l = b + 1L // works with Byte and Long arguments.
>>> foo(42) // The compiler interprets 42 as a Long value.
42

“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是不可为空类型,包含三个方法:toStringequalshashCode。其它方法诸如waitnotify并不可用,除非显式转换为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
2
3
4
5
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
>>> fail("Error occurred")
java.lang.IllegalStateException: Error occurred

同样,Nothing类型没有任何值,仅作为函数的返回类型或类型参数使用。这种返回类型为Nothing的函数,可以用在 _elvis operator_的表达式上,

1
2
val address= company.address ?: fail("No address")
println(address.city)

Collections and arrays

Kotlin在Java内建集合的基础上,增加了extension的feature。注意,是作为一种feature实现。

Nullability and collections

集合类型对于空值类型参数,需要严格区分List<Int?>还是List<Int>?还是List<Int?>?

1
2
3
4
5
6
7
8
9
10
11
12
fun readNumbers(reader: BufferedReader): List<Int?> {
val result = ArrayList<Int?>() // Creates a list of nullable Int values
for (line in reader.lineSequence()) {
try {
val number = line.toInt()
result.add(number) // Adds an integer(a non-null value) to the list
} catch (e: NumberFormatException) {
result.add(null) // Adds null to the list, because the current line can't be parsed to an integer
}
}
return result
}

Figure 6.10

Read-only and mutable collections

Kotlin中将对集合数据的访问和修改分离为不同的两个接口实现了。

Figure 6.11

大多数情况下, 我们只会用的只读的接口api。如果需要更新集合实例,可能需要创建一份copy新的实例。

1
2
3
4
5
6
7
8
9
10
11
fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
for (item in source) { // Loops over all items in the source collection
target.add(item) // Adds items to the mutable target collection
}
}

>>> val source: Collection<Int> = arrayListOf(3, 5, 7)
>>> val target: MutableCollection<Int> = arrayListOf(1)
>>> copyElement(source, target)
>>> println(target)
[1, 3, 5, 7]

** read-only collections aren’t necessarily immutable **.

对于只读集合是不能被修改的,这样设计可以保证在多线程环境下获取数据的一致性(强一致)。

Figure 6.12

Kotlin collections and Java

无可否有,Kotlin的集合都有一个与之唯一对应的Java集合接口。集合中的数据不需要转换或封装类型。因为Kotlin中的集合更多是作为feature存在。

Figure 6.13

下面是常见的集合类型的扩展函数,

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
2
3
4
5
fun main(args: Array<String>) {
for (i in args.indices) { // Uses the array.indices extension property to iterate over the range of indices
println("Argument $i is: ${args[i]}") // Accesses elements by index with array[index]
}
}

访问方式依然通过数组下标访问array[index]

要在Kotlin中创建一个数组,有下面几种形式:

  • arrayOf函数创建,
  • arryOfNulls函数创建,包含null元素。当然,也仅能用于创建可能为空的元素。
  • Array构造函数创建,参数为元素个数+lambda表达式,如下
1
2
3
>>> val letters = Array<String>(26) { i -> ('a' + i).toString() }
>>> println(letters.joinToString(""))
abcdefghijklmnopqrstuvwxyz

其中,类型参数可以省略,编译期可以自动推断真实的数组类型。

为了表示Java的原生数据类型,Kotlin提供了对其的包装类型,譬如IntArrayByteArrayCharArrayBooleanArray等等。对应于Java的int[]byte[]char[]

1
2
val fiveZeros = IntArray(5)
val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0)

类似地,也可以调用构造函数创建,

1
2
3
>>> val squares = IntArray(5) { i -> (i + 1) * (i + 1) }
>>> println(squares.joinToString())
1, 4, 9, 16, 25

相应的,Kotlin也提供了内联函数toIntArray实现对原生数据类型到封装数据类型的转换。

Summary

  • Kotlin支持运行时的空值类型的探测可能 NullPointerException错误。
  • Kotlin提供工具诸如 ?. ( safe call )、?: ( elvis operator )、!! ( not-null assertions ) 以及let函数等简化空值的处理。
  • as?提供简单的类型转换。
  • Kotlin提供对Java原生类型和封装类型的编译规则。
  • 空值原生类型在Kotlin中对应于Int?,编译为Java的包装类型java.lang.Integer
  • Any类是所有类的超类,但waitnotify方法是不可用的;Unit对应于void的逻辑实现。
  • Nothing类作为一种返回类型,表示函数没用正常结束的情况。
  • Kotlin中的集合实际上是对Java集合的增强。因此可以进行混用,无需关系集合的数据类型。
  • Kotlin要求区分可变集合、可空(nullability)集合,以避免多线程数据不一致等问题。
  • Kotlin中的Array类看起来是普通的类,但实际上是编译为Java的数组。
  • 原生类型的数组,在Kotlin中有对应的包装类型代替,譬如IntArray对应于int[],以及提供了相应的内联函数进行转换,譬如toIntArray