美文网首页Kotlin学习之路
Kotlin基本语法之(五)类型与空安全

Kotlin基本语法之(五)类型与空安全

作者: wanderingGuy | 来源:发表于2019-05-26 00:15 被阅读0次

    本小节是Kotlin基本语法的一个重点章节,介绍了Kotlin中的类型体系和空安全这个重要特性,最后分析了空安全在与Java互操作过程中存在的问题。

    类型体系

    在Java中Object是所有引用类型的基类,而在Kotlin的类型系统中对应的为Any类,另外java存在int/long等等基本数据类型,而在Kotlin中没有,统一使用Int/Long等引用类型。

    数字类型

    Kotlin中的数字类型与Java基本一致。

    类型 宽度(Bit)
    Double 64
    Float 32
    Long 64
    Int 32
    Short 16
    Byte 8

    这些数字类统一继承Number类,其提供了不同类型间显式转换的方法。

    val x: Int = 1
    //toXXX转换函数
    val y: Long = x.toLong()
    

    这里需要注意一个问题,如果Kotlin中没有Java中的基本类型,所有对象都是引用类型,那对于最最常用的数字会不会产生巨大的性能开销?

    事实上,虽然Kotlin中没有基本类型,但它编译成字节码时会做一步优化:将不可空类型(比如Int)优化为Java中的基本类型,将可空对象(比如Int?)转为包装(比如Integer)类型,因为可空类型可赋值为null,Java基本类型是不够用的,所以选择包装类型,此过程可通过反编译kotlin字节码验证。

    //测试代码
    var i: Int = 1
    var j: Int? = 1
    println(i)
    println(j)
    
    //反编译结果
    int i = 1;
    Integer j = 1;
    System.out.println(i);
    System.out.println(j);
    

    字符串类型String

    Kotlin中的String比Java更为强大,支持一系列的扩展函数(后面会讲到),在日常的开发过程中非常实用,举个栗子。

    //使用filter扩展函数 过滤掉'c'字符
    val result = "abcddddface".filter { it != 'c' }
    print("result:$result")
    
    输出:
    result:abddddfae
    

    上面的打印中使用了$来引用一个变量,我们称之为字符串模板。这种用法已经和现阶段的脚本语言完全一致了。

    如果想引用一个表达式需要$后面跟花括号。

    val name = "jenny"
    print("size:${name.length}")
    

    访问字符串中的元素可以像访问数组一样。

    val name = "jenny"
    //访问首字母
    print("size:${name[0]}")
    

    如果像打印字符串中原始内容而不受转义字符、空格、回车的影响可以使用三引号(""")实现。

    val s: String = """
    for (a in "abc")
        print(a)
    """
    

    数组类型

    数组类为Array,可通过arrayOf方法创建一个数组。与Java不同的是,arrayOf可接收不同类型的元素,如果类型不同相当于Java中的Object类型的数组。

    //Int类型数组
    val array1 = arrayOf(1, 4, 5)
    //Any类型数组
    val array2 = arrayOf("1", 1, null)
    

    使用arrayOfNulls可创建指定大小的所有元素都为null的数组,但使用时需声明类型。

    //声明Int类型
    val xx = arrayOfNulls<Int>(9)
    

    默认数组的打印是数组类型,若想打印所有元素可以使用joinToString方法

    val array1 = arrayOf(1, 4, 5)
    println(array1.joinToString())
    //输出
    1, 4, 5
    

    获取类型

    若想获取一个Kotlin对象的类型可以使用符号::,如果想获取其Java类型则继续使用.java方法。
    由于获取类型使用Kotlin的反射,需额外依赖反射库。

    implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
    

    来个栗子:

    val any = Any()
    println(any)   //打印java.lang.Object
    println(any::class)  //打印class kotlin.Any
    println(any::class.java)  //打印class java.lang.Object
    
    //输出
    java.lang.Object@27c170f0
    class kotlin.Any
    class java.lang.Object
    

    另外::还能获取到方法的引用。

    //引用顶层函数println,依次打印数组元素
    arrayOf("a","b","c").forEach(::println)
    

    类型检查与转换

    使用is关键字检查类型,与之相反是!is,对应Java中的instanceof。不同的是,如果检查类型相符则不需要再进行强制转换。

    open class Person {
        fun work() {
        }
    }
    
    class Student: Person() {
        fun study() {
        }
    }
    
    val p: Person = Student()
    if(p is Student) {
        //类型检查通过后不需强转,并且可直接使用对应类型的方法
        p.study()
    }
    

    显式强制转换类型需使用as关键字,如果想避免类型转换异常需使用as?,如果强转失败会返回null。

    val p = Person()
    val student = p as Student //java.lang.ClassCastException
    //val student = p as? Student// 返回null
    println(student)
    

    可空类型

    Kotlin使用可空类型实现空安全,类型后跟?表示可空类型。

    //声明一个可空的Int类型变量
    val x: Int? = null
    

    反编译为Java代码可以发现,Kotlin中的Any变量,到Java中会使用@NotNull修饰,而Any?会被@Nullable修饰。

    我们先来看看null到底是什么类型。

    fun main(args: Array<String>) {
        println(null == null)  //打印true
        println(null != null)  //打印false
        println(null is Any)  //打印false
        println(null is Any?)  //打印true
    }
    

    可见null不是Any类型,而是Any?类型。



    空安全

    有了类型具体的可空/非空性,可大大缩减空指针出现的几率。

    Kotlin编译器在编译阶段对可空类型进行检查,防止程序在运行时发生空指针异常。

    我们访问可空类型对象的方法时需加?,如果此对象确实是null,则方法表达式结果最终返回null。

    val x: Int? = null
    val y = x?.toLong()
    println(y) //输出null
    

    在调用函数过程中可进行验空判断使用?:操作符,后面可以跟对象或表达式。

    val x: Int? = null
    val y = x?.toLong() ?: 0
    println(y)//打印0
    x?.toLong() ?: println("null ex")//打印 null ex
    

    有了这个操作符,原本Java语言中的验空代码就可以一行完成,更加简洁。

    //java验空
    String str = ...
    if(str != null) {
        str.toUpperCase();
    }
    
    //kotlin验空
    val str: String? = ...
    str?.toUpperCase()
    

    如果确认访问的对象一定不是空可使用!!操作符告诉编译器此处空指针的检查。

    val x: Int? = 3
    val y = x!!.toLong()//x 此时一定不是null
    println(y)
    

    真的就没有空指针了吗?

    在Kotlin的体系下看上去确实解决了空指针问题,但实际场景是项目中存在Kotlin和Java代码相互调用的场景。

    • Java模块使用Kotlin开发的library,反之同理。
    • 同一个模块同时混编java和Kotlin代码。

    一旦出现与Java的互操作则情况就变得复杂了。

    Java调用Kotlin

    我们先来看Java调用Kotlin代码。

    //java类
    public class TestMethod {
        public static void test() {
            Person p = new Person();
            p.work(null);//传入空 编译通过
        }
    }
    
    //kotlin类
    open class Person {
        //调用参数为不可空类型
        fun work(detail: String) {
            println("my work is $detail")
        }
    }
    
    @JvmStatic
    fun main(args: Array<String>) {
        //Exception in thread "main" java.lang.IllegalArgumentException: Parameter specified as non-null is null
        TestMethod.test()
    }
    

    可见在运行抛出了IllegalArgumentException异常。因为在Java环境并不会做Kotlin实参的空类型检查,而进入kotlin代码后会检查形参的可空性,当检查失败时抛出非法参数异常。

    为了弄清这个异常时如何抛出的,我们反编译了Person类的Kotlin代码。

    # 反编译后的java代码
    public class Person {
       public final void work(@NotNull String detail) {
          //参数检查
          Intrinsics.checkParameterIsNotNull(detail, "detail");
          String var2 = "my work is " + detail;
          System.out.println(var2);
       }
    }
    
    public static void checkParameterIsNotNull(Object value, String paramName) {
        if (value == null) {
            //若参数为空 抛出异常
            throwParameterIsNullException(paramName);
        }
    }
    
    private static void throwParameterIsNullException(String paramName) {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
    
        ...
        //非法参数异常在这里创建并最终抛出
        IllegalArgumentException exception =
                new IllegalArgumentException("Parameter specified as non-null is null: " +
                                             "method " + className + "." + methodName +
                                             ", parameter " + paramName);
        throw sanitizeStackTrace(exception);
    }
    

    Kotlin调用Java

    反过来,我们再来看看Kotlin调用Java类的例子。

    # Utils是一个Java工具类
    public class Utils {
        static String format(String text) {
            return text.isEmpty() ? null : text;
        }
    }
    

    使用Kotlin测试Utils类的format方法。

    @JvmStatic
    fun main(args: Array<String>) {
        doSomething("")
    }
    
    fun doSomething(text: String) {
        //call java method
        val f: String = Utils.format(text) //(1) IllegalStateException
        println ("f.len : " + f.length)
    }
    

    运行main函数会发现,代码(1)处会抛出IllegalStateException异常。因为这里试图将一个空类型赋值给一个不可空类型,然而这个异常在编译阶段是不会被检查的。

    解决办法是我们将f声明为可空类型。

    val f: String? = Utils.format(text)//运行正常
    

    但实际场景是我们经常使用类型推断,因而根本不会显式声明f的类型。

    fun doSomething(text: String) {
        //call java method
        val f = Utils.format(text)
        println ("f.len : " + f.length) //(2) NullPointerException
    }
    

    再次运行main函数,会发现出现我们最不想看到的空指针异常,异常发生在代码(2)处。由于没有显式声明f的类型,Kotlin通过format方法的返回值推断为String类型,且不会做参数的检查,当执行到f.length时便触发了空指针异常。

    我们看看反编译的Java代码加深理解。

    public final void doSomething(@NotNull String text) {
      Intrinsics.checkParameterIsNotNull(text, "text");
      String f = Utils.format(text);
      String var3 = "f.len : " + f.length();
      System.out.println(var3);
    }
    

    总结一下,Java与Kotlin的相互调用出现异常的原因。

    • Java调用Kotlin方法时并不检查实参的可空性。
    • Kotlin调用Java方法声明返回值类型时不具备类型推断能力。

    总结

    可见,当Java代码与Kotlin代码在项目中产生调用关系时,Kotlin的空安全特性可能引发一些不可期的异常。然而在现阶段这几乎不可避免,因为常见的第三方库几乎都是使用Java编写的,这也是Kotlin官方急于在新特性新功能方面优先支持Kotlin语言的一个重要原因。

    相关文章

      网友评论

        本文标题:Kotlin基本语法之(五)类型与空安全

        本文链接:https://www.haomeiwen.com/subject/govttctx.html