可空性
它是kotlin类型系统中帮助你避免NullPointerException错误的特性。
可空类型
kotlin和java类型系统第一条也可能是最重要的一条区别:kotlin对可空类型的显式的支持,这是一种指出你的程序中哪些变量和属性允许为null的方式。如果一个变量可以为null,对变量的方法的调用就是不安全的,因为这样会导致空指针异常,kotlin不允许这种调用。
int strLen(String s){
return s.length()
}
这个函数如果传一个null参数,就会抛出空指针异常,我们试着用kotlin重写该函数,但我们要决定这个函数被调用的时候传给它的实参可以是null吗?
不能为null的写法
此时使用为null的实参调用strLen在编译期就会标记成错误,这个函数中的参数被声明为String类型,在kotlin中表示它必须包含一个String实例。
可以为null的写法
只需要在类型名称后面加上问号来标记。问好可以加载任何类型的后面来表示这个类型的变量可以存储null引用,没有问号的类型表示这种类型的变量不能存储null引用,这说明所有常见类型默认都是非空的,除非显式把它标记为可空
Type? =Type or null
一旦有一个可空类型的值,能对它进行的操作也会受到限制,比如不能再调用它的方法,也不能把它赋值给非空类型的变量,还不能把可空类型的值传给拥有非空类型参数的函数。我们可以做的最重要的操作就是和null进行比较,一旦进行了比较操作,编译器就会记住,并且在这次比较方法的作用域内把这个值当作非空来对待
安全调用运算符"?.
"
该运算符允许你把一次null检查和一次方法调用合并成一个操作。
比如s?.toUpperCase()等同于if(s!=null)s.toUpperCase()else null。也就是说,如果你试图调用一个非空值的方法,这次方法调用会被正常地执行,但如果值是null则调用不会发生,整个表达式的值为null。
这次调用的结果类型也是可空的,s?.toUpperCase()结果的类型是String?,即使它本身的方法声明中是String。
安全调用也可以用来访问属性
如果你的对象图中有多个可空类型的属性,通常可以在同一个表达式中方便地使用多个安全调用
链接多个安全调用
Elvis运算符"?:
"
kotlin用Elvis运算符来提供代替null的默认值
该运算符接收两个运算数,如果第一个运算数不为null,运算结果就是第一个运算数,如果为null,运算结果就是第二个运算数。
它经常与安全调用运算符一起使用,用一个值代替对null对象调用方法时返回的null
像return和throw这样的操作其实是表达式,因此可以把它们卸载Elvis运算符的右边。这种情况下如果左边的值为null,函数就会立即返回一个值或者抛出一个异常
安全转换:"as?
"
as?运算符长十八值转换成指定的类型,如果值不是合适的类型就返回null,一种常见的模式是把安全转换和Elvis运算符结合使用
使用安全转换实现equals
使用这种模式可以非常容易检查实参是否是适当的类型,转换它并在它的类型不正确时返回false,而且这些操作全部在同一个表达式中
当你检查过类型并拒绝了null值,编译器就确定了变量otherp的类型是person并让你相应地使用它
非空断言:"!!
"
非空断言是kotlin提供的最简单的处理可空类型值的工具。
它使用双感叹号表示,可以把任何值转换成非空类型。如果对null值做非空断言,则会抛出异常,如下图
注意异常抛出的位置是非空断言所在的那一行,而不是接下来使用值的那一行
当你使用!!并且它的结果是异常时,异常调用栈的跟踪信息只表明异常发生在哪一行代码,而不会表明一场发生在哪一个表达式。为了让跟踪信息更清晰表示哪个值为null,不要在同一行使用多个非空断言
let
函数
它让处理可空表达式变得更容易。和安全调用运算符一起,它允许你对表达式求值,检查求值结果是否为nul,并把结果保存为一个变量。所有的这些动作都在同一个简洁的表达式中。可空参数最常见的一种用法就是被传递给一个接收非空参数的函数。而let函数做的所有事情就是把一个调用它的对象变成lambda表达式的参数。结合安全调用语法,它能有效地把调用let函数的可空对象,转变成非空类型
如上图,不使用let函数的情况下必须显式检查这个值不为null
let函数只在email的值非空时才会被调用,所以你能在lambda中把email当作非空实参使用
用it
简化代码
如果有一些很长的表达式结果不为null,而你又要使用这些结果时,let表示法很方便,这种情况下不必创建一个单独的变量。当你需要检查多个值是否为null时,可以用嵌套的let调用来处理,但大多数情况下这种代码又啰嗦又难理解,用普通if来一次性检查更好
延迟初始化的属性:lateinit
kotlin通常要求你在构造方法中初始化所有的属性,如果某个属性是非空类型,你就必须提供非空的初始化值,否则你就必须使用可空类型,但若果这么做,该属性的每一次访问都需要null检查或者!!运算符
为了解决这个问题,我们可以使用lateinit修饰符,把属性声明成可以延迟初始化的
注意!延迟初始化的属性都是var,因为需要在构造方法外修改他的值,而val属性会被编译成必须在构造方法中初始化的final字段。尽管该属性是非空类型,但你不需要在构造方法中初始化它。如果在属性被初始化之前就访问它就会异常。
lateinit
属性常见的一种用法是依赖注入,在这种情况下,lateinit属性的值是被依赖注入框架从外部设置的。为了保证和各种java依赖注入框架的兼容性,kotlin会自动生成一个和lateinit属性具有相同可见性的字段。
可空类型的扩展
为可空类型定义扩展函数是一种更强大的处理null值的方式。可以允许接收者为null的扩展函数调用,并在该函数中处理null,而不是在确保变量不为null之后再调用它的方法,只有扩展函数能做到这一点,普通成员方法是通过对象实例分发的,因此实例为null时成员方法永远不能运行。如String库中的isEmpty就是这样。
input是可空类型的值,inNullOrBlank是可空类型的扩展。此处不需要安全访问,可以直接调用为可控接收者声明的扩展函数,这个函数会处理可能的null值
最后一行的第二个this使用了智能转换
当你为一个可空类型定义扩展函数时,你可以对可空的值调用这个函数,并且函数体中的this可能为null,所以你必须显式地检查。在java中this永远是非空的,因为它引用的是当前你所在这个类的实例。而在kotlin中,在可空类型的扩展函数中,this可以为null。我们之前讨论的let函数也能被可控的接收者调用,但它并不检查值是否为null。如果你在一个可空类型上直接调用let函数而不使用安全调用运算符,lambda的实参将会是可空的。因此,如果想使用let来检查非空的实参,你就必须使用安全调用运算符"?."
类型参数的可空性
kotlin中所有泛型类和泛型函数的类型参数默认都是可空的。任何类型,包括可空类型在内,都可以替换类型参数。这种情况下,使用类型参数作为类型的声明都允许为null,尽管类型参数T没有用问号结尾
要使类型参数非空,必须要为它指定一个非空的上界
注意!当你定义自己的扩展函数时,需要考虑该扩展是否需要为可空类型定义。默认情况下应把它定义成非空类型的的扩展函数。如果发现大部分情况下需要在可空类型上使用这个函数,你可以稍后再安全修改它。
可空性和java
当kotlin和java混用时,根据java类型的注解,java类型会在kotlin中表示为可空类型和非空类型
@Nullable + Type = Type?
@NotNull + Type = Type
平台类型
平台类型本质上就是kotlin不知道可空性信息的类型,既可以把它当作可空类型处理,也可以当作非空类型处理。这意味着你要像在java中一样对你在这个类型上做的操作负有全部责任。如果你认为这个值为null,使用前可以和null作比较,不认为它是null,就直接使用,若错误理解则会导致空指针
Type = Type? or Type
对于公有的kotlin函数,编译器会生成对每个非空类型的参数和接收者的检查,所以使用不正确的参数的调用尝试都会立即被报告为异常
在kotlin中不能声明一个平台类型的变量,这些类型只能来自java代码
继承
在kotlin中重写java的方法时,可以选择把参数和返回类型定义成可空的,也可以选择定义成非空的。在实现java类或者接口的方法时一定要搞清楚它的可空性,因为方法的实现可以在非kotlin的代码中被调用,kotlin编译器会为你声明的每一个非空的参数生成非空断言