可空类型
在Kotlin中,我们可以在任何类型后面加上“?
”,比如“Int?
”,实际上等同于“Int? = Int or null
”。
通过合理的使用,不仅能够简化很多判空代码,还能够有效避免空指针异常。
注意:由于null
只能被存储在 Java 的引用类型的变量中,所以在 Kotlin 中基本数据的可空版本都会使用该类型的包装形式。同样,如果你用基本数据类型作为泛型类的类型参数,Kotlin同样会使用该类型的包装形式。(即可空类型会自动装箱)
Java中对于null
的一些解决方案:
- 函数内对于无效值,可以抛异常处理。
- 采用
@NotNull
/@Nullable
标注。 - 使用专门的
Optional
对象对可能为null
的变量进行装箱。
可空类型相关的安全操作符
安全的调用 ?.
s.student?.glasses?.degreeOfMyopia
Elvis操作符 ?:
val result = student.glasses?.degreeOfMyopia ?: -1
又称合并运算符
非空断言 !!
val result = student!!.glasses
类型检查
在Kotlin中,我们可以用“is
”来判断。
if (obj is String) {
print(obj.length)
}
if (obj !is String) {
// 等同于 !(obj is String)
print("Not a String")
} else {
print(obj.length)
}
when (obj) {
is String -> print(obj.length)
!is String -> print("Not a String")
}
类型智能转换
Smart Casts 可以将一个变量的类型转变为另一种类型,它是隐式完成的。
val stu: Any = Student(Glasses(189.00))
if(stu is Student) println(stu.glasses)
对于可空类型,我们可以使用 Smart Casts:
val stu: Student = Student(Glasses(189.00))
if (stu.glasses != null) println(stu.glasses.degreeOfMyopia)
我们将这个例子反编译成Java,核心代码如下
...
Intrinsics.checkParameterlsNotNull(args, "args");
Student stu = new Student(new Glasses(189.0D));
if (stu instanceof Student) {
Glasses var2 = ((Student)stu).getGlasses();
System.out.println(var2);
}
...
我们可以看到,这与我们写的Java版本一致,这其实是Kotlin的编译器帮我们做出了转换。
根据官方文档介绍:当且仅当Kotlin的编译器确定在类型检查后该变量不会再改变,才会产生SmartCasts。
利用这点,我们能确保多线程的应用足够安全。举个例子:
class Kot {
var stu: Student? = getStu()
fun dealStu() {
if (stu != null) {
print(stu.glasses)
}
}
}
上述代码中,我们将stu
声明为引用可空类型变量,这意味着在判断 stu != null
之后,stu
在其他线程中还是会被修改的,所以被编译器无情地拒绝了。
将var
改为val
就不会存在这样的问题,引用不可变能够确保程序运行不产生额外的副作用。你也许会觉得这样写不够优雅,我们可以用let
函数来简化⼀下:
class Kot {
var stu: Student? = getStu()
fun dealStu() {
stu?.let {
print(it.glasses) }
}
}
这样就会满足 Smart Casts 的条件,就不会被拒绝编译。
在实际开发中,我们并不总能满足 Smart Casts 的条件。并且 Smart Casts 有时会缺乏语义,并不适用于所有场景。
当类型需要强制转换时,我们可以利用“ as
” 操作符来实现。
class Kot {
val stu: Student? = getStu() as Student?
fun dealStu() {
if (stu != null) {
print(stu.classes)
}
}
}
由于val
只允许赋值一次,这样,我们在外部已经确定了stu
的类型,当stu
不为空时,在dealStu
方法里就可以成功调用stu
的参数。
注意:
- 这里
stu
变量是在dealStu()
方法的外面,必须使用val
生声明,否则如果是var
,仍然可能有线程安全问题所以也会被拒绝编译。 - 如果
stu
变量是放在dealStu()
方法的里面,那么可以使用var
, 因为是方法本地局部变量,会被线程独占,此时也满足 Smart Casts 的条件。
因为getStu
可能为空,如果我们将其转换类型改为 Student
:
val stu: Student? = getStu() as Student
则会抛出类型转换失败的异常,因为它不是可空的。所以,我们通常称之为“ 不安全” 的类型转换。
那是否有安全版本的转换呢?除了上述写法外,Kotlin还提供了操作符“ as?
”,我们可以这样改写:
val stu: Student? = getStu() as? Student
这时,如果stu
为空将不会抛出异常,而是返回转换结果null
。
Any:非空类型的根类型
与 Object
作为 Java 类层级结构的顶层类似,Any
类型是 Kotlin 中所有非空类型(如String
、Int
)的超类:
与 Java 不同的是,Kotlin 不区分“原始类型”(primitivetype)和其他的类型,它们都是同一类型层级结构的一部分。
如果定义了一个没有指定父类型的类型,则该类型将是Any
的直接子类型。如:
class Animal(val weight: Double)
如果你为定义的类型指定了父类型,则该父类型将是新类型的直接父类型,但是新类型的最终根类型为Any
。
另外,Kotlin 把 Java 方法参数和返回类型中用到的 Object
类型看作 Any
(更确切地说是当作“平台类型”)。当在 Kotlin 函数中使用 Any
时,它会被编译成 Java 字节码中的 Object
。
什么是平台类型?
平台类型本质上就是 Kotlin 不知道可空性信息的类型,所有 Java 引用类型在 Kotlin 中都表现为平台类型。当在 Kotlin 中处理平台类型的值的时候,它既可以被当作可空类型来处理,也可以被当作非空类型来操作。
平台类型的引入是 Kotlin 兼容 Java 时的一种权衡设计。试想下,如果所有来自 Java 的值都被看成非空,那么就容易写出比较危险的代码。反之,如果 Java 中的值都强制当作可空,则会导致大量的 null
检查。综合考量,平台类型是一种折中的设计方案。
Any?:所有类型的根类型
如果说Any
是所有非空类型的根类型,那么 Any?
才是所有类型(可空和非空类型)的根类型。
Any
与 Any?
看起来没有继承关系,然而在我们需要用 Any?
类型值的地方,显然可以传入一个类型为Any
的值,这在编译上不会产生问题。比如在 Kotlin 中 Int
是 Number
的子类:
fun printNum(num : Number){
println(num)
}
val n : Int = 1
printNum(n)
反之却不然,比如一个参数类型为Any
的函数,我们传入符合 Any?
类型的null
值,就会出现如下的错误:
error