6.1 可空性
可空性是Kotlin类型系统中帮助你避免NullPointException
异常的一个特性。
现代语言包括Kotlin,是将这个问题从运行时错误变为编译时错误。通过将可空性作为类型系统的一部分,编译器可以在编译期发现很多可能的问题,并减少在运行时发生异常的几率。
6.1.1 可空性
Kotlin和Java第一个也许是最重要得一个区别就是:Kotlin对可空类型有显式的支持。意思就是可以指明在程序中哪个变量或者属性是可以为空Null的。
在Java中调用这个函数可能导致空指针异常
public int strLength(String str) {
return str.length();
}
使用kotlin重写这个函数,如果我们不想希望调用这个函数时,传递的实参是Null,Kotlin中要像下面这样声明
fun strLength(str: String):Int = str.length
此时,传递一个null给这个方法,就会报编译时错误
strLength(null)
Error:Null can not be a value of a non-null type String
如果想传递任意类型的实参,包括null类型,需要在实参类型后面加 ?
fun strLength(str: String?):Int = ...
在类型后面加?,代表这个类型的值可以为空。一个没有?符号的类型的变量是不能存储空引用的,这就意味着常见的类型默认都是非空的,除非显式的把它表明为可空的。
6.1.2 类型的含义
什么是类型呢?维基百科的解释是:类型时数据的分类...决定了该类型可能的值,以及该类型值所能完成的操作。
在java中,以String为例,一个String类型的变量可能是两种类型的值:一个类的实例或者Null,这两种值是完全不同的。而且在这两种变量的值上进行的操作也是完全不同的.
这就说明,Java的类型系统在这种情况下并不能很好的工作。即使你已经声明了变量的类型是String,但是你依然无法知道能对这个变量进行何种操作,除非进行额外的检查。
Kotlin中的可空类型为这个问题提供了全面的解决方案,区分可空和和非空类型使这些操作变得明朗:哪些的值的操作是允许的,哪些操作可能导致运行时异常,因此要禁止。
6.1.3 安全调用操作符"?.
"
安全调用操作符"?.
",可以让你结合空检查和方法调用在一个操作符中。也就是说,如果你要调用一个非空值的方法,方法会正常被调用;如果值是空,方法不会被调用,整个表达式的值为null。
安全调用符不仅可以用来调用方法,也可以用来获取属性。如果对象中有多个可空类型的属性,通常可以在同一个表达式中方便的使用多个安全调用。
带空安全检查的方法调用序列在Java代码中太常见了,Kotlin中多个安全调用符一起使用会让语句更简洁。
6.1.4 Elvis 运算符"?:
"
Kotlin中有方便的运算符来提供代替null的默认值,叫做Elvis运算符
//如果字符串是null的话,返回的字符串长度为 0
fun strLength(str: String?): Int? = str?.length ?: 0
这个操作符接受两个值,如果第一个值不为空,运算结果就是第一个值;如果第一个运算符为null,运算结果就是第二个值。
Elvis运算符经常和安全调用操作符一起使用,用一个值来代替对null对象调用方法是返回的null.
6.1.5 安全转换:as?
Kotlin中常规用来转换类型的操作符是:as
运算符。但是如果要转换的值并不是我们指定的类型的话就会抛出ClassCastException
异常。
Kotlin中使用as?
尝试把值转换成指定的类型,如果值不是合适的类型就返回null
一种常见的模式是,把Elvis运算符和安全转换相结合使用。
val otherPerson = o as? Person ?: return false
6.1.6 非空断言:!!
非空断言!!
是Kotlin中处理空类型最简单最简单的方式。它会将任何值转换为非空类型,如果是null值,则会抛出异常。
如果传null给下面这个函数,Kotlin会抛出空指针异常,但是是在断言声明那里抛出的,而不是调用str的方法时调用的。这样做,你就是在告诉编译器,我知道这个值不是null,并且为如果出错导致的异常做好了准备。
fun ignoreNull(str: String?) {
println(str!!.length)
}
>>ignoreNull(null)
Exception in thread "main" kotlin.KotlinNullPointerException
at com.m1Ku.kt07.TypeDemo1Kt.ignoreNull(TypeDemo1.kt:20)
at com.m1Ku.kt07.TypeDemo1Kt.main(TypeDemo1.kt:6)
非空断言也有适合的使用场景,例如你已经在一个函数中检查了某参数是非空的,然后再在另一个函数中使用这个参数时,此时编译器并不能识别这个参数是否是安全的,此时你就无需再次检查参数的非空性,可以直接使用非空断言。
注意
当你使用非空断言!!发生异常时,异常栈跟踪信息只会指出异常抛出的行数,而不会指出是哪一个表达式。所以,要避免在同一行代码中使用多个非空断言。
6.1.7 let函数
let
函数用在将可空类型的参数传给期望为非空参数的函数时使用。
将let
函数和安全调用符一起使用,可以有效的将调用let
函数的可空对象转换为非空类型的对象。
//只有当str不为空时,let函数才会被调用
fun printStrLength(str: String?) {
str?.let { println(it.length) }
}
6.1.8 延迟初始化属性
Kotlin中会要求你在构造方法中初始化所有属性,如果一个属性是非空类型的,你必须提供一个非空的初始化值。如果不提供初始化值,就必须定义可空类型,但是这样做的话,每次访问这个属性都需要进行非空检查或者使用非空断言。
当你需要多次访问属性时,这种方式看起来很难看。为了解决这个问题,我们可以声明属性为延迟初始化
的,使用lateinit
修饰符来修饰。
private lateinit var loanFragment: LoanFragment
override fun initViews() {
loanFragment = LoanFragment.newInstance()
mCurFragment = loanFragment
}
6.1.9 可空类型的扩展函数
对于可空类型的扩展函数,调用时不需要加安全调用操作符?.
fun String?.getStrLength() = this?.length
str.getStrLength()
当为可空类型定义了扩展函数时,就意味着我们可以用可空类型的值调用这个函数。在函数体中的this
就可能是空的,所以需要显式的检查。在Java中,this永远是非空的,因为他就是你当前类实例的引用。在Kotlin中不再是这样的,在可空类型的扩展函数中,this就可能是空了。
前面讨论的let
函数可以接受可空类型的参数,并且他不能检查值是否为空。如果使用let
函数时不使用安全调用符,lambda的参数也会是可空的。所以,如果想使用let
函数检查参数的非空,就需要使用安全调用符?.
6.1.10 类型参数的可空性
默认情况下,Kotlin在函数和类中的所有类型参数都是可空的。一个类型参数可以替代为任何类型,包括可空类型;在这种情况下,将类型参数用作一个类型的声明是可以为null的,及时类型参数T后面没有?问号
fun <T> printHashCode(t:T){
println(t?.hashCode())
}
在这个例子中,T类型被推断为一个可控的类型Any?
,因此参数t可能为null的,即使类型声明T后面没有?问号
如果想让类型参数是非空的,你需要指定类型上界,这样就不能传递可空参数了。
//现在T就不是可空的了
fun <T:Any> printHashCode(t:T){
println(t.hashCode())
}
6.1.11 可空性和Java
前面的讨论都是针对Kotlin可空性的讨论,但是我们知道Java的类型系统是不支持可空性的。那么当Kotlin和Java一起使用时,我们丢失会所有的安全性嘛,还是每个都需要进行空安全的检查呢。
其实Kotlin可以识别Java中@Nullable``@NotNull
等对可空性的注解,带@Nullable的String会被Kotlin识别为String?,而@NotNull的String会被识别为String。而当没有这些注解时,Java类型会被Kotlin识别为平台类型
平台类型
本质上,平台类型就是在Kotlin中没有可空性信息的一个类型,你可以把当做可空类型或者非空类型使用。这就意味着你需要对这种类型所做的所有操作负责,并且编译器允许你对这种类型执行所有操作。在Kotlin中不能声明一个平台类型的变量,这些类型只能来自Java代码。
我们在Kotlin中使用平台类型时,要充分理解可空性
6.2 基本数据类型和其他基本类型
Kotlin不区分基本数据类型和其包装类
6.2.1 基本数据类型:Int,Boolean和其它
在Java中对基本数据类型和引用类型做了区分,基本数据类型变量直接存储的值,而引用类型存储的是引用内存地址。当我们需要调用基本数据类型的方法时,就需要对其进行包装: int -> Integer
,此时也可以定义一个整形的集合Collection<Integer>
Kotlin中并不区分基本数据类型和其包装类型,可以使用相同的类型。这样设计很方便,可以直接调用数字类型的值的方法
val a: Int = 1
val list: List<Int> = listOf(1, 2, 3)
对应Java基本数据类型的类型完整列表如下:
- 整数类型:
Byte
、Short
、Int
、Long
- 浮点类型:
Float
、Double
- 字符类型:
Char
- 布尔类型:
Boolean
6.2.2 可空基本数据类型:Int,Boolean和其它
Kotlin中的可空类型不能用Java的基本数据类型来表示,因为null只能被存储在Java的引用类型变量中。这意味着当在在Kotlin中使用可空类型的变量时,它会编译成Java中相应的包装类
class Person(val name: String, val age: Int?)
Person的age属性会被当做Integer
来存储
6.2.3 数字转换
Kotlin和Java有一个很重要的不同点就是数字转换:Kotlin不会自动将数字从一个类型转换为另一个类型,即使是转换成范围更大的类型
//代码编译错误 :Type mismatch
val a = 1
val b:Long = a
Kotlin为基本类型都定义了转换函数(除了Boolean):toByte()
,toChar()
,toChar()
等等;这些函数支持双向转换,可以将小范围类型扩展为大范围类型,比如Int.toLong()
,也可以将大范围类型变为小范围的,如Long.toInt()
当书写数字字面值时,一般不需要使用转换函数:使用特殊语法显式的标明常量的类型,比如:42L
或者42.0f
;或者使用算数运算符时,他们可以接收所有适当的数字类型,并自动进行类型转换
val a:Byte = 1
//字节类型和长整型的计算
val b = 2L + a
6.2.4 “Any”和“Any?”:根类型
类似于Object是Java类体系层级中的根类,Any
类型Kotlin中所有非空类型的超类。但是在Java中,Object只是所有引用类型的超类,而Kotlin中Any
是所有类型的超类,包括基本类型比如Int
。和Java中一样的是,将一个基本类型的值赋值给Any
类型的变量会自动装箱
val attr: Any = 2 //自动装箱
需要注意的是,Any是一个非空类型,所以他不能持有null值。如果想持有Kotlin中所有的类型,包括null值,那就要使用Any?
类型
6.2.5 Unit类型:Kotlin的 "void"
Kotlin中的Unit
类型实现了和Java中void一样的功能。语法上,没有返回值的函数可以省略Unit
Kotlin的Unit和Java的有什么不一样呢?
Unit是一个完备的类型,可以作为类型参数,而void不行。只存在一个值时Unit类型,这个值也叫作Unit,并且在函数中隐式地返回。
interface Processor<T> {
fun process(): T
}
//Unit作为类型参数,此时不需要显式的写上return语句,编译器会隐式的加上return语句
class defaultProcessor:Processor<Unit>{
override fun process() {
}
}
6.2.6 Nothing类型:“这个函数永不返回”
对一些Kotlin中的函数来说,“返回值”这个概念没有意义,因为他们永远不会成功的结束。例如,在测试库中fail
函数,通过抛异常让当前测试失败,或者是有无限循环的函数也不会成功的结束。当分析调用这样函数的代码时,知道这些函数不会正常结束是有意义的。
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
Kotlin使用特殊的返回值Nothing
来表示这个概念。Nothing
类型没有任何值,所以只有把它用作函数返回类型或者类型参数才有意义
返回Nothing
的函数可以放在Elvis运算符右边来做先决条件检查:
val address = company.address ?: fail("No address")
println(address.city)
6.3 集合和数组
Kotlin的集合建立在Java集合库的基础上,通过扩展函数的形式扩展其特性的。
6.3.1 可空性和集合
Kotlin集合支持类型参数的可空性,就像在变量类型后面加?符号一样,当类型作为类型参数时,也可以以相同的方式来标记
fun readNumbers(reader: BufferedReader): List<Int?> {
//定义一个可以持有Int?类型的集合,也就是说它可以持有Int和null
val result = ArrayList<Int?>()
for (line in reader.lineSequence()) {
try {
val a = line.toInt()
result.add(a)
} catch (e: NumberFormatException) {
result.add(null)
}
}
return result
}
List<Int?>
和List<Int>?
区别
List<Int?>
:list本身不能为空,它有持有的元素可以为空List<Int>?
:list集合可能是空引用,而不是list的实例,而它持有的元素不能为空
filterNotNull
函数过滤元素可空集合中的空元素,得到一个持有非空元素的集合
val result = ArrayList<Int?>()
//filterResult 是 ArrayList<Int>类型的
val filterResult.filterNotNull()
6.3.2 只读和可变集合
Kotlin中的集合设计和Java一个重要的不同的特点是:它把获取数据和修改数据的接口分来。
Collection
接口定义了集合获取元素等的基本操作,但是不能添加或者删除元素。如果需要添加或者删除元素需要使用MutableCollection接口,这个接口继承了Collection接口,提供了添加、删除和清楚集合的方法。
一般规则是在代码中的任何地方使用只读集合,只要在需要改变集合的地方使用可变集合。就像val和var一样,只读和可变集合接口的分离使代码中数据的操作更容易理解。
有一点需要注意的是,可读集合并不一定是不可改变的,因为可能两个不同类型的集合引用指向同一个集合。所以,只读集合并不是线程安全的,在多线程环境下,要保证代码正确的同步了对数据的访问。
6.3.3 Kotlin集合和Java
每一个Kotlin集合都是对应Java集合接口的一个实例.Kotlin和Java之间不需要转换以及包装类。但是Java集合接口在Kotlin中有两种表现形式,如前所述,一种是只读的,一种是可变的。
上面的接口都是定义在Kotlin中的,Kotlin中只读和可变集合接口的基本体系和Java的java.util包中的集合接口是平行的,每个可变集合接口都继承自其对应的只读接口。可变集合接口直接对应于java.util包中的接口,而只读接口缺少所有的转换操作方法。
ArrayList和HashSet是Java中的标准类,Kotlin中将他们看作继承自MutableList和MutableSet。这里只列出了两个Java标准类,其他没列出的Java集合的实现类也是类似的。这样,兼容性以及只读和可变集合都得到了保证。Map在Kotlin中也是分为Map和MutableMap两个版本。
集合类型 | 只读集合 | 可变集合 |
---|---|---|
List | listOf() | arrayListOf() |
Set | setOf() | hashSetOf(), linkedSetOf(), sortedSetOf() |
Map | mapOf() | hashMapOf(), linkedMapOf(), sortedMapOf() |
以上是创建集合的方法
调用Java方法传递集合时,可以直接传递不用做任何其他的处理
6.3.4 集合作为平台类型
如前面所看到的那样,Kotlin将在Java中定义的类型看作是平台类型
。Java中定义的集合类型的变量在Kotlin中也是被看做是平台类型的,不同的是这种集合的平台类型本质上是缺少可变性
信息的---Kotlin将它看作是只读或者是可变的。这一般不会有什么问题,因为你想执行的操作都会顺利执行。
但是当需要重写含有集合类型的签名的Java方法时,这点不同就变得重要了。这种情况下,作为平台类型的可空性,这就需要你决定使用Kotlin中哪种类型用来表示你复写的Java方法中集合类型。这种情况下,需要做多种选择,这些选择都会反映在Kotlin的参数类型中:
- 集合是可空的嘛?
- 集合中元素是可空的嘛?
- 你的方法会修改集合嘛?
要做出正确的选择,我们要充分理解我们的实现需要实现什么要的功能
6.3.5 对象数组和基本类型数组
Java中main函数的签名中就是一个数组,如下
fun main(args: Array<String>) {
}
我们可以发现,Kotlin中的数组就是一个有类型参数的类,数组元素的类型由类型参数决定。以下几种方式都可以在Kotlin中创建一个数组:
-
arrayOf()
函数创建一个包含在函数列表中列出元素的数组 -
arrayOfNulls()
函数创建一个给定大小的包含空元素的数组 -
Array()
构造器需要传入一个数组大小和一个lambda函数,通过这个lambda函数初始化数组中的每一个元素
//使用Array()定义一个包含26个英文字母的数组
val letters = Array(26) { i ->
('a' + i).toString()
}
println(letters.joinToString(" "))
>>a b c d e f g h i j k l m n o p q r s t u v w x y z
当我们定义Array<Int>
类型的数组时,他其实是一个int包装类型Integer的数组。当我们需要定义基本数据类型的数组时,需要使用专门的函数:IntArray
,ByteArray
,CharArray
,BooleanArray
等等。要创建基本数据类型的数组时,有以下几种选择:
- 向构造器中传入数组大小,返回一个对应其基本数据类型默认值的数组
- 使用工厂函数(intArrayOf或者其他),创建一个包含传入参数的数组
- 向构造器中传入数组大小和一个初始化数组元素的lambda
//定义基本数据类型的数组
val nums = IntArray(5)
val apples = intArrayOf(0, 0, 0)
val studentNos = IntArray(10) { i ->
i * 2
}
如果有一个包装类型的集合或者数组可以调用toIntArray()
以及其他函数将其转换为基本数据类型的数组。Kotlin标准库对数组定义了和集合同一套扩展函数,例如filter,map等都可以用在数组上,但要注意的是这些函数操作后返回的是集合而不是数组