Kotlin学习笔记 – 空类型安全与智能类型转换

空类型安全与智能类型转换

空类型安全与智能类型转换,Java里面是没有的。

这是编译器层面实现的,不完全是代码层面。或者说,不完全是由IDE帮忙实现的,而是编译器本身就可以实现这两个新特性。

空类型安全

为了说清楚空类型安全,首先看一段Java代码。

public class NullUnsafeAndSmartCast {
    public static void main(String [] args){
        System.out.println(getNullString().length());
    }

    public static String getNullString(){
        return null;
    }
}

这里是写在同一个类的,而且固定返回了null。

实际应用中,这个get函数可能是别人写好的,在别的地方的,当返回正常字符串的时候,这个程序没有问题,但是返回null的时候,这个程序会Crash掉。

这个String有可能是null,所以这么做的话可就会抛空指针异常了。

snipaste_20170730_112056

snipaste_20170730_112108

所以我们就要特别判断一下,首先把这个String保留下来,看看是不是null,如果不是null,就输出他的长度,否则给出Prompt。

String s = getNullString();
if (s!=null)
    System.out.println(getNullString().length());
else
    System.out.println("Invalid!");

snipaste_20170730_112522

可是在Kotlin不用这么麻烦。

还是一个返回String的函数。

fun getNullString(): String{
    return "Null Unsafe"
}

这是没问题的。

但是当我要求返回null的时候,会报错,理由是,String是不可null的类型,你不能返回null。

snipaste_20170731_214734

Null can not be a value of a non-null type String

这个时候,就可以在main函数里面放心大胆地写length了。

fun main(args: Array<String>) {
    println(getNullString().length)
}

因为String是一个不可null的类型,如果返回null,肯定连编译都过不了。

如果多此一举去像Java一样判断,会发生什么呢?

IDE会告诉你,( getNullString() != null )这个表达式永远为true。或者写成判断相等的话,( getNullString() == null )永远为false。

snipaste_20170731_215107

那如果我就要返回null怎么办?

这时候就要用可null类型了,加个问号就可以了。

也就是说,『String』是不可null类型,『String?』是可null类型。

于是getNullString就编译通过了。

fun getNullString(): String?{
    return null
}

可是如果用了String的可null类型,main函数就没有办法通过了。

这里还不像是Java,如果String s是一个null,Java如果输出就变成null.length,抛出NullPointerException。

可是Kotlin,连编译都过不了。

snipaste_20170731_215525

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

意思是,只有空类型安全的类型(即不可null类型),或者是一个宣称了不可能是null的类型,才可以访问可null类型『String?』。

解决方法只有判断了,只有判断了getNullString不是null,才输出。

fun main(args: Array<String>) {
    val s = getNullString()
    if (s == null){
        println("Invalid")
    } else {
        println(s.length)
    }
}

这里注意一个细节,声明变量的时候,直接写的是

val s = getNullString()

而不是

val s: String = getNullString()

这是因为,如果直接写成 val s = , 那么s的类型将有编译器自动决定。

snipaste_20170731_220233

而写成 val s: String ,就是指明了s就是一个『String』,但是显然这里s不是一个『String』,而是一个『String?』。

(s不是一个String的不可null类型,而是一个String的可null类型。)

snipaste_20170731_220248

那么这样一比较,Kotlin比Java的优越性就不能体现出来了。

所以,上面这段代码其实还是可以继续简化的。

fun main(args: Array<String>) {
    val s = getNullString()
    println(s?.length)
}

这意思是,如果,s不为null,那么我就返回s的length,否则,就输出一个null。

非常安全,就算给一个null,也不会Crash。

在Java里面,还有一种很常见的操作,就是对参数进行判断,如果为null,就直接return,表示这个操作无效。

if (s == null) return
println(s?.length)

Kotlin里面这么写也没错,但其实没这么麻烦的。

IDE提示,可以简化。

snipaste_20170731_220750

val s = getNullString() ?: return

意思是,定义一个s的变量,它的值是getNullString()的返回值,如果这个返回值是null的话,就return。

那么,提问,这里的s,到底是『String』类型还是『String?』类型,也就是说,这里的String,到底是可null的,还是不可null的?

看一下,这句话的意思,如果是null,就return,换句话说,如果s被赋值了,就一定不会是null,既然一定不会是null,那么这是一个不可null的类型也不会有任何问题。

所以,这么写也是对的:

val s = getNullString() ?: return
val s: String = getNullString() ?: return

也正是这个原因,IDE又给出提示,说下面println的那句话,这个问号加得一点用都没有,这是Unnecessary的。

snipaste_20170731_221359

Unnecessary safe call on a non-null receiver of type String

于是,整个main函数就是

fun main(args: Array<String>) {
    val s: String = getNullString() ?: return
    println(s.length)
}

『?:』运算符,叫作elvis运算符,意思是对前面一个表达式判断一下是不是为空,如果为空,就执行后面一个表达式,否则的话,就继续执行下去就好了。

还有一种情况。

对于一个变量,是可null的类型,但是我知道它的值不可能是null。

val s: String? = "jxtxzzw"

如果只写

println(s.length)

编译是不过了的。

这时候,加上!!,告诉编译器,这个是宣称了不是null的,由我宣称的,你放心大胆去用吧。

println(s!!.length)

智能类型转换

还是先看Java。

在此之前建议先复习一下Upcasting和Downcasting。

定义一个Sup类,定义一个Sub类,Sub是继承自Sup的,也就是Sub是Sup的子类。

public class Sup {
}
public class Sub extends Sup{
    public String whoami(){
        return "Sub";
    }
}

Sub有一个方法,whoami,返回sub。

注意Sup没有这个方法。

也就是这个方法不是继承来的,是子类自己的。

这时候,如果写一个测试类。

public class TypeCast {
    public static void main(String[] args){
        Sup s = new Sub();
        System.out.println(s.whoami);
    }
}

子类的对象交给父类的变量,Upcasting,没有问题。

但是想要输出s.whoami,编译过不了,说,s没有whoami。

这时候,s看成的是Sup。

snipaste_20170731_222658

于是,只能强制类型转换了。

System.out.println(((Sub)s).whoami());

看上去很合理不是吗?

接下去我们写这样子的代码。

if (s instanceof Sub)

如果s是Sub类型的话,那么显然是有whoami()的,那么我去调用这个方法可不可以呢?

输入到一半,不提示错误,好像是可以的。

snipaste_20170731_222935

点回车。

发现『聪明』的IDE已经自动补全了。

snipaste_20170731_222941

又强转了一次。

是不是觉得有点傻——既然你都知道了s是Sub,你还强转什么呀。

同样的代码,在Kotlin里面写一下。

情况大不一样。

因为Java里面的东西是直接可以拿来用的,所以没有必要重新定义一个类了。

fun main(args: Array<String>) {
 val s: Sup = Sub()
}

还是一个父类的变量s,new一个子类的对象交给它。

这时候我去判断s是不是Sub,然后想看看有没有whoami。

snipaste_20170731_223321

snipaste_20170731_223328

有,而且不用强转。

这是因为,编译器早就知道了,只要能够进入if条件,那么s就是Sub,那就有whoami,这是智能类型转换。

snipaste_20170731_223436

Smart cast to ...

再拿空类型安全说说智能类型转换。

如果s是一个可null的类型,要输出length,IDE提示是给了,但是是灰的,如果点进去,会报错,过不了编译。

val s: String? = "Hello"
println(s.length)

snipaste_20170731_223618

snipaste_20170731_223733

放Java里还得做Cast。

但是Kotlin,只要判断是不是String就行了。

val s: String? = "Hello"
if (s is String)
println(s.length)

那么,『String?』类型(String的可null类型)的s,就被智能转换成了『String』(String的不可null类型)类型的s。

snipaste_20170731_223831

同样地,上面的if条件句判断是不是不等于null,也可以触发智能类型转换。

if (s != null)
    println(s.length)

还有一个也是Java没有的。

Java遇到类型转换的时候,经常会有类型转换异常,ClassCastException。

把刚才的代码改一下,就new一个Sup。

Sup s = new Sup();
System.out.println(((Sub)s).whoami());

那么做Downcasting一定会失败。

snipaste_20170731_224157

在Kotlin里面,同样会有类的转换异常的问题。

但是我们希望的是,出了异常,你不要给我Crash掉,你给我点信息,给个null也行啊。

val sp: Sup = Sup()
val sb: Sub? = sp as Sub

也是,就new一个父类的对象,交给sp。

然后让一个Sub类型的sb去接受一下。

sp as Sub,就是和Java一模一样的强转了。

如果转换失败的话,直接抛异常,这个跟Java一模一样。

亲眼见证一下ClassCastException被抛出来了。

snipaste_20170731_224621

如果这里的as后面加个问号,意思就是,如果转换失败了,不要给我抛异常,你给我弄一个null给我。

注意这里的Sub必须是可以为null的类型,即『Sub?』。

这时候sb就是null了,如果做println(sb),就会输出null。

简单小结一下空类型安全与智能类型转换,如下图:

snipaste_20170731_224836

snipaste_20170731_224850

snipaste_20170731_224856