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


theme: orange

可空性

前言: 可空性是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 如果 snull 的话, 则该函数直接返回 null , 函数调用者 可以借助返回值 null 使用 if 判断是否为空

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

fun strLen(s: String) = s.length

对了, 和前面的 whenis 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

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

这里的“已经不管了”,是错的。正确的说法是 null!! 如果对象本身就是 null 直接抛出空指针异常,所以 !! 是一种不负责任的行为,除非你能保证该变量百分百不会是 null,最好别用,可以使用 ?: 代替

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 当作 形式参数 , 但这里的 thisnull , 传递一个等于 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 之类的属性

判断 lateinit 修饰的对象是否已经被初始化

private class Player {
    private lateinit var equipment: String

    fun ready() {
        equipment = "sharp knife"
    }

    fun battle() {
        if (this::equipment.isInitialized) {
            println(equipment)
        }
    }
}

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

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

Unitvoid 的差别在于:

  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?>?

wait / notify函数只能通过Any强转成Object` 来调用该函数

Unit 类型: kotlin 的 void

Unitvoid 的差别在于:

  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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值