Kotlin系列之可空类型的处理

在前面的文章中,咱们已经看到了kotlin为了解决NPE问题做出的一些努力。这篇文章咱们继续学习kotlin中与可空类型处理相关的一些知识。java

非空断言

在程序的编写过程当中有这样一种场景,咱们已经在前一个函数中对一个可空类型的变量进行了检查,以后咱们在接下来的函数中使用这个变量,咱们其实已经很明确地知道这个变量前面已经进行了判空处理,后续不可能为空,可是编译器没法清楚地推测出来,这时候在编译器眼里这个变量仍是有可能为空,这就形成咱们的代码中出现了不少无用的判空代码。安全

这种状况下咱们就可使用非断言“!!”。咱们在一个可空的变量后面加上两个感叹号,用来明确告诉编译器咱们肯定这个变量是非空的。这样咱们就简单粗暴地实现了将一个可空的变量转换为一个非空变量。bash

以下面的代码所示:ide

fun testNull(str: String?){
    val notNullStr: String = str!!
    println(notNullStr.length)
}
复制代码

上面这种状况下至关于咱们强制进行了转化,因此非空就由咱们本身来保证,一旦传入了空类型,程序就会抛出异常。好比下面的异常:函数

Exception in thread "main" kotlin.KotlinNullPointerException
	at MainKt.testNull(Main.kt:6)
	at MainKt.main(Main.kt:2)
复制代码

let函数

有时候,咱们会有这样的需求,咱们有一个可空类型的变量,可是咱们要将其传入一个要求是非空参数的函数中,那咱们必须在传递以前作非空判断,不然编译器不容许咱们直接传入,就像下面这样:学习

fun sendMsg(msg: String){
    //xxxxx
}

fun main(args: Array<String>) {
    val msg: String? = "something"
    
    if (msg != null) sendMsg(msg)
}
复制代码

针对上面这种很常见的状况,kotlin为咱们提供了一个let函数,这个函数能够将调用它的参数转化为lambda表达式中的参数。就像下面这样:ui

msg.let {
    println(it)
}
复制代码

上面lambda表达式中的it其实就是msg变量,结合上一节介绍过的安全调用运算符,咱们能够这样写:this

msg?.let{
    println(it.length)
}
复制代码

上面的let函数在msg为空时不会发生调用,当msg不为空时,传递到let函数的lambda表达式中的it变量天然就变成了非空变量,这样就完美且优雅地将一个可空类型的变量转化为了非空类型的变量。spa

延迟初始化属性

在kotlin中,你必须在构造函数中初始化全部非空类型的属性,不然就会报错,就像下面这样:code

class Main{
    private var mText: String
    
    constructor(){
        mText = ""
    }

    fun showLen(){
        println(mText.length)
    }
}
复制代码

可是有时候咱们但愿将属性的初始化放到本身的初始化函数中去完成,那这样咱们就与kotlin中的规定冲突了,那咱们就必须将属性声明为可空属性,可是那样,又致使咱们在调用这个属性的方法时必须使用安全调用符或者是“!!”将可空类型转化为非空类型,那样的代码都不够简洁优雅,就像下面这样:

class Main{
    private var mText: String? = null

    fun initArgs(){
        mText = "xxxxx"
    }
    
    fun showLen(){
        println(mText?.length)
    }
}
复制代码

对于这种咱们想本身掌握属性的初始化,同时又想将属性声明为非空类型的状况,kotlin提供了延迟初始化属性,使用“lateinit”修饰符来表示一个延迟初始化的属性,拥有这个修饰符的属性,kotlin不会强制你必须在构造函数中初始化属性,你能够本身在任意的时刻本身掌握属性的初始化。就像下面这样:

class Main{
    private lateinit var mText: String

    fun initArgs(){
        mText = "xxxxx"
    }

    fun showLen(){
        println(mText.length)
    }
}
复制代码

这样的代码就优雅了许多。假如咱们上面属性的初始化时机不对,致使咱们还没初始化这个属性就已经调用了这个属性的一些方法,就会报下面的这个异常:

Exception in thread "main" kotlin.UninitializedPropertyAccessException: 
lateinit property mText has not been initialized
复制代码

异常也对这种状况说明得很是清楚,延迟初始化属性还没初始化便进行了访问。

可空类型的扩展函数

在Java中,调用一个对象的方法,若是这个对象为null,就会发生NPE,而后kotlin中经过安全调用符来解决这个问题,保证了在变量不为null时调用才会发生。可是有时候咱们就须要一些函数能够包括对null的检查,不须要咱们在函数外部进行检查。 咱们先看下面的这段Java代码:

public boolean isEmpty(String str){
    if (str == null){
        return true;
    }

    return str.isEmpty();
}
复制代码

咱们自定义的一个函数,在调用isEmpty()以前,咱们必须本身先判空,处理掉为null这种状况,不然就可能出现NPE。咱们想能够不能够定义一种扩展函数,它能够被null进行调用,而且不会报NPE,这就是kotlin中的可空类型的扩展函数。

注意:kotlin中只有扩展函数是能够针对可空类型的,常规的方法使用null去调用,要么是编译失败,要么就是NPE,咱们下面举个例子,看下面的代码:

fun String?.isEmpty(): Boolean = this == null || this.isBlank()
复制代码

这样一行代码,咱们就为String的可空类型定义了一个isEmpty()这样一个扩展函数,也就是说null也能够调用这个函数。值得注意的是,函数中的this可能为null,第一个this进行了null的处理后,第二个this就成了一个非空类型的变量。而后咱们就能够想下面这样进行调用了:

val nullStr: String? = null
println(nullStr.isEmpty())
复制代码

注意,上面的调用咱们并无使用安全调用运算符,而且代码也没有报NPE,由于它是为String的可空类型的定义的扩展函数,在函数内部包含了对null值的处理。

可空性与Java的统一

虽然kotlin中有可空类型和非空类型,可是在Java中却不是这样划分的,可是,咱们会常常进行Java和kotlin的互调。那在互调时这种状况是怎么处理的呢?

存在注解的状况

在Java或者是Android中都提供了这样两种注解“@NotNull”和“@Nullable”,一种表示变量不可为空,一种表示变量可为空,这主要是为了辅助编译器检查代码,就像下面这样:

public void showMsg(@NotNull String tag, @Nullable String msg){
        
}
复制代码

当咱们发生Java代码和kotlin代码的互相调用时,kotlin会将使用@Nullable注解的变量对应到kotlin中的可空类型的变量,将使用@NotNull注解的变量对应到kotlin中的非空类型的变量。

不存在注解的状况

在咱们平常使用的大部分类库中,其实都没有上面介绍的两种注解,那kotlin是怎么处理的呢?kotlin针对这种状况提出了一种类型叫平台类型,也就是没有注解的Java类型,对应为kotlin中的平台类型。你能够将其看做是kotlin中的可空类型,也能够看做是kotlin中的非空类型,并且你能够在其上面调用各类方法,编译器都不会报错,由于kotlin将变量的可空性控制权交到了使用者手上,彻底由使用者来控制,并且若是你使用null进行了调用,就会报NPE。就像下面这样。

//咱们在Java中定义的一个Message类
public class Message {
    private String msg;
    private int code;


    public Message(String msg, int code) {
        this.msg = msg;
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}
复制代码
//在kotlin中进行调用
fun showMsg(msg: Message){
    println(msg.msg.toUpperCase())
}

fun main(args: Array<String>) {
    showMsg(Message(null, 500))
}
复制代码

kotlin的编译器根本没法肯定Message类中msg属性的可空性,因此只能将其做为平台类型由用户控制,因此上面的代码运行会出现NPE。

继承的状况

当kotlin继承或者实现了Java中的某个类或者接口时,那对于方法的参数和返回值怎么处理呢?这里咱们只讨论没有注解的平台类型,存在注解的状况跟前面说过的同样。咱们看下面的代码例子:

//Java定义的接口
interface Account{
    void login(String username);
}
复制代码
//kotlin中实现接口(参数类型能够是可空类型)
class UserAccount: Account{
    override fun login(username: String?) {

    }
}
复制代码
//kotlin中实现接口(参数类型能够是不可空类型)
class UserAccount: Account{
    override fun login(username: String) {

    }
}
复制代码

上面的两种状况,kotlin的编译器都不会报错,均可以编译经过。因此这个彻底由开发者本身掌控。

kotlin中的类型在Java中被调用

前面说的几种状况都是Java中的类型在kotlin中的处理状况,那么kotlin中的可空类型和不可空类型在Java中是怎么处理的呢?

//kotlin中声明一个非空参数的函数
fun showToast(msg: String){
}
复制代码

在Java中使用以下代码进行调用:

public static void main(String[] args) {
    MainKt.showToast(null);
}
复制代码

而后你会发现抛出了一个异常以下:

Exception in thread "main" java.lang.IllegalArgumentException: 
Parameter specified as non-null is null: 
method MainKt.showToast, parameter msg
复制代码

仔细想一下会发现有个疑问,虽然咱们给showToast函数传递了一个null,可是咱们的showToast函数中并无对传入的参数进行使用啊。在Java中,只有咱们在null对象上发生了调用,才会报异常,可是在koltin中则不一样。再仔细查看上面的异常,你会发现它报的并非NPE,而是参数异常。由此得出结论,kotlin中会为每个声明为非空类型的参数生成一个非空断言,当咱们尝试在Java中传入一个null值时,就会触发这个断言,也就是咱们上面看到的异常。

写在最后

本节主要介绍了kotlin中对于可空类型的处理的一些方法和运算符,能够看出kotlin中仍是有不少特性来保证代码尽量的优雅。可是在与Java进行互调的过程当中,为了保证尽量与Java良好的互调特性,因此存在上面介绍的平台类型,因此仍是无法彻底避免NPE的产生。

相关文章
相关标签/搜索