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

本小节是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语言的一个重要原因。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容