Kotlin自学之旅(八)类型系统

可空类型

Kotlin 和 Java 的类型系统之间第一条也可能是最重要的一条区别是, Kotlin 对可空类型的显式的支持。这是一种指出你的程序中哪些变量和属性允许为 null 的方式。如果一个变量可以为 null ,对变量的方法的调用就是不安全的,因为这样会导致 NullPointerException 。
Kotlin使用在类型名称后面加问号的方式来表示这个类型的变量可以储存null引用,String?、 Int?、 MyCustomType?,等等,而没有问号的类型表示这种类型的变量不能存储null引用,这也说明所有常见类型默认都是非空的,除非显式地把它标记为可空。一旦你有一个可空类型的值,你就不能再调用它的方法,也不能把它赋值给非空类型的变量:

//Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
fun strLenSafe(s: String?) = s.length


val x: String? = null
var y: String = x //Type mismatch. Required: String. Found: String?

但是如果你对一个可空变量与null进行了比较操作,那么在这次比较发生的作用域里,你可以把这个变量当作非空变量使用:

fun strLenSafe(s: String?): Int  {
    //return  s.length //报错
    if (s != null) {
        return s.length //OK
    }
    return 0
}

处理可空类型

事实上,Kotlin 有比if-else更好的方法来处理可空变量,比如安全调用运算符:?. ,这个运算符将一次null检查和一次方法调用合并成一个操作,举个例子,以下两个语句是等价的:

var result1 = s?.toUpperCase()
var result2 = if (s == null) null else s.toUpperCase()

也就是说,如果这个使用 ?. 运算符可空变量不为空,这个方法会正常的执行,否则这次调用不会发生,而整个表达式的值是null。此外,要注意,这个调用的结果类型也是可空的,因为它可能为 null。
?. 会在调用变量为null的时候返回null,但有些时候,我们更想在变量为null的时候返回一个默认值,比如开始的例子,我们会在字符串s为null的时候把它当作长度为0的字符串 “”。这个时候,我们可以使用另一个null合并运算符: ?: ,我们可以使用它把strLenSafe改造得简单一点:

fun strLenSafe(s: String?): Int  {
    val result = s?:""
    return  result.length
}

?:运算符接收两个参数,如果第一个参数不为null,就返回第一个参数,否则返回第二个参数。
我们还可以把这两个运算符一起使用:

fun strLenSafe(s: String?): Int  {
    return  s?.length?:0
}

上面这个表达式 先使用 ?. 运算符得到一个Int?类型的返回值,然后再使用 ?: 运算符在返回值为null的时候返回0作为结果。
上面两个运算符用来处理需要安全检查变量是否为空的情况,还有一个常见的情形是把一个变量转换成某个类型,这个时候也有一个安全检查的运算符。
在Kotlin中,有一个和常规Java类型转换一样的 as 运算符,这个运算符的使用方式就像下面这样:

fun castNumberToInt(number: Number):Int {
    return number as Int
}

println(castNumberToInt(15)) //15
println(castNumberToInt(12.3)) //java.lang.ClassCastException

as 运算符在被转换的值不是你试图转换的类型时,就会抛出一个ClassCastException异常。 这显然很是麻烦,如果我们想不抛出异常的话,就要在每次使用 as 之前用 is 检查来确保类型正常。于是这个时候我们就需要用到 as? 了。as?运算符会在被转换的值和转换类型不匹配的时候返回null:

fun castNumberToInt(number: Number):Int? {
    return number as? Int
}

println(castNumberToInt(15))  //15
println(castNumberToInt(12.3)) //null

同样的,我们也可以将 as? 运算符和 ?:运算符一起使用,以在转换失败之后返回一个默认值:

  fun castNumberToInt(number: Number):Int {
    return number as? Int?:-1
 } 

延迟初始化

Kotlin通常要求我们在构造方法中初始化所有属性,如果某个属性是非空类型,我们就必须提供非空的初始化值。否则,就必须使用可空类型,但是可空类型每次使用的时候都必须进行繁琐的null检查,尤其是这个属性要经常时候的时候。为了解决这个问题,我们可以在声明一个不想给初始化值的属性的时候,给它加上 lateinit 修饰符:

class Person() {
    //var name:String    //error:Property must be initialized or be abstract
    lateinit var name:String
    //lateinit var age:Int //error:'ateinit' modifier is not allowed on properties of primitive types
    var age:Int = -1
}

var xiaoming = Person()
println(xiaoming.name) //error:lateinit property name has not been initialized

延迟初始化的属性都是 var,因为需要在构造方法外修改它的值 , 而 val 属性会被编译成必须在构造方法中初始化的 final 字段 。如果在属性被初始化之前就访问了它,会得到这个异常 “lateinit property name has not been initialized” 。 该异常清楚地说明了发生了什么。
此外还要注意的是,Int这样的基本类型是不能延迟初始化的,因为事实上Kotlin会使用null来对每一个用lateinit修饰的属性做初始化,而基础类型是没有null类型的,所以像我上面那样对一个基本类型使用 lateinit 的时候就会报错。


数据类型

众所周知, Java 把基本数据类型和引用类型做了区分。一个基本数据类型(如int)的变量直接存储了它的值,而一个引用类型(如 String )的变量存储的是指向包含该对象的内存地址的引用。基本数据类型的值能够更高效地存储和传递,但是你不能对这些值调用方法,或是把它们存放在集合中。为此Java提供了包装类型。
而Kotlin则并不区分基本数据类型和包装类型,对于Int之类的基本类型来说,我们使用的总是同一类型,你可以对把它保存在集合里,也可以对它调用函数:

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

基本类型

虽然看起来Kotlin是在使用对象来表示所有的数字,但事实上 ,在运行时,数字类型会尽量使用高效的方式表示,对于变量、属性、参数和返回类型——Kotlin 的 Int 类型会被编译成 Java 基本数据类型 int 。 用作泛型类型参数的基本数据类型会被编译成对应的 Java 包装类型 。
像 Int 这样的 Kotlin 类型在底层可以轻易地编译成对应的 Java 基本数据类型,因为两种类型都不能存储 null 引用。而对于可空的基本数据类型,因为 null 只能被存储在 Java 的引用类型的变量中。这意味着任何时候只要使用了基本数据类型的可空版本,它就会编译成对应的包装类型。


数字转换

Kotlin 和 Java 之间一条重要的区别就是处理数字转换的方式 。 Kotlin 不会自动地把数字从一种类型转换成另外一种,即便是转换成范围更大的类型。例如,Kotlin中下面这段代码不会编译,而必须使用显示转换:

val i:Int = 1
//val l: Long = i  //Type mismatch.
val l: Long = i.toLong()

每一种基本数据类型( Boolean 除外)都定义有转换函数:toByte()、toShort()、toChar()等。这些函数支持双向转换:既可以把小范围的类型括展到大范围, 比如 Int.toLong(),也可以把大范围的类型截取到小范围,比如Long.toInt()。
为了避免意外情况 ,Kotlin 要求转换必须是显式的,尤其是在比较装箱值的时候。比较两个装箱值的 equals 方法不仅会检查它们存储的值,还要比较装箱类型,因为Kotiin 要求只有类型相同的值才能比较。


根类型和空类型

和 Object 作为 Java 类层级结构的根差不多, Any 类型是 Kotlin 所有非空类型的超类型(非空类型的根 )。但是在 Java 中, Object 只是所有引用类型的超类型(引用类型的根),而基本数据类型并不是类层级结构的一部分。这意味着当你需Object的时候,不得不使用像 java.lang.Integer 这样的包装类型来表示基本数据型的值。而在 Kotlin 中, Any 是所有类型的超类型(所有类型的根),包括像 Int 这样的基本数据类型 。
Any 是非空类型 ,所以 Any 类型的变量不可以持有 null 值 。 在 Kotlin中如果你需要可以持有任何可能值的变量,包括 null 在内,必须使用 Any?类型 。
而Kotlin 中的 Unit 类型完成了 Java 中的 void 一样的功能。当函数没什么有意义的结果要返回时,可以使用它作为函数的返回类型。Unit和void的差别在于Unit是一个完备的类型,可以作为类型参数,而void却不行。只存在一个值是Unit类型,这个值也叫作 Unit,并且(在函数中)会被隐式地返回。
对某些 Kotlin 函数来说,“返回类型”的概念没有任何意义,这时候知道函数永远不会正常终止是很有帮助的。Kotiin使用一种特殊的返回类型Nothing来表示。Nothing类型没有任何值,只有被当作函数返回值使用,或者被当作泛型函数返回值的类型参数使用才会有意义。在其他所有情况下,声明一个不能存储任何值的变量没有任何意义 。


集合

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


集合的可空性

Kotlin完全支持类型参数的可空性。 就像变量的类型可以加上 ?字符来表示变量科二以持有 null 一样,类型在被当作类型参数时也可以用同样的方式标记:

 val list = ArrayList<Int?>()

注意,变量自己类型的可空性和用作类型参数的类型的可空性是有区别的。在第一种情况下 , 列表本身始终不为 null ,但列表中的每个值都可以为null 。 第二种类型 的变量可能包含空引用而不是列表实例,但列表中的元素保证是非空的 。


只读集合和可变集合

Kotlin 的 集合设计和 Java 不同的一项重要特质是,它把访问集合数据的接口和修改集合数据的接口分开了 。 这种区别存在于最基础的使用集合的接口之中 :kotlin .collections.Collection。使用这个接口,可以遍历集合中的元素、获取集合大小、判断集合中是否包含某个元素,以及执行其他从该集合中读取数据的操作。但这个接口没有任何添加或移除元素的方法 。而使用 kotlin.collections.MutableCollection 接口可以修改集合中的数据。它继承了普通的 kotlin.collections.Collection接口,还提供了方法来添加和移除元素、清空集合等。
但事实上,使用集合接口时需要牢记的一个关键点是只读集合不一定是不可变的。如果你使用的变量拥有一个只读接口类型,它可能只是同一个集合的众多引用中的一个。任何其他的引用都可能拥有一个可变接口类型,如果你正在使用集合的时候它被其他代码修改了,这会导致 concurrentModificationException 错误和其他一些问题。因此,必须了解只读集合并不总是线程安全的。


数组

Kotiin中的一个数组是一个带有类型参数的类,其元素类型被指定为相应的类型参数。要在 Kotlin 中创建数组,可以选择下面的方式:

  • arrayOf 函数创建一个数组,它包含的元素是指定为该函数的实参
  • arrayOfNulls 创建一个给定大小的数组,包含的是 null 元素。当然,它只能用来创建包含元素类型可空的数组。
  • Array 构造方法接收数组的大小和一个 lambda 表达式,调用 lambda 表达式
    来创建每一个数组元素 。

和其他类型一样,数组类型的类型参数始终会变成对象类型.因此如果你声明了 一个Array< Int>,它将会是一个包含装箱整型的数组(它的 Java 类型将是java.lang . Integer[])。
为了表示基本数据类型的数组, Kotlin 提供了若干独立的类,每一种基本数据类型都对应一个。例如,Int 类型值的数组叫作IntArray。Kotlin 还提供了ByteArray、CharArray、BooleanArray等给其他类型。所有这些类型都被编译成普通的Java基本数据类型数组,比如 int[]、byte[]、char[]等。因此这些数组中的值存储时并没有装箱,而是使用了可能的最高效的方式。


总结

Kotlin对可空类型的支持,可以帮助我们在编译期,检测出潜在的NullPointerException错误。同时提供了像安全调用(?.)、Elvis 运算符(?:)这样的工具来简洁地处理可空类型。Kotlin使用标准 Java 集合类,并通过区分只读和可变集合来增强它们 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值