Kotlin的类型系统

可空性

可空类型

1
2
3
4
5
fun strLen(s:String) = s.length
//使用可能为null的实参调用StrLen是不允许的,在编译期就会被标记成错误
>>>strLen(null)
ERROR:Null can not be a value of a non-null type String

问号可以加在任何类型的后面来表示这个类型的变量可以存储null引用

1
Type? = Type or null

没有问号的类型表示这种类型的变量不能存储null引用,这说明所有常见类型默认都是非空的,除非显式的把它标记为空

安全调用运算符:“?.”

它允许把一次null检查和一次方法调用合并成一个操作

1
2
//表达式 s?.toUpperCase() 等同于下面这种烦琐的写法
if(s!=null) s.toUpperCase() else null
1
2
3
4
5
foo != null
-----------> foo.bar()
foo?.bar()
foo == null
-----------> null

安全调用运算符只会调用非空值的方法

Elvis运算符:“?:”

Elvis运算符接收两个运算数,如果第一个运算数不为null,运算结果就是第一个运算数;如果第一个运算数为null,运算结果就是第二个运算数

1
2
3
4
5
foo != null
-----------> foo
foo ?: bar
foo == null
-----------> bar

安全转换:“as?”

as?运算符尝试把值转换成指定的类型,如果值不是合适的类型就返回null

1
2
3
4
5
foo is type
-----------> foo as Type
foo as? Type
foo !is Type
-----------> null

安全调用、安全转换和Elvis运算符都非常有用,它们出现在Kotlin代码中的频率很高。但有时你病不需要Kotlin的这些支持来处理null值,你只需要直接告诉编译器这个值实际上并不是null,所以我们就来继续看看如何做到这点

非空断言:“!!”

非空断言是Kotlin提供给你的最简单直率的处理可空类型值的工具。它使用双感叹号表示,可以把任何值转换成非空类型。如果对null值做非空断言,则会抛出异常

1
2
3
4
5
foo != null
-----------> foo
foo!!
foo == null
-----------> null

“let”函数

let函数就是把一个调用它的对象变成lambda表达式的参数。如果结合安全调用语法,他能有效的把调用let函数的可空对象转变成非空类型

1
2
3
4
5
6
foo != null
-----------> 在lambda内部it是非空的
foo?.let{
..it .. }
foo == null
-----------> 什么都不会发生

安全调用“let”只在表达式不为null时执行lambda

1
2
3
4
5
6
7
8
9
10
val email:String? = "adu@163.com"
fun sendEmailTo(email:String){
println("email:$email")
}
//1、email非空时调用,在lambda中把email当作非空实参使用
email?.let{ email -> sendEmailTo(email)}
//2、使用自动生成的名字it
email?.let{ sendEmailTo(it) }

注意,如果有一些很长的表达式结果不为null,而你又要使用这些结果时,let表示法特别方便。这种情况下你不必创建一个单独的变量。

1
2
3
4
5
6
7
8
9
10
val person:Person? = getTheBestPerosnInTheWorld()
if (perosn != null) sendEmailTo(person.email)
//和功能相同但没有额外变量的代码
getTheBestPerosnInTheWorld()?.let{ sendEmailTo(it.email)}
//这个函数返回null,所以lambda中的代码永远不会执行
fun getTheBestPerosnInTheWorld() : Person? = null

还有一种情况是,属性最终是非空的,但不能使用非空值在构造方法中初始化。接下来我们看看Kotlin如何处理这种情况

延迟初始化的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyService {
fun performAction() :String = "foo"
}
class MyTest {
private lateinit var myService: MyService
@Before
fun setUp() {
myService = MainActivity.MyService()
}
@Test fun testAction() {
Assert.assertEquals("foo",myService.performAction())
}
}

延迟初始化的属性都是var,因为需要在构造方法外修改它的值。尽管这个属性是非空类型,但是你不需要在构造方法中初始化它

可空类型的扩展

用可空接收者调用扩展函数

1
2
3
4
5
6
7
8
9
10
fun verifyUserInput (input:String?){
//这里不需要安全调用
if (input.isNullOrBlank()) {
println("Please fill in the required fields")
}
}
>>>verifyUserInput(" ")
Please fill in the required fields
>>>verifyUserInput(null)//这个接收者调用isNullorBlank并不会导致任何异常
Please fill in the required fields

类型参数的可空性

Kotlin中所有泛型类和泛型函数的类型默认都是可空的。任何类型,包括可空类型在内。都可以替换类型参数

1
2
3
4
5
6
7
8
//处理可空的参数类型
fun<T> printHashCode(t:T) {
//因为“t”可能为null,所以必须使用安全调用
println(t?.hashCode())
}
>>> printHashCode(null) //“T”被推导成“Any?”
>>> null

上面代码中“T”被推导的类型时可空类型“Any?”,因此,尽管没有用问号结尾,实参t依然允许持有null

1
2
3
4
5
6
7
8
//为类型参数声明非空上界
fun<T: Any> printHashCode(t:T) { //现在T就不是可空的
println(t.hashCode())
}
>>> printHashCode(null) //这段代码是无法编译的,不能传递null,期望是非空值
>>> ERROR

可空性和Java

在实现Java类或者接口的方法时一定要搞清楚它的可空性。因为方法的实现可以在非Kotlin的代码中被调用,Kotlin编译器会为你声明的每一个非空的参数生成非空断言。如果Java代码传给这个方法一个null值,断言将会触发,你会得到一个异常,即便你从没有在你的实现中访问过这个参数的值。

基本数据类型和其他基本类型

Kotlin并不区分基本数据类型和它们的包装类

基本数据类型:Int、Boolean及其他

在Java中,不能对基本数据类型的值调用方法或者把他们存放在集合中,在需要对象的时候对基本数据进行封装。因此,你不能用Collection< int>来定义一个整数的集合,而必须用Collection< Integer>来定义

Kotlin并不区分基本类型和包装类型,你使用的永远是同一个类型(比如:Int)

1
2
val i: Int = 1
Val list: List<Int> = listOf(1,2,3)

可空的基本数据类型:Int?、Boolean?及其他

1
2
3
4
5
6
7
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
}
}

必须检查两个值都不为null,然后编译器才允许你正常的比较它们

数字转换

1
2
3
4
5
6
val i =1
val l:Long = i //错误:类型不匹配
//因此,必须显式地进行转换
val i =1
val l:Long = i.toLong()

每一种基本数据类型(Boolean除外)都定义有转换函数:toByte()、toShort()、toChar()。这些函数都支持双向转换:既可以把小范围的类型括展到大范围,比如Int.toLong(),也可以把大范围的类型截取到小范围,比如Long.toInt()

Kotlin要求转换必须是显式的

1
2
3
val x =1
println(x in listof(1L,2L,3L)) //假设Kotlin支持隐式转换的话仍是false
println(x.toLong() in listof(1L,2L,3L))//true
  • 字符串转换

kotlin标准库提供了一套相似的扩展方法,用来把字符串转换成基本数据类型(toInt、toByte、toBoolean等)

1
2
println("42".toInt())
>>42

“Any”和“Any?”:根类型

在kotlin中,Any是所有类型的超类型(所有类型的根),包括像Int这样的基本数据类型

在java中,Object只是所有引用类型的超类(引用类型的根),而基本数据类型并不是类层级结构的一部分

注意Any是非空类型,所以Any类型的变量不可以持有null值。在Kotlin中如果需要可以持有任何可能值的变量,包括null在内,必须使用Any?类型

当Kotlin函数使用Any时,它会被编译成Java字节码中的Object

Unit类型:Kotlin的“void”

Kotlin中的Unit类型完成了Java中的void一样的功能。
Unit是一个完备的类型,可以作为类型参数,而void却不行。
当重写返回泛型参数的函数时,只需要让方法返回Unit类型的值

1
2
3
4
5
6
7
8
9
10
interface Processor<T> {
fun process() : T
}
class NoResultProcessor : Processor<Unit> {
//返回Unit,但可以省略类型说明
override fun process() {
//do stuff
//这里不需要显式的return
}

Nothing类型:“这个函数永不返回”

在kotlin中throw是表达式,所以你可以使用塔作为Elvis表达式的一部分

1
val s = person.name ?: throw IllegalArgumentException("Name required)

throw表达式的类型是特殊类型Nothing。该类型没有值,而是用于标记永远不能达到的代码位置

集合与数组

kotlin以java集合库为基础构建,并通过扩展函数增加的特性来增强它

可空性和集合

kotlin完全支持类型参数的可空性

1
2
3
4
5
6
7
8
9
10
11
12
fun readNumbers(reader: BufferedReader):List<Int?>{
val result = ArrayList<Int?>() //创建包含可空Int值的列表
for (line in reader.lineSequence()) {
try {
val number = line.toInt()
result.add(number) //向列表添加整数(非空值)
}catch (e:NumberFormatException){
result.add(null) //向列表添加null,因为当前行不能被解析成整数
}
}
return result
}

对包含可空值的集合使用filterNotNull

1
2
3
4
5
fun addValidNumbers(numbers:List<Int?>) {
val validNumbers = numbers.filterNotNull()
println("sum of valid numbers: ${validNumbers.sum()}")
println("Invalid numbers: ${numbers.size - validNumbers.size}")
}

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器