null引用
null 做了哪些恶
null 存在歧义,一个值为null可以代表很多含义,比如:未初始化、值不合法、值不需要、值不存在
一个典型的例子就是 HashMap 保存数据,Java中的HashMap允许 key 为null,比如一个教室,我们将座位号与坐在上面的人保存到HashMap中,在获取这些位置信息的时候,null就产生了歧义,到底是座位不存在还是座位没人。
难以避免的 NPE
冗余的防御使代码,各种判空
可空类型
Java8里面有Optional ,这个不作讨论。
Kotlin的可空类型
在Kotlin中,我们可以在任何类型后面加上 ? ,比如 “Int?”,实际等同于 “Int? = Int or null”
安全的调用 ?.
Kotlin的Elvis操作符(也称为合并运算符) ?: ,比如,学生不戴眼镜,度数就为 -1,则可以类似如下表示:
val result = student.glasses?.degreeOfMyopia ?: -1
- 非空断言 !! ,有时候我们想确保一个学生是戴眼镜的,那么:
val result = student!!.glasses
其中,关于第2点,Kotlin能够这样写,具体实现其实还是根据 if-else 去逐层判断,并没有什么魔法。这样判断的原因无非就是:兼容Java、性能考虑
类型检查
在经过is判断会后之后使用就无须强制转换,如下使用方法:
1 | when(obj) { |
智能类型转换
除了上述的智能转换,对于可空类型我们也可以使用 Smart Casts:
1 | val stu: Student = Student(Glasses(189.00)) |
不过,根据官方文档,当且仅当Kotlin编译器确定在类型检查后该变量不会再改变,才会产生Smart Casts。利用这点,我们能够确保多线程应用安全,举个例子:
1 | class Kot { |
当然,我们也能利用let来更优雅一点:
1 | fun dealStu(){ |
在开发过程中,难免碰到类型转换,一般使用类似:
1 | var stu: Student = getStu as Student? |
除此之外,有些同学可能会认为需要频繁类型转换,所以会配合泛型封装一个“有效的”类型转换方法:
1 | fun <T> cast(original: Any): T? = original as? T |
使用上了 as? ,看起来没问题,那我们应该可以这样用:
1 | val ans = cast<String>(140163L) |
用法看起来也挺合理,但是在调用的时候,会抛出 Long cannot be cast to String 这样的异常。这其实是类型擦除的后果。Kotlin的设计者们同样注意到这点,加入了 reified关键字,可以理解为具体化,利用它我们可以在方法体内访问泛型指定的 JVM 对象(注意,还要在方法前加入 inline 修饰)。代码使用如下:
1 | inline fun <reified T> cast(original: Any): T? = original as? T |
比Java 更面向对象的设计
Java并不能真正意义上被称作一门“纯面向对象”的语言,因为它的原始类型(如int)的值与函数并不能视作对象。
Any: 非空类型的根类型
与Object 作为 Java类层级的顶层类似,Any类型是Kotlin中所有非空类型(如String、Int)的超类。与Java不同,Kotlin不区分原始类型和其他类型,Koltin中,所有类型的最终根类型都是Any。另外,Kotlin把Java方法参数和返回类型中用到的Object类型看做 Any ,当Kotlin函数中使用Any时,它会被编译成 Java字节码中的 Object。
Any?:所有类型的根类型
如果说Any是所有非空类型的根类型,那么 Any? 才是所有类型(可空和非空类型)的根类型。如果只有Java这门编程语言的经验,很容易陷入一个误区:继承关系决定父子类型关系。因为在Java中,类与类型大部分情况下都是“等价的”。
事实上,“继承”和“子类型化”是两个完全不同的概念。如Kotlin中的Int是Number的子类,那么在需要Number类型的地方传入 Int类型是没问题的。这是“继承”强调的“实现上的复用”。而子类型化是一种类型语义的关系,与实现没关系。虽然Any 与 Any? 看起来没有继承关系,然而在我们需要用 Any? 类型值的地方,显然可以传入一个类型为 Any 的值,反之却不然!
所以,我们可以大胆地说,Any? 是 Any 的父类型,而且是所有类型的根类型。
Any? 与 Any??。 你可能会问,那 Any??是不是 Any? 的父类型?如果成立,岂不是意味着没有所谓的所有类型的根类型了?其实,Kotlin的可空类型可以看做数学上的并集。如果用类型的并集表示 Any ,可以写成 Any U Null ,那么 Any?? 就可以写成 Any U Null U Null ,等价于 Any U Null ,即 Any??等价于 Any? 。
Nothing 与 Nothing?
顾名思义,Nothing是没有实例的类型。Kotlin 中 return、throw等(流程控制中与跳转相关的表达式)返回值都是 Nothing。Nothing 只能包含一个值:null,本质上与null没有区别,所以我们可以使用null作为任何可空类型的值。
自动装箱与拆箱
Kotlin 中没有 int、float、double、long这类的原始类型,取而代之的是它们对应的引用类型包装类: Int、Float、Double、Long。看起来让Kotlin比Java更加接近纯面向对象设计,但这样说其实是不够严谨的。
以Int 为例,虽然它可以像Integer 一样提供额外的操作函数,但这两个类型在底层实现上存在差异,看一段代码:
1 | val x1 = 18 //kotlin |
但是我们观察 Kotlin 编译完成后的字节码可以发现,Kotlin中的Int在JVM中实际以int 存储(对应字节码类型为 I)。但是作为一个“包装类型”,Int编译后应该装箱才对,难道Kotlin不会自动装箱?其实可以再看看 Int? 的字节码,就可以得出结论:
- Kotlin 中的 Int 类型等同于 int
- Kotlin 中的 Int? 等同于 Integer !
Int 作为一种小技巧,让Int看起来是引用类型。
“新”的数组类型
Kotlin 中可以这样创造数组:
1 | val funList = arrayOf()//声明长度为0的数组 |
要注意的是,IntArray等类型并不是 Array的子类。还有,Kotlin对原始类型的特殊优化,主要体现在避免了自动装箱带来的开销!
泛型: 让类型更安全
首先,Kotlin中也有泛型。
泛型:类型安全的利刃
大家都知道1.5以前的Java的List需要靠强制转换来取值的,并且可以存入各种各样类型的值,在编译期还发现不了,这就很难受了。所以后来就出了泛型,泛型主要优势有几点:
- 类型检查,在编译时就检查出错误。
- 更加语义化,List
便可以知道里面存储的是 String 类型。
自动类型转换,获取数据时不需要手动强转,更安全。 - 能写出更通用的代码。
Kotlin中使用泛型
首先,在Kotlin中,使用 val arrayListt = ArrayList()
这种方式是不允许的,但在Java中可以这么做,这是因为在Java中 1.5版本后才引入的,Java为了兼容可以这么做。但是,由于Kotlin具有类型推导能力,所以 val arrayListt = arrayListOf("one","two")
是允许的。
类型约束:设定类型上界
我们知道,泛型本身就有类型约束的作用,比如,你无法向一个String类型List中添加一个Double对象。那么这里说的是约束什么呢?用例子来看下,假设我们有一个盘子:
1 | class Plate<T>(val t: T) |
这个盘子类有一个泛型参数,表示可以接收各种东西,如水果或者主食。但是,如果有一天想把盘子归类,有些只能放水果,有些只能放菜,又该如何呢?还是看例子,我们可以定义一个水果类(Fruit),并声明 Apple 和 Banana 来继承它,并定义出水果盘子:
1 | open class Fruit() |
上述的FruitPlate中T被限定了只能是 Fruit 类及其子类类型,其他类则不被允许。这种约束我们叫做上界约束,和Java的语法类似,只不过java 用extends关键字。假如我们要求水果盘子不一定装水果,有时候还能空着,那应该怎么办呢?我们可以在泛型参数类型后面加一个”?”即可:
1 | class FruitPlate<T: Fruit?>(val t: T) |
**如果,泛型有多个条件,怎么办?比如,有一把刀,只能用来切长在地上的水果,我们可以用 where 关键字这样实现:
1 | interface Ground{} |
这个where关键字就限定了多个条件,水果以及长在地上。
泛型的背后: 类型擦除
Java 为何无法声明一个泛型数组
我们看个简单的例子,Apple是Fruit的子类:
1 | Apple[] appleArray = new Apple[10]; |
为什么数组可以这么做,而List就不行呢?关键的一点,数组是协变的,而List是不变的,换句话说,Object[] 是所有对象数组的父类,而 List