阅读 140

四、kotlin的可空性和基本数据类型

可空性

前言: 可空性是kotlin类型系统提供的功能, 帮助你避免 NullPointerException

是什么?

是一种可以为 null 的类型, 本质是下面这样:

Type? == Type or null

var str: String? = null
复制代码

说白了, 你就把他当作一种新的类型就好, 这样的话, 如果遇到

var a: Int? = 10
var b: Int = a // 报错
复制代码

这面这种情况时, 不会觉得诧异, 毕竟是不同的类型不是么???

作用

在不影响程序运行性能的前提下, 显示的帮助程序员避免空指针异常 NullPointerException

可空类型在编译期间, 就把空指针异常解决了, 在运行期间不做任何操作, 所以不影响运行时性能

在java中这样容易出现空指针异常

int strLen(String s) {
    return s.length(); // 这句话无法确定 s 是否为 null
}
复制代码

在实际的java项目, 都需要 if 判断

// 可以用 三目运算符, 一行解决, 但也很麻烦
int strLen(String s)  {
    int len = 0;
    if (null == s || (len = s.length()) <= 0) {
        throw new RuntimeException("字符串长度为空")
    }
    return len;
}
复制代码

当然 jdk1.8 之后出现的 Optional , 但还是麻烦的, 不仅使代码变得冗长而且还存在性能问题

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

使用 kotlin 重写这个函数前需要程序员主动判断该函数是否接受实参为空的情况, 如果需要支持的话,

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

在上面代码中, s?.length 如果 s 为 null 的话, 则该函数直接返回 null , 函数调用者 可以借助返回值 null 使用 if 判断是否为空

如果实参一定不为 null 的话, 则

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

对了, 和前面的 when 的 is Int 智能转换一样, 可空类型也存在智能转换

var a: String? = "zhazha"
var b: String
if (a != null) {
    b = a // 这行代码不会报错
    println(b)
}
复制代码

怎么用?

方法一: 使用安全调用运算符 ?.

image.png

前面的示例代码中 s?.length 会发现 ? 运算符, 这种方式相当于

if (s == null) null else s.length
复制代码

如果 s == null 的情况下 整个 s?.length 表达式的值为 null, 在该表达式为 null 的情况下, 会出现

val len: Int? = s?.length
//          👆
复制代码

接收该函数返回值的变量类型也应该是 可空的, 毕竟结果可能是 null

所以使用安全调用操作符?, 其接收结果的变量也需要可空操作符

另外 ? 运算符还可以链式调用, 比如:

val name:String? = person?.children?.name
复制代码

只要有一步骤结果为 null, 后面的代码不再运行, 整个表达式的结果为 null

? 这种方式是线程安全的

方法二: Elvis运算符 ?:

image.png

val firstName: String? = "zhazha"
val lastName: String = firstName ?: ""
复制代码

可以看到使用这种方式之后 ? 运算符消失了

类似于:

if (firstName == null) "" else firstName
复制代码

Elvis 还是这样:

val lastName: String = firstName ?: throw Exception("错误")
复制代码

方法三: if

if (firstName != null) {
    val lastName: String = firstName
}
复制代码

这种方法在你觉得代码可读性比较低时,使用, 但是有个前提, firstName 不为 共享变量(多线程的共享变量), 否则还是会报错

方法四: 使用非空断言运算符!!.

image.png

使用这种方式确实可以脱下? 外衣, 但对于空指针的检测直接关闭了, 表达式中的变量是否会发生空指针异常已经不管了

val firstName: String? = null
val lastName: String = firstName
复制代码

这种方式不推荐使用, 除非你能保证该值绝对不为空, 比如: 不使用 object 定义的单例

方式五: 先决条件函数

这些函数都能脱下 ? 外衣

fun main(args: Array<String>) {
    val firstName: String? = null
//    checkNotNull(firstName)
//    checkNotNull(firstName) { "firstName 为空" }
//    requireNotNull(firstName) { "firstName 为空" }
//    check(firstName != null)
    require(firstName != null)
    val s: String = firstName
}
复制代码

如果要给函数类型添加可空性, val funType: (() -> T)? 这样做

安全转换 as?

image.png

前面的章节学过, as 作为强转操作符, 在使用的过程中可以 配合 is 强制转换, 但如果类型转化不成功就会报ClassCastException

所以kotlin创造了 as? 使用方法

private fun sum(a: Any, b: Any) {
    val c: Int? = a as? Int
    val d: Int? = b as? Int
}
复制代码

在例子中, 如果 Any 参数指向的类型不是 Int , 则返回 null 给 c 变量, 否则强转成功

一般 as? 配合 ?: 使用

private fun sum(a: Any, b:Any): Int {
    val c: Int = a as? Int ?: 0
    val d: Int = b as? Int ?: 0
    return c + d
}
复制代码

let 函数

let 函数源码:

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
复制代码

可以看出它就是个扩展函数

fun main(args: Array<String>) {
    val str:String? = null
    println(str?.let { it.length + 100 }) // null
}
复制代码

打印出了 null , str == null, 所以 str? == null 后面的let函数将不执行, 直接返回 null 但是我们需要 null 的时候等于 0 最终要打印 100

fun main(args: Array<String>) {
    val str:String? = null
    println(str.let { (it?.length ?: 0) + 100 })
}
复制代码

看到代码中的 str.let 了么? str == null 但是 str.let 却不会报错? 看的出来 扩展函数 的优势了么? null.let 不会报错, 了解扩展函数的本质后, 会发现不报错也合理, 扩展函数仅仅是把目标对象的 this 当作 形式参数 , 但这里的 this 是 null , 传递一个等于 null 的参数没问题吧???

可空性扩展函数

为可空类型定义扩展函数处理 null 问题

val str: String? = null

if (str.isNullOrBlank()) {
    throw Exception("str == null or str is blank")
}
复制代码

源码就类似这样: return this == null || this.isBlank()

可以看的出来 str == nullnull 能调用 null.isNullOrBlank() (null直接调用 isNullOrBlank 会报错, 但 把 null 赋值给 str , 再调用 isNullOrBlank 不会报错)

只有扩展函数才能做到这一点,普通成员方法的调用是通过对象实例来分发的,因此实例为 null 时(成员方法)永远不能被执行。

延迟初始化 lateinit

很多时候, 成员属性的初始化未必全部都需要在构造函数内完成, 看下面这段代码的成员属性 a

private class MyClass(val b: Int) {
    var a: Person // 报错
    constructor(a: Person, b: Int) : this(b) {
        this.a = a
    }
    
    init {
        // init balabala
    }
}
复制代码

这里的 a 报错, 主要的问题是 kotlin 对象的初始化顺序是

调用主构造函数 => 主构造外的成员属性或者init代码块(根据这俩的定义顺序判断) => 再调用次构造函数

比如 b 在主构造函数内, 而 a 在主构造函数外

次构造函数在构建一个对象的时候, 会调用两个构造函数, 一个是主构造函数, 另一个是次构造函数

主构造函数外属性init代码块 在构造一个对象时, 都会被编译器放入到 主构造函数体内

// 假设这是主构造函数
constructor(b: Int) {
    this.b = b
    // 上面就是主构造全部的内容
    // 接下来是init和主构造函数外属性的内容
    this.a = ? // error, 在初始化变量 a 的时候不清楚要给它初始化成什么???? 所以报错了
    // init balabala
    // 然后再调用次构造函数(如果你使用次构造函数构造一个对象的话)
}

// 然后调用次构造函数
constructor(a: Int) {
    this.a = a // 在次构造函数初始化时, 主构造函数报错了, 次构造函数来不及构建一个对象
}
复制代码

遇到这种情况一般都解决方案都是 var a: Int = 0 给它初始化, 但是在一些架构中, 人家有专门的初始化方案, 不需要程序员主动帮助初始化, 比如: Spring

这时候就需要 lateinit 关键字

private class MyClass(val b: Int) {
    lateinit var a: Person
    constructor(a: Person, b: Int) : this(b) {
        this.a = a
    }
    
    init {
        // init balabala
    }
}
复制代码

但是这关键字有限制的:

  1. 不能修饰 val 属性, 只能修饰 var
  2. 不能修饰基础数据类型, 比如: Int Double Float Long 之类的属性

懒加载初始化(惰性初始化)

class Player {
    val config: String by lazy { loadConfig() }
    fun loadConfig(): String {
        println("load Config...")
        return "xxxxxxxxx"
    }
}
复制代码
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
复制代码

lazy 后面传入的是 函数类型 , 一个无参数的返回 T 类型的函数类型 () -> T

类型参数的可空性(泛型可空性)

类型参数传递可以是空的

fun <T> printHashCode(t: T) {
    print(t?.hashCode())
}

fun main(args: Array<String>) {
    printHashCode(null)
}
复制代码

类型参数传递的是 T 没有任何的 ? , 但是仍然可以传递 null , 这时在函数内部如果没写上 t? 那么就会报 空指针异常

null 的类型是 Any?

可空性在 kotlin 和 java 之间的问题

平台类型

kotlin调用 java 的函数时, 无法判断 java 的参数是否为 可空性 , 所以专门推出了 平台类型

java的平台类型 = kotlin的可空类型 or kotlin的非空类型

image.png

这项判断由程序员自主判断

在java下, 创建 Person

public class Person {
    private final String name;
    public String getName() {
        return name;
    }
    public Person(String name) {
        this.name = name;
    }
}
复制代码

在 kotlin 中使用

fun yellAt(person: Person) {
//    println(person.name.toUpperCase() + "!!!") // java.lang.NullPointerException: person.name must not be null
    println(person.name?.toUpperCase() + "!!!")
}

fun main(args: Array<String>) {
    val person = Person(null)
    yellAt(person)
}
复制代码

person: Person 参数没有 可空性 ?, 但他是 平台类型, 程序员可以选择是否按照可空类型判断 person.name?.toUpperCase(), 也可以按照非空判断 person.name.toUpperCase() 怎么不报错怎么来

kotlin 用 Person! 表示一个来自java平台的 平台类型 , 用户不可以自行使用 ! , 它仅仅是提示程序员 该变量 未知可空性

平台类型遇到继承

kotlin 继承重写 java 函数时, 可以选择 类型为 可空的 ,也可以选择类型为 非空的

public interface StringProcessor {
    void process(String value);
}
复制代码
class StringPrinter : StringProcessor {
    override fun process(value: String) {
        println(value)
    }
}

class NullableStringPrinter : StringProcessor {
    override fun process(value: String?) {
        println(value ?: "")
    }
}
复制代码

基本数据类型和其他数据类型

kotlin 没有包装类型

基本数据类型

  1. kotlin不区分基本数据类型和包装类型, kotlin使用的都是包装类型,但是在运行时使用的却是基础类型. 对kotlin编码期间的函数最终都会被kotlin编译器修改成对基础类型的操作
var a: Int = 10
val plus = a.plus(1)
复制代码

java 反编译后:

int a = 10;
int plus = a + 1;
复制代码

不得不吹一波 kotlin 编译器的强大, 但强大带来的却是编译速度缓慢, 哎~~~

  1. 泛型的基本数据类型最终会被编译成 Integer
val list = ArrayList<Int>()
复制代码

Java:

ArrayList<Integer> list = new ArrayList<Integer>();
复制代码
  1. kotlin的基本数据类型不能存储 null

kotlin的基本数据类型和java的一致, 都不能存储 null, java的基本数据类型在 kotlin 中不会变成 平台类型 而是直接变成 基本数据类型

可空的基本数据类型

kotlin的可空基本数据类型无法翻译成 java 的 基本数据类型, 所以任何可空类型, 最终都会变成 包装类型

class Person(val name: String, val age: Int?) {
   fun isOldThan(other: Person): Boolean? = this.age?.let {
      other.age?.let { it2 ->
         it > it2
      }
   }
}

fun main(args: Array<String>) {
   val person = Person("zhazha", 23)
   val person1 = Person("xixix", 21)
   val b = person.isOldThan(person1)
   if (null == b) println("不清楚") else if (b) println("大于") else println("小于")
}
复制代码
public final class Person {
    @NotNull
    private final String name;
    @Nullable
    private final Integer age;
    
    // 略
}
复制代码

数字转换

  1. kotlin 不会将基本数据类型隐式转换, 比如 小范围的Int 变量转换成 Long, 这和java还是有区别, 这样做的好处在于更加的安全可控
val a: Int = 100
val b: Long = a // 报错
复制代码
  1. kotlin 对每个基本数据类型提供了 toXXXX 函数(除了 Boolean), 这种显示的转换可以大范围转小范围, 也可小范围转大范围

  2. 在 java 中, 包装类型的比较会出现下面这种问题

new Integer(42).equals(new Long(42)) // false
复制代码

这俩明明都是 42 但不相等, 在 java 中 equals 有判断类型的, 所以会返回false, 如果需要则要转换成相同类型

在 kotlin 中, 如果变量没有转换到同一个类型, 也无法比较

image.png

需要转换

image.png

Any 和 Any? 根类型

  1. Any 类似于 java 的Object 对象, 是 kotlin 所有非空类的共有根类, 而不论是空类还是非空类的所有类都可以传给 Any?

  2. Any 有很多 Object 的函数, 但并不是所有, 有些函数 比如 wait / notify 函数只能通过 Any 强转成 Object 来调用该函数

Unit 类型: kotlin 的 void

Unit 和 void 的差别在于:

  1. 在 kotlin 中, Unit 是一个类, Unit 可以当作函数的参数, 平时使用时 Unit 会被转化成 java 的 void

  2. Unit 不需要主动的 return , 会隐式的返回 Unit

public object Unit {
    override fun toString() = "kotlin.Unit"
}
复制代码

Nothing 类型: 这个函数不返回

Nothing 没有值, 只有被当作函数返回值或者被当作泛型函数返回值的类型参数使用才会有意义

源码:

public class Nothing private constructor()
复制代码

使用:

fun fail(message: String): Nothing {
    throw Exception(message)
}
复制代码

可空性和集合

List<Int?>List<Int?>?

image.png

文章分类
后端
文章标签