类型安全是编程中非常重要的内容,在实际编程中我们总是会遇到许许多多的空指针异常,类型错误等异常。因此解决好类型安全的问题可以大大提升程序的健壮性。同时Kotlin针对类型安全就引入了许多有用的机制,它将帮助我们走向防御型编程的道路。本篇文章就将介绍关于Kotlin类型安全的相关信息。
首先我们将先介绍Kotlin中的两个特殊类:Any类和Nothing类。
Kotlin中的所有类都继承于Any类,就是相当于Java中的Object类。Any类的最大目的不是让我们将变量,参数或者返回类型定义为Any类,而是提供了一些通用方法,比如像equals,hashCode和toString等像在Object类中定义的方法一样。
当然,Any类和Object类也不是完全相同的,Any通过拓展函数提供了一些特殊的方法。比如像to拓展函数,拓展函数的相关内容将在后面介绍。
在Java中,我们使用void表示方法不返回任何东西,但在Kotlin中,使用Unit来告诉我们什么时候函数没有返回任何有用的东西。那什么都没有返回的情况如何表示呢?Kotlin中用Nothing类来表示实际上一个函数什么也没有返回。
Nothing类没有实例,它表示一个永远不存在的值或者结果。当用作方法的返回类型时,意味着函数永远不会返回–函数调用只会导致异常。在实际编写时,我们也会发现无法返回Nothing的实例。
Nothing的一个独特能力是它可以代表任何东西,也就是说Nothing可以替代任何类,包括Int,Double,String等。比如说:
fun computeSqrt(n:Double):Double{
if(n >= 0){
return Math.sqrt(n)
}else{
throw java.lang.RuntimeException("Error!")
}
}
else部分的异常就用Nothing表示,为了帮助我们理解我们可以看看Nothing给出的提示:
翻译过来,大概就是在说:
凡事皆有实例。你可以用Nothing来表示“一个永远不存在的值”:例如,如果一个函数的返回类型是Nothing,这意味着它永远不会返回(总是抛出异常)。
总的来说,编译器可以确定if表达式的返回类型,在本例中为Double类型。因此,Nothing的唯一目的就是能够帮助编译器验证程序中类型的完整性是健全的。
在Java中,我们遇到的最头疼的问题应该就是空指针引用异常了,null是一种我们一直无法解决的问题。如果对null对象进行方法调用,就会导致空指针异常,因此在对可能为空的对象进行方法调用时,最安全的做法就是对该对象进行检查。
而在Kotlin中是不允许我们随便将null赋值给任意引用的,对于可能为null的引用,我们应该使用可空引用。根据引用是否为空,我们可以将引用分为普通引用和可空引用。普通引用是不能被赋值为null的,对应的,可空引用就允许我们将null赋值给它。
普通引用和可空引用的区别也很简单,就是可空引用在普通引用的后面加了一个?,我们看下面这个表:
可空引用 | 普通引用 |
---|---|
Int? | Int |
Double? | Double |
String? | String |
List< String >? | List< String > |
… | … |
具体也就是这么个规律,很简单。
前面提到可空类型的实际引用可能为null,那么在调用引用时就可能会出现空指针异常,所以在Kotlin中我们对可空类型进行方法调用时就需要先进行非空检查,否则编译器就会报错,比如:
fun nikeName(name:String?):String?{
//可空类型
if(name == "William"){
return "Bill"
}
return name.reversed()
}
这段代码就会报错,因为name为可空类型,对可空类型进行方法调用前要进行检查,比如我们要修改成这样:
fun nikeName(name:String?):String?{
//可空类型
if(name == "William"){
return "Bill"
}
if(name != null){
return name.reversed()
}
return null
}
这次在方法调用前我们进行了非空判断,这样就可以正确运行了。
像上面一样每次要调用非空引用的方法时都要进行非空判别的话就很麻烦,Kotlin中也提供了方便的运算符来实现上面的效果比如说:
if(name != null){
return name.reversed()
}
return null
我们就可以使用这一行来代替:
return name?.reversed()
这个?.就是安全运算符,该运算符会先判断可空引用是否为空,如果不为空就会接着调用后面的方法,否则会直接返回null,这就优雅了许多。
上面提到的安全运算符在引用为空的情况下会返回null,但是如果我们不想要返回null呢?这时就可以使用Elvis运算符了,?:就是Elvis运算符。我们来给出两段效果一致的代码来帮助我们理解这个运算符的效果:
val result = name?.reversed()?.toUpperCase()
return if(result == null) "Joker" else result
如果使用Elvis运算符是这样的:
return name?.reversed()?.toUpperCase() ?: "Joker"
可以说,我们使用安全调用运算符加上Elvis可以大大提升我们代码的健壮性同时又不失优雅。
除了上面两种安全的运算符之外,还有一个不太安全的断言运算符,同时也不太建议使用。断言运算符将强制使编译器停止检查,使得非空引用在不经过非空判断就可以进行方法调用,显然这种使用会导致空指针异常,也不太简易使用它,不过这里还是给出一个示例:
return name!!.reversed().toUpperCase()
这个!!就是断言运算符,这两个感叹号可能也说明了这个运算符的危险性。
有时候我们可能会想知道手头的对象是不是我们所期望的类型,这种情况下就要进行类型检查。在Java中我们会使用instanceof,而在Kotlin中我们使用is运算符。比如说:
override fun equals(other: Any?): Boolean {
return other is Animal //is 运算符,检查引用所指向对象是否属于特定类型 像InstanceOf
}
is运算符用于检查引用所指向的对象是否属于特定类型,如果是,则返回true,否则为false。我们也可以使用带否定的is,比如说!is。
最后is运算符还有一个特性,那就是智能转换,如果使用了is运算符或者!is运算符,那么就会自动进行类型转换。
具体来说,这个智能转换是和is运算符相关联的,当is运算符判定了一个引用是一个特定类型后,我们对该引用就不必再进行强制类型转化而是可以直接使用它。
比如说,我们有一个类为Cat,它有Int类型的age变量和String类型的name变量;同时我们还有一个Object类型的引用。一般来说,我们如果想把这个Object当做Cat来用的话是先用instanceof运算符,然后再将Object对象强制转化为Cat之后才能访问其成员变量,给出Java示例:
public void main(){
Object feakCat = new Cat(10,"Kitty");
if(feakCat instanceof Cat){
String name = ((Cat)feakCat).name;
int age = ((Cat)feakCat).age;
System.out.println("this cat's name is "+name+" ,and its age is "+age);
}
}
可以看到,即使已经判断了feakCat是Cat的实例,我们仍需要对其进行强制类型转化,而在Kotlin中,当is运算符判断出了feakCat的类型后,我们就会自动得到类型转化:
fun main() {
val feakCat:Any = Cat(10,"Kitty")
if(feakCat is Cat){
println("this cat's name is ${feakCat.name},and its age is ${feakCat.age}")
}
}
这显然就优雅多了。
Kotlin将在可能的情况下自动应用智能转换。如果它可以确定引用的类型是特定类型,那么他将允许你避免使用转换。同样,一旦Kotlin确定了一个对象引用不为空,他就可以用用智能转换,来自动将可空类型转换为不可空类型,从而再次省去了显示转换。
当然智能转换也不是万能的,在编译器不能确定类型时,就需要我们进行显示类型转换。Kotlin中提供了两个运算符来进行显示类型转换。分别是as和as?。
as运算符的作用就类似于Java中的强制类型转换,所以说它就有可能会转换出错,比如当被转换的类不是需要转成的类的子类时。
而as?运算符就是as运算符的安全版本,当转换成功时,它的效果与as运算符一致,而当转换出错时,它就会返回一个null引用,这样就不会爆出异常了。
在书中也给出了一些建议: