Kotlin学习(6)Kotlin的类型系统

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运算符示意图

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函数的可空对象转换为非空类型的对象。

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基本数据类型的类型完整列表如下:

  • 整数类型:ByteShortIntLong
  • 浮点类型:FloatDouble
  • 字符类型: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接口,提供了添加、删除和清楚集合的方法。

微信截图_20180122142632.png

​ 一般规则是在代码中的任何地方使用只读集合,只要在需要改变集合的地方使用可变集合。就像val和var一样,只读和可变集合接口的分离使代码中数据的操作更容易理解。

​ 有一点需要注意的是,可读集合并不一定是不可改变的,因为可能两个不同类型的集合引用指向同一个集合。所以,只读集合并不是线程安全的,在多线程环境下,要保证代码正确的同步了对数据的访问。

6.3.3 Kotlin集合和Java

​ 每一个Kotlin集合都是对应Java集合接口的一个实例.Kotlin和Java之间不需要转换以及包装类。但是Java集合接口在Kotlin中有两种表现形式,如前所述,一种是只读的,一种是可变的。

微信截图_20180122160946.png

​ 上面的接口都是定义在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的数组。当我们需要定义基本数据类型的数组时,需要使用专门的函数:IntArrayByteArrayCharArrayBooleanArray等等。要创建基本数据类型的数组时,有以下几种选择

  • 向构造器中传入数组大小,返回一个对应其基本数据类型默认值的数组
  • 使用工厂函数(intArrayOf或者其他),创建一个包含传入参数的数组
  • 向构造器中传入数组大小和一个初始化数组元素的lambda
//定义基本数据类型的数组
val nums = IntArray(5)
val apples = intArrayOf(0, 0, 0)
val studentNos = IntArray(10) { i ->
    i * 2
}

​ 如果有一个包装类型的集合或者数组可以调用toIntArray()以及其他函数将其转换为基本数据类型的数组。Kotlin标准库对数组定义了和集合同一套扩展函数,例如filter,map等都可以用在数组上,但要注意的是这些函数操作后返回的是集合而不是数组

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容