空指针
Android上奔溃率最高的异常类型就是 空指针异常 —— NullPointerException
可空类型系统
Kotlin 利用编译时判空检查的机制几乎杜绝了空指针异常。
Kotlin 提供了一系列辅助工具,让我们能轻松地处理各种判空情况。
拿之前的代码举例了一下:
fun doStudy(study: Study) {
study.doHomework()
study.readBook()
}
如果直接运行该代码:
fun testNullPointer(){
//直接传入空值
doStudy(null)
↑
| Null can not be a value of a non-null type Study |
}
可以看到,当我们传入 null 时编译器已经报异常了!
因为Kotlin默认所有的参数和变量都不可为空,所以这里传入的Study参数一定不为空。
由于Kotlin将空指针异常的检查提前到了编译时期,像这样直接传null就会报错,当我们修正后才能正常运行。
那么有的同学可能会问,我就TM要传 null 值 该怎么做?
Kotlin为我们提供了一套 [ 可为空的类型系统 ] 只不过在使用 [ 可为空的类型系统 ] 时,我们需要在编译时期就将所有潜在的空指针异常都处理掉,否则代码将无法编译通过。
什么是 [ 可为空的类型系统 ] ?
就是在类名的后面加上一个问号。
比如:
//表示不可为空的整型
Int
//表示可为空的整型
Int?
//表示不可为空的字符串
String
//表示可为空的字符串
String?
再回到上面的 doStudy()
我们将在Study后面加个 ?
fun doStudy(study: Study?) {
study.doHomework()
study.readBook()
}
可以看到 调用该方法时不会报错了
fun testNullPointer(){
//直接传入空值
doStudy(null)
}
不过啊,doStudy里面又报错了。因为study可能是空的啊,调用doHomework()就可能导致异常。
我们还需要 处理一下里面
比如:
fun doStudy(study: Study?) {
if (study!=null){
study.doHomework()
study.readBook()
}
}
可以看到,我们加了个判断,如果为 null 就不执行了。这样也就不会报异常了。
不过这样写看起来很麻烦,如果有大量或者全局的判空 这样用起来就会很捉急。
接下来 将展示 Kotlin 提供的一系列辅助工具 这些判空将会很简单。
判空辅助工具
- ?. 操作符(注意: 问号后面还有个点)
当对象不为空时正常调用,当对象为空时就什么也不做。
上面的study就可以简化为:
//原始
if (study!=null){
study.doHomework()
study.readBook()
}
//简化后
study?.doHomework()
study?.readBook()
感觉是真TM的方便!
- ?: 操作符(注意: 问号后面还有个冒号)
操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。
听着描述有点像三目运算符的感觉不。
我这里还是用study举例吧,好理解:
fun doStudy3(study: Study?) {
val a = study
val b = "something"
//原始
val c = if (a !=null){
a
}else{
b
}
//简化后
val c = a ?: b
}
解释下:当 a 不为空时 c 就等于 a ,若 a 为空时 那么 c 就等于 b 。
- ?. 和 ?: 结合在一起举例
//传统写法
fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}
//简化后
fun getTextLen(text: String?): Int {
return text?.length ?: 0
}
//再简
fun getTL(text: String?) = text?.length ?: 0
这波啊,这波是我没学熟练,所以不能一次变到位。
这里:首先由于text是可能为空的,因此我们在调用它的length字段时需要使用 ?. 操作符,而当text为空时,text?.length会返回一个null值,这时我们再借助 ?: 操作符让它返回0。
- !! 非空断言(两个感叹号)
虽然Kotlin写起来很方便,不过有时候还是显得不那么聪明。
有种情况,虽然我们已经将空指针异常处理了,但是Kotlin编译器还是不让我们通过。
举例:
var content: String? = "hello"
fun runUnAI() {
if (content != null) {
UpCase()
}
}
fun UpCase() {
val upContent = content.toUpperCase()
print("大写后:$upContent")
}
编译器已经报错了,并且是运行不起来的。
可以看到,在 runUnAI()方法 中 我们已经做了content的判空,但是在UpCase()中,它并不知道。所以在UpCase里调用toUpperCase编译器还是会认为这是有风险的,从而不让编译通过。
那应该怎么办呢?非空断言工具,在对象后面直接加 !!
fun UpCase() {
val upContent = content!!.toUpperCase()
print("大写后:$upContent")
}
可以看到,编译器不再报错了。
这就是等于告诉 Kotlin编译器,我已经做了处理了,你不用管了。如果出错,直接抛异常即可。
我的代码,我嗦了蒜!
- let 函数
它提供了函数式API的编程接口,并将原始调用对象作为参数传递到Lambda表达式中
fun letTest() {
val obj = 3
obj.let {
//你的逻辑
doStudy(Student("",0))
println("fuck this is let")
println("let:${it + 2}")
}
}
可以看到,这里调用了obj对象的let函数,然后Lambda表达式中的代码就会立即执行,并且这个obj对象本身还会作为参数传递到Lambda表达式中。
这里实际写法 和 郭神 讲的有点不太一样。
不过这都不是重点,重点是:
let函数 属于 Kotlin中的 标准函数,这个let函数的特性配合 ?. 操作符使用:
回到doStudy
fun doStudy2(study: Study?) {
study?.doHomework()
study?.readBook()
}
这种写法虽然有一定的简化,但其实这种表达方式有点啰嗦,这种写法本质是:
fun doStudy2(study: Study?) {
if (study != null) {
study.doHomework()
}
if (study != null) {
study.readBook()
}
}
可以看到 等于 调用了两次判空。
如果我们用 let 和 ?. 结合起来
fun doStudyPlus(study: Study?) {
study?.let {
it.readBook()
it.doHomework()
}
}
就变成了这样: ?.操作符 表示对象不为空调用 let函数,而 let函数 会将 Study对象本身作为参数传递到Lambda表达式中,此时Study肯定不为空,就可以随便调用了。
- let对全局变量的处理
let函数 是可以处理 全局变量 的判空问题的,而if判断语句则无法做到这一点。
如果我们将doStudy()函数中的参数变成一个全局变量,使用let函数任然正常工作,但使用if判断语句 就会报错。
var study:Study? =null//注意这里是变量
fun doStudyPlusLet() {
study?.let {
//这里正常运行
it.readBook()
it.doHomework()
}
}
fun doStudyPlusIf() {
if (study!=null){
//这里会报错
study.readBook()
study.doHomework()
}
}
为什么呢?这是因为:
全局变量的值随时都有可能被其他线程所修改,即使做了判空处理,仍然无法保证if语句中的Study变量没有空指针风险。
Kotlin中的小魔术(小技巧)
字符串内嵌表达式
在Kotlin里,我们不需要像Java里拼接字符串了,而是可以直接将表达式写在字符串里:
fun magicTest() {
val student = Student("WaHaa", 10)
println("fuck u , my dear ${student.name} !")
}
可以看到,Kotlin允许我们在字符串里嵌入 ${} 这种语法结构的表达式,并在运行时使用表达式执行的结果替代这部分内容。
另外,当表达式中仅有一个变量时,还可以将两边的大括号省略:
fun magicTest0() {
val str = "Don't take yourself humiliation"
println("fuck u , $str !")
}
函数的参数默认值
在前边我们学 次构造函数时说过,次构造函数 在Kotlin中很少用。因为Kotlin提供了函数设定参数默认值的功能,他在很大程度上能替代次构造函数的作用。
我们可以再定义函数的时候给任意参数设定一个默认值,这样当调用此函数时就不会强制要求 调用方 为此函数传值,在没有传值的情况下会自动使用参数的默认值。
fun printParams(num: Int, str: String = "hello") {
println("num is $num , str is $str")
}
可以看到,这里我们给printParams()函数的第二个参数设定了一个默认值,这样当调用printParams()函数时,可以选择给第二个参数传值,也可以选择不传,在不传的情况下就会自动使用默认值
fun printParTest() {
printParams(100, "ddd")
printParams(101)
}
可以看到,在没有给第二个参数传值的情况下,printParams()函数 自动使用了参数的默认值。
郭神又将骚东西了,将代码的第一个参数设置默认值
fun printParams1(num: Int = 102, str: String) {
println("num is $num , str is $str")
}
这时如果想让num参数使用默认值该怎么办呢?模仿刚才的写法肯定是行不通的,因为编译器会认为我们想把字符串赋值给第一个 num 参数,从而报类型不匹配的错误:
fun printParTest1() {
printParams1(100, "ddd")
printParams1("ddd")
}
不过 不必担心,Kotlin提供了另外一套机制
通过键值对的方式传值
稍加改造
fun printParTest1() {
printParams1(102 ,"ddd")
printParams1(num = 103,str = "ddd")
printParams1(str = "ddd")
}
可以看到,此时编译器不再报错。并且运行正常。
所以说 这种写法 可以在很大程度上替代次构造函数的作用。只需给个默认值即可。
前边学的Student就可以改造成
//之前
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
constructor(name: String, age: Int) : this("", 0, "", 0) {
}
constructor() : this("dd", 0) {
}
}
//改造后
class Students(val sno:String = "", val grade:Int = 0,name:String = "",age:Int=0):Person(name,age){
}