Kotlin教程(四)可空性

写在开头:本人打算开始写一个Kotlin系列的教程,一是使本身记忆和理解的更加深入,二是能够分享给一样想学习Kotlin的同窗。系列文章的知识点会以《Kotlin实战》这本书中顺序编写,在将书中知识点展现出来同时,我也会添加对应的Java代码用于对比学习和更好的理解。java

Kotlin教程(一)基础
Kotlin教程(二)函数
Kotlin教程(三)类、对象和接口
Kotlin教程(四)可空性
Kotlin教程(五)类型
Kotlin教程(六)Lambda编程
Kotlin教程(七)运算符重载及其余约定
Kotlin教程(八)高阶函数
Kotlin教程(九)泛型android


这一章实际上在《Kotlin实战》中是第六章,在Lambda以后,可是这一章的内容其实是Kotlin的一大特点之一。所以,我将此章的内容提到了前面汇总。编程

可空性

可空性是Kotlin类型系统中帮助你避免NullPointerException错误的特性。安全

可空类型

若是一个变量可能为null,对变量的方法的调用就是不安全的,由于这样会致使NullPointerException。例如这样一个Java函数:bash

int strLen(String s) {
    return s.length();
}
复制代码

若是这个函数被调用的时候,传给它的是一个null实参,它就会抛出NullPointerException。那么你是否须要在方法中增长对null的检查呢?这取决与你是否指望这个函数被调用的时候传给它的实参能够为null。若是不能够的话,咱们用Kotlin能够这样定义:框架

fun strLen(s: String) = s.length
复制代码

看上去与Java没有区别,可是你尝试调用strLen(null) 就会发如今编译期就会被标记成错误。由于在Kotlin中String 只能表示字符串,而不能表示null,若是你想支持这个方法能够传null,则须要在类型后面加上?ide

fun strLen(s: String?) = if(s != null) s.length else 0
复制代码

? 能够加在任何类型的后面来表示这个类型的变量能够存储null引用:String?Int?MyCustomType?等。函数

一旦你有一个可空类型的值,能对它进行的操做也会受到限制。例如不能直接调用它的方法:工具

val s: String? = ""
//    s.length  //错误,only safe(?.) or non-null asserted (!!.) calls are allowed
    s?.length   //表示若是s不为null则调用length属性
    s!!.length  //表示断言s不为null,直接调用length属性,若是s运行时为null,则一样会crash
复制代码

也不能把它赋值给非空类型的变量:post

val x: String? = null
//    val y: String = x  //Type mismatch
复制代码

也就是说,加? 和不加能够看作是两种类型,只有与null进行比较后,编译器才会智能转换这个类型。

fun strLen(s: String?) = if(s != null) s.length else 0  
复制代码

这个例子就与null进行比较,因而String? 类型被智能转换成String 类型,因此能够直接获取length属性。

Java有一些帮助解决NullPointerException问题的工具。好比,有些人会使用注解(@Nullable和@NotNull)来表达值得可空性。有些工具能够利用这些注解来发现可能抛出NullPointerException的位置,但这些工具不是标准Java编译过程的一部分,因此很难保证他们自始至终都被应用。并且在整个代码库中很难使用注解标记全部可能发生错误的地方,让他们都被探测到。

Kotlin的可空类型完美得解决了空指针的发生。 注意,可空的和非空的对象在运行时没有什么区别:可空类型并非非空类型的包装。全部的检查都发生在编译器。这意味着使用Kotlin的可空类型并不会在运行时带来额外的开销。

安全调用运算符:"?."

Kotlin的弹药库中最有效的一种工具就是安全调用运算符:?. ,它容许你爸一次null检查和一次方法调用合并成一个操做。例如表达式s?.toUpperCase() 等同于if (s != null) s.toUpperCase() else null 。 换句话说,若是你视图调用一个非空值得方法,此次方法调用会被正常地执行。但若是值是null,此次调用不会发生,而整个表达式的值为null。所以表达式s?.toUpperCase() 的返回类型是String?

安全调用一样也能用来访问属性,而且能够连续获取多层属性:

class Address(val street: String, val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
    val country = this.company?.address?.country  //多个安全调用连接在一块儿
    return if (country != null) country else "Unknown"
}
复制代码

Kotlin 可让null检查的变得很是简洁。在这个例子中你用一个值和null比较,若是这个值不为空就返回这个值,不然返回其余的值。在Kotlin中有更简单的写法。

Elvis运算符:"?:"

if (country != null) country else "Unknown" 经过Elvis运算符改写成:

country ?: "Unknown"
复制代码

Elvis运算符接受两个运算数,若是第一个运算数不为null,运算结果就是第一个运算数,若是第一个运算数为null,运算结果就是第二个运算数。 fun strLen(s: String?) = if(s != null) s.length else 0 这个例子也能够用Elvis运算符简写:fun strLen(s: String?) = s?.length ?: 0

安全转换:"as?"

以前咱们学习了as 运算符用于Kotlin中的类型转换。和Java同样,若是被转换的值不是你试图转换的类型,就会抛出ClassCastException异常。固然你能够结合is 检查来确保这个值拥有合适的类型。但Kotlin做为一种安全简洁的语言,有优雅的解决方案。 as? 运算符尝试把值转换成指定的类型,若是值不合适的类型就返回null。 一种常见的模式是把安全转换和Elvis 运算符结合使用。例如equals方法的时候这样的用法很是方便:

class Person(val name: String, val company: Company?) {
    override fun equals(other: Any?): Boolean {
        val o = other as? Person ?: return false  //检查类型不匹配直接返回false
        return o.name == name && o.company == company //在安全转换后o被智能地转换为Person类型
    }

    override fun hashCode(): Int = name.hashCode() * 31 + (company?.hashCode() ?: 0)
}
复制代码

非空断言:"!!"

非空断言是Kotlin提供的最简单直接的处理可空类型值得工具,它能够把任何值转换成非空类型。若是对null值作非空断言,则会抛出异常。 以前咱们也演示过非空断言的用法了:s!!.length

你可能注意到双感叹号看起来有点粗暴,就像你冲着编译器咆哮。这是有意为之的,Kotlin的设计设视图说服你思考更好的解决方案,这些方案不会使用断言这种编译器没法验证的方式。

可是确实存在这样的状况,某些问题适合用非空断言来解决。当你在一个函数中检查一个值是否为null。而在另外一个函数中使用这个值时,这种状况下编译器没法识别这种用是否安全。若是你确信这样的检查必定在其余某个函数中存在,你可能不想在使用这个值以前重复检查。这时你就可使用非空断言。

"let" 函数

let函数让处理可空表达式变得更容易。和安全调用运算符一块儿,它容许你对表达式求值,检查求值结果是否为null,并把结果保存为一个变量。全部这些动做都砸系统一个简洁的表达式中。 可空参数最多见的一种用法应该就是被传递给一个接受非空参数的函数。好比说下面这个函数,它接收一个String类型的参数并向这个地址发送一封邮件,这个函数在Kotlin中是这样写的:

fun sendEmailTo(email: String) { ... }
复制代码

不能把null传给这个函数,所以一般须要先判断一下而后调用函数: if(email != null) sendEmailTo(email) 。 但咱们有另外一种方式:使用let函数,并经过安全调用来调用它。let函数作的全部事情就是把一个调用它的对象变成lambda表达式的参数: email?.let{ email -> sendEmailTo(email) } let函数只有在email的值非空时才被调用,若是email值为null则{} 的代码不会执行。 使用自动生成的名字it 这种简明语法以后,能够写成:email?.let{ sendEmailTo(it) } 。(Lambda的语法在只有章节会详细讲)

延迟初始化的属性

不少框架会在对象实例建立以后用专门的方法来初始化对象。例如Android中,Activity的初始化就发生在onCreate方法中。而JUnit则要求你把初始化的逻辑放在用@Brefore注解的方法中。 可是你不能再狗仔方法中彻底放弃非空属性的初始化器。仅仅在一个特殊的方法里初始化它。Kotlin一般要求你在构造方法中初始化全部属性,若是某个属性时非空类型,你就必须提供非空的初始化值。不然,你就必须使用可空类型。若是你这样作,该属性的每次访问都须要null检查或者!! 运算符。

class Activity {
    var view: View? = null

    fun onCreate() {
        view = View()
    }

    fun other() {
        //use view
        view!!.onLongClickListener = ...
    }
}
复制代码

这样使用起来比较麻烦,为了解决这个麻烦,使用lateinit 修饰符来声明一个不须要初始化器的非空类型的属性:

class Activity {
    lateinit var view: View

    fun onCreate() {
        view = View()
    }

    fun other() {
        //use view
        view.onLongClickListener = ...
    }
}
复制代码

注意,延迟初始化的属性都是var 由于须要在构造方法外修改它的值,而val 属性会被编译成必须在构造方法中初始化的final字段。尽管这个属性时非空类型,可是你不须要再构造方法中初始化它。若是在属性被初始化以前就访问了它,会获得异常"lateinit property xx has not been initialized" ,说明属性尚未被初始化。

注意lateinit属性常见的一种用法是依赖注入。在这种状况下,lateinit属性的值是被依赖注入框架从外部设置的。为了保证和各类Java框架的兼容性,Kotlin会自动生成一个和lateinit属性具备相同可见性的字段,若是属性的可见性是public,申城字段的可见性也是public。

public final class Activity {
   public View view;

   public final View getView() {
      View var10000 = this.view;
      if(this.view == null) {
         Intrinsics.throwUninitializedPropertyAccessException("view");
      }
      return var10000;
   }

   public final void setView(@NotNull View var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.view = var1;
   }

   public final void onCreate() {
      this.view = new View();
   }

   public final void other() {
   }
}
复制代码

可空性的扩展

为可空类型定义扩展函数是一种更强大的处理null值的方式。能够容许接收者为null的(扩展函数)调用,并在该函数中处理null,而不是在确保变量为null以后再调用它的方法。 Kotlin标准库中定义的String的两个扩展函数isEmptyisBlank 就是这样的例子。第一个函数判断字符串是不是一个空的字符串"" 。第二个函数判断它是不是空的或则只包含空白字符。一般用这些函数来检查字符串是有价值的,以确保对它的操做是有意义的。你可能意识到,像处理无心义的空字符串和空白字符串这样处理null也颇有用。事实上,你的确能够这样作:函数isEmptyOrNullisNullOrBlank 就能够由String? 类型的接收者调用。

fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) { //此方法是String?的方法,不须要安全调用
        println("Please fill in the required fields")
    }
}
复制代码

不管input是null仍是字符串都不会致使任何异常。咱们来看下isNullOrBlank 函数的定义:

public inline fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank()
复制代码

能够看到扩展函数是定义给CharSequence? (String的父类),所以不像调用String的方法那样须要安全调用。 当你为一个可空类型定义扩展函数时,这觉得这你能够对可空的值调用这个函数;而且函数体中this可能为null,因此你必须显示地检查。在Java中,this永远是非空的,由于他引用的时当前你所在这个类的实例。而在Kotlin中,这并不永远成立:在可空类型的扩展函数中,this能够为null。 以前讨论的let 函数也能被可空的接收者调用,但它并不检查值是否为null。若是你在一个可空类型直接调用let 函数,而没有使用安全调用运算符,lambda的实参将会是可空的:

val person: Person? = ...
person.let { sendEmailTo(it) }  //没有安全调用,因此it是可空类型

ERROR: Type mismatch:inferred type is Person? but Person was expected
复制代码

所以,若是想要使用let来检查非空的实参,你就必须使用安全调用运算符?. 就像以前看到的代码同样:person?.let{ sentEmailTo(it) }

当你定义本身的扩展函数时,须要考虑该扩展是否须要可空类型定义。默认状况下,应该把它定义成非空类型的扩展函数。若是发现大部分状况下须要在可空类型上使用这个函数,你能够稍后再安全地修改他(不会破坏其余代码)。

类型参数的可空性

Kotlin中全部泛型和泛型函数的类型参数默认都是可空的。任何类型,包括可空类型在内,均可以替换类型参数。这种状况下,使用类型参数做为类型声明都容许为null,尽管类型参数T并无用问号结尾。

fun <T> printHashCode(t: T) {
    println(t?.hashCode())
}
复制代码

在该函数中,类型参数T推导出的类型是可空类型Any? 所以,尽管没有用问号结尾。实参t依然容许持有null。 要使用类型参数非空,必需要为它指定一个非空的上界,那样泛型会拒绝可空值做为实参:

fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}
复制代码

后续章节会讲更多的泛型细节,这里你只须要记得这一点就能够了。

可空性和Java

咱们在Kotlin中经过可空性能够完美地处理null了,可是若是是与Java交叉的项目中呢?Java的类型系统是不支持可空性的,那么该若是处理呢? Java中可空性信息一般是经过注解来表达的,当代码中出现这种信息时,Kotlin就会识别它,转换成对应的Kotlin类型。例如:@Nullable String -> String?@NotNull String -> String。 Kotlin能够识别多种不一样风格的可空性注解,包括JSR-305标准的注解(javax.annotation包下)、Android的注解(android.support.annitation) 和JetBrans工具支持的注解(org.jetbrains.annotations)。那么还剩下一个问题,若是没有注解怎么办呢?

平台类型

没有注解的Java类型会变成Kotlin中的平台类型 。平台类型本质上就是Kotlin不知道可空性信息的类型。便可以把它当作可空类型处理,也能够当作非空类型处理。这意味着,你要像在Java中同样,对你在这个类型上作的操做负有所有责任。编译器将会容许全部操做,它不会把对这些值得空安全操做高亮成多余的,但它平时倒是这样对待非空类型值上的空安全操做的。 好比咱们在Java中定义一个Person类:

public class Person {
    private  String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

咱们在Kotlin中使用这个类:

fun yellAt(person: Person) {
    println(person.name.toUpperCase()) //不考虑null状况,可是若是为null则抛出异常
    println((person.name ?: "Anyone").toUpperCase()) //考虑null的可能
}
复制代码

咱们便可以当成非空类型处理,也能够当成可空类型处理。

Kotlin平台类型在表现为:Type!

val i: Int = person.name

ERROR: Type mistach: inferred type is String! but Int was expected
复制代码

可是你不能声明一个平台类型的变量,这些类型只能来自Java代码。你能够用你喜欢的方式来解释平台类型:

val person = Person()
val name: String = person.name
val name2: String? = person.name
复制代码

固然若是平台类型是null,赋值给非空类型时仍是会抛出异常。

为何须要平台类型? 对Kotlin来讲,把来自Java的全部值都当成可空的是否是更安全?这种设计也许可行,可是这须要对永远不为空的值作大量冗余的null检查,由于Kotlin编译器没法了解到这些信息。 涉及泛型的话这样状况就更糟糕了。例如,在Kotlin中,每一个来自Java的ArrayList 都被看成ArrayList<String?>?,每次访问或者转换类型都须要检查这些值是否为null,这将抵消掉安全性带来的好处。编写这样的检查很是使人厌烦,因此Kotlin的设计者做出了更实用的选择,让开发者负责正确处理来自Java的值。

继承

当在Kotlin中重写Java的方法时,能够选择把参数和返回类型定义成可空的,也能够选择把它们定义成非空的。例如,咱们来看一个例子:

/* Java */
interface StringProcessor {
    void process(String value);
}
复制代码

Kotlin中下面两种实现编译器均可以接收:

class StringPrinter : StringProcessor {
    override fun process(value: String) {
        println(value)
    }
}

class NullableStringPrinter : StringProcessor {
    override fun process(value: String?) {
        if (value != null) {
            println(value)
        }
    }
}
复制代码

注意,在实现Java类或者接口的方法时必定要搞清楚它的可空性。由于方法的实现能够在非Kotlin的代码中被调用,Kotlin编译器会为你声明的每个非空的参数生成非空断言。若是Java代码传给这个方法一个null值,断言将会触发,你会获得一个异常,即使你从没有在你的实现中访问过这个参数的值。

所以,建议你只有在确保调用该方法时绝对不会出现空值时,才用非空类型取接收平台类型。

相关文章
相关标签/搜索