一、可空性
可空性是Kotlin系统中帮助避免NullPointerException错误的特性。
1)可空类型
问号加在任何类型的后面来表示这个类型的变量可以存储null引用,如String?、Int?。没有问号的类型表示这种类型的变量不能存储null引用。
2)安全调用运算符:“?.”
安全调用运算符“?.”允许把一次null检查和一次方法调用合并成一个操作。
3)Elvis运算符:“?:”
Elvis运算符可以用一个值代替对null对象调用方法时返回的null。
4)安全转换:“as?”
as?运算符尝试把值转换成指定类型,如果值不是合适的类型就返回null。
5)非空断言:“!!”
非空断言可以把任何值转换成非空类型。如果对null值做非空断言,则会抛出异常。
6)延迟初始化的属性
Kotlin通常要求在构造方法中初始化所有属性,使用lateinit可以实现延迟初始化,如
class MyService {
fun performAction(): String = "foo"
}
class MyTest {
private lateinit var myService: MyService
@Before fun setUp() {
myService = MyService()
}
@Test fun testAction () {
Assert.assertEquals( "foo", myService.performAction())
}
}
通过lateinit定义了myService变量,该变量在setUp中初始化,在testAction中时不需要null检查。延迟初始化的属性都是var。
7)可空类性的扩展
为可空类型定义扩展函数是一种更强大的处理null值的方式。可以允许接收者为null的扩展函数的调用,并在该函数中处理null,而不是在确保变量不为null之后再调用它的方法。只有扩展函数才能做到这一点,普通成员方法的调用是通过对象实例来分发的,因此实例为null时,成员方法永远不能被执行。如系统函数isEmptyOrNull和isNullOrBlank,如
fun verifyUserInput(input: String?) {
if (input.isNullOrBlank()) {
}
}
函数isNullOrBlank显示地检查了null,这种情况下返回true,然后调用isBlank,它只能在非空String上调用
fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()
在Java中,this永远是非空的,因为它引用的是当前你所在这个类的实例。在Kotlin中,这并不永远成立,在可空类型的扩展函数中,this可以为null。
8)类型参数的可空性
Kotlin中所有泛型类和泛型函数的类型参数默认都是可空的,使用类型参数作为类型的声明都允许为null,尽管类型参数T并没有用问号结尾。
fun <T> printHashCode(t: T) {
println(t?.hashCode())
}
在printHashCode调用中,类型参数T推导出的类型是可空类型Any?。因此尽管没有用问号结尾,实参t依然允许持有null。要使类型参数非空,必须要为它指定一个非空的上界,那样泛型会拒绝可空值作为实参。如下
fun <T: Any> printHashCode(t: T) {
println(t.hashCode())
}
9)Kotlin和Java互操作
Java中@Nullable String被Kotlin当作String?,@NotNull String在Kotlin中为String。当类似这种注解不存在时,Java类型会变成Kotlin中的平台类型。
平台类型本质上就是Kotlin不知道可空性信息的类型。既可以把它当作可空类型处理,也可以当作非空类型处理。编译器将会允许所有的操作,在编码时需要我们自己对这个类型上做的操作负有全部责任,在实现Java类或者接口的方法时一定要搞清楚它的可空性。
在Kotlin中不能声明一个平台类型的变量,这些类型只能来自Java代码。
二、基本数据类型和其他基本类型
Kotlin并不区分基本数据类型和它们的包装类。
1)基本数据类型:Int、Boolean及其他
基本数据类型的值能够更高效地存储和传递,但是你不能对这些值调用方法,或是把它们存放在集合中。
Kotlin并不区分基本数据类型和包装类型,你使用的永远是同一个类型(如Int):
val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)
还能对一个数字类型的值调用方法,如下,使用标准库的函数coerceIn来把值限制在特定范围内:
fun showProgress(progress: Int) {
val percent = progress.coerceIn(0, 100)
}
大多数情况下对于变量、属性、参数和返回类型,Kotlin的Int类型会被编译成Java基本数据类型int。唯一不可行的例外是泛型类,比如集合。用作泛型类型参数的基本数据类型会被编译成对应的Java包装类型。
在Kotlin中使用Java声明时,Java基本数据类型会变成非空类型(而不是平台类型),因为它们不能持有null值。
2)可空的基本数据类型:Int?、Boolean?及其他
Kotlin中的可空类型不能用Java的基本数据类型表示,只要使用了基本数据类型的可空版本,就会编译成对应的包装类型。
当基本类型可能为null以及作为泛型类的类型参数时,Kotlin会使用该类型的包装形式。
3)数字转换
Kotlin不会自动地把数字从一种类型转换成另外一种,即便是转换成范围更大的类型。每一种基本数据类型(Boolean除外)都定义有转换函数:toByte()、toShort()、toChar()等。这些函数支持双向转换:既可以把小范围的类型转换到大范围,比如Int.toLong(),也可以把大范围的类型截取到小范围,比如Long.toInt()。
比较两个装箱值的equals方法不仅会检查它们存储的值,还要比较装箱类型。
使用数字字面值去初始化一个类型已知的变量时,又或者是把字面值作为实参传给函数时,转换会自动地发生。
4)“Any”和“Any?”:根类型
Any类型是Kotlin所有非空类型的超类型,包括像Int这样的基本数据类型。
把基本数据类型的值赋给Any类型的变量时会自动装箱,如下
val answer: Any = 42
Any是引用类型,所以值42会被装箱。
Any是非空类型,可空类型用Any?。所有Kotlin类都包含下面三个方法:toString、equals和hashCode,这些方法都继承自Any。Any并不能使用其他java.lang.Object的方法(比如wait和notify),但是可以通过手动把值转换成java.lang.Object来调用这些方法。
5)Unit类型:Kotlin的“void”
Kotlin中的Unit类型完成了Java中的void一样的功能。当函数没什么有意思的结果要返回时,它可以用作函数的返回类型。Unit是一个完备的类型,可以作为类型参数,而void不行。
Java中为了解决使用“没有值”作为类型参数的解法可能有如下两种:1)使用分开的接口定义来分别表示需要和不需要返回值的接口(如Callable和Runnable);2)用特殊的java.lang.Void类型作为类型参数。在Kotlin中使用Unit类型就很方便解决。
6)Nothing类型:“这个函数永不返回”
使用Nothing类型作为返回类型可以表示函数永远不会正常终止。如
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
Nothing类型没有任何值,只有被当作函数返回值使用,或者被当作泛型函数返回值的类型参数使用才有意义。在其他所有情况下,声明一个不能存储任何值的变量没有任何意义。
返回Nothing的函数可以放在Elvis运算符的右边来做先决条件检查:
val address = company.address ?: fail("No address")
这种返回类型的函数从不正常终止,在分析调用这个函数的代码时利用这个信息。在上面这个例子中,编译器会把address的类型推断成非空,因为它为null时的分支处理会始终抛出异常。
三、集合与数组
1)可空性和集合
可空集合包括变量自己类型的可空性和用作类型参数的类型的可空性集合,举例如List?及List<Int?>。可空列表包含可空数字如List<Int?>?。
2)只读集合与可变集合
Kotlin把访问集合数据的接口和修改集合数据的接口分开了。kotlin.collections.Collection可以遍历集合元素、获取集合大小、判断集合中是否包含某个元素,从该集合读取数据,但这个接口不能添加或移除元素;kotlin.collections.MutableCollection接口可以修改集合中的数据,添加和移除元素、清空集合等。只读集合接口与可变集合接口的分离能让程序中的数据发生的事情更容易理解。
只读集合不一定是不可变的。如果你使用的变量拥有一个只读接口类型,它可能只是同一个集合的众多引用中的一个。任何其他的引用都可能拥有一个可变接口类型,如图
两个不同的引用,一个只读,另一个可变,指向同一个集合对象。如果你调用了这样的代码,它持有其他指向你集合的引用,或者并行地运行了这样的代码,有可能你正在使用集合的时候它被其他代码修改了,这样会导致concurrentModificationException错误和其他一些问题,因此只读集合并不总是线程安全的。如果在多线程环境下处理数据,需要保证代码正确地同步了对数据的访问,或者使用支持并发访问的数据结构。
3)Kotlin集合和Java
每一种Java集合接口在Kotlin中都有两种表示:一种只读,另一种可变。下表为用来创建不同类型集合的函数:
即使Kotlin中把集合声明成只读的,Java代码也能够修改这个集合。Kotlin编译器不能完全地分析Java代码到底对集合做了什么。如果你写了一个Kotlin函数,使用了集合并传递给了Java,你有责任使用正确的参数类型,这取决于你调用的Java代码是否会修改集合。
4)作为平台类型的集合
在Kotlin中重写或实现签名中有集合类型的Java方法时需要注意以下几点:
a) 集合是否可空?
b) 集合中的元素是否可空?
c) 你的方法会不会修改集合?
5)对象和基本数据类型的数组
如下使用array.indices在小标范围迭代,通过下标使用array[index]访问元素,
fun main(args: Array<String>) {
for (i in args.indices) {
println("Argument $i is: ${args[i]}")
}
}
在Kotlin中创建数组,有下面这些方法供你选择:
a) arrayOf函数创建一个数组,它包含的元素是指定为该函数的实参
b) arrayOfNulls创建一个给定大小的数组,包含null元素。它只能用来创建包含元素类型可空的数组。
c) Array构造方法接收数组的大小和一个lambda表达式,调用lambda表达式来创建每一个元素。
如下展示用Array函数来创建从"a"到"z"的字符串数组,
val letters = Array<String>(26) { i -> ('a' + i).toString() }
letters.joinToString("")
集合中的数据转换为数组,使用toTypedArray,*为展开运算符,如
val strings = listOf("a", "b", "c")
println("%s/%s/%s".format(*strings.toTypedArray()))
数组类型的类型参数始终会变成对象类型,为了表示基本数据类型,可以使用如IntArray、ByteArray、CharArray、BooleanArray等类型,所有这些类型都被编译成普通的Java基本数据类型数组,比如int[]、byte[]、char[]等。因此这些数组中的值存储时并没有装箱。
创建基本数据类型的数组,有如下方式:
a) 该类型的构造方法接收size参数并返回一个使用对应基本数据类型默认值(通常是0)初始化好的数组。
b) 工厂函数(IntArray的intArrayOf,以及其他数组类型的函数)接收变长参数的值并创建存储这些值的数组。
c)另一种构造方法,接收一个大小和一个用来初始化每个元素的lambda。
如下三种方式:
//方式一
val fiveZeros = IntArray(5)
//方式二
val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0)
//方式三
val squares = IntArray(5) { i -> (i+1) * (i+1) }
squares.joinToString()