写在开头: 本人打算开始写一个 Kotlin 系列的教程, 一是使自己记忆和理解的更加深刻, 二是可以分享给同样想学习 Kotlin 的同学系列文章的知识点会以 Kotlin 实战这本书中顺序编写, 在将书中知识点展示出来同时, 我也会添加对应的 Java 代码用于对比学习和更好的理解
Kotlin 教程 (一) 基础
Kotlin 教程 (二) 函数
Kotlin 教程 (三) 类对象和接口
Kotlin 教程 (五) 类型
这一章实际上在 Kotlin 实战中是第六章, 在 Lambda 之后, 但是这一章的内容实际上是 Kotlin 的一大特色之一因此, 我将此章的内容提到了前面汇总
可空性
可空性是 Kotlin 类型系统中帮助你避免 NullPointerException 错误的特性
可空类型
如果一个变量可能为 null, 对变量的方法的调用就是不安全的, 因为这样会导致 NullPointerException 例如这样一个 Java 函数:
- 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, 则需要在类型后面加上? :
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
也不能把它赋值给非空类型的变量:
- 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 的两个扩展函数 isEmpty 和 isBlank 就是这样的例子第一个函数判断字符串是否是一个空的字符串 "" 第二个函数判断它是否是空的或则只包含空白字符通常用这些函数来检查字符串是有价值的, 以确保对它的操作是有意义的你可能意识到, 像处理无意义的空字符串和空白字符串这样处理 null 也很有用事实上, 你的确可以这样做: 函数 isEmptyOrNull 和 isNullOrBlank 就可以由 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 值, 断言将会触发, 你会得到一个异常, 即便你从没有在你的实现中访问过这个参数的值
因此, 建议你只有在确保调用该方法时绝对不会出现空值时, 才用非空类型取接收平台类型
来源: https://juejin.im/post/5abb026af265da239f076a77