0%

第5章:类型系统

null引用

null 做了哪些恶

  • null 存在歧义,一个值为null可以代表很多含义,比如:未初始化、值不合法、值不需要、值不存在

    一个典型的例子就是 HashMap 保存数据,Java中的HashMap允许 key 为null,比如一个教室,我们将座位号与坐在上面的人保存到HashMap中,在获取这些位置信息的时候,null就产生了歧义,到底是座位不存在还是座位没人。

  • 难以避免的 NPE

  • 冗余的防御使代码,各种判空

可空类型

Java8里面有Optional ,这个不作讨论。

Kotlin的可空类型

在Kotlin中,我们可以在任何类型后面加上 ? ,比如 “Int?”,实际等同于 “Int? = Int or null”

  1. 安全的调用 ?.

  2. Kotlin的Elvis操作符(也称为合并运算符) ?: ,比如,学生不戴眼镜,度数就为 -1,则可以类似如下表示:

val result = student.glasses?.degreeOfMyopia ?: -1

  1. 非空断言 !! ,有时候我们想确保一个学生是戴眼镜的,那么: val result = student!!.glasses

其中,关于第2点,Kotlin能够这样写,具体实现其实还是根据 if-else 去逐层判断,并没有什么魔法。这样判断的原因无非就是:兼容Java、性能考虑

类型检查

在经过is判断会后之后使用就无须强制转换,如下使用方法:

1
2
3
4
when(obj) {
is String -> print(obj.length)
else -> print("not a String")
}

智能类型转换

除了上述的智能转换,对于可空类型我们也可以使用 Smart Casts:

1
2
val stu: Student = Student(Glasses(189.00))
if(stu.glasses != null) println(stu.glasses.degreeOfMyopia)

不过,根据官方文档,当且仅当Kotlin编译器确定在类型检查后该变量不会再改变,才会产生Smart Casts。利用这点,我们能够确保多线程应用安全,举个例子:

1
2
3
4
5
6
7
8
9
10
class Kot {
var stu: Student? = getStu()
fun dealStu(){
if(stu != null) {
//还是不能这样写,编译器会因为可能在其他线程会修改该值有风险而报错,如果 stu 改成val 的就不会有这样的情况
print(stu.glasses)
}
}

}

当然,我们也能利用let来更优雅一点:

1
2
3
fun dealStu(){
stu?.let {print(it.glasses)}
}

在开发过程中,难免碰到类型转换,一般使用类似:

1
2
3
var stu: Student = getStu as Student?
//当然,也能使用以下方法,二者效果是一样的
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
2
3
val x1 = 18 //kotlin
int x2 = 18;//Java
Integer x3 = 18;//Java

但是我们观察 Kotlin 编译完成后的字节码可以发现,Kotlin中的Int在JVM中实际以int 存储(对应字节码类型为 I)。但是作为一个“包装类型”,Int编译后应该装箱才对,难道Kotlin不会自动装箱?其实可以再看看 Int? 的字节码,就可以得出结论:

  • Kotlin 中的 Int 类型等同于 int
  • Kotlin 中的 Int? 等同于 Integer !

Int 作为一种小技巧,让Int看起来是引用类型。

“新”的数组类型

Kotlin 中可以这样创造数组:

1
2
3
4
5
6
7
8
9
val funList = arrayOf()//声明长度为0的数组
val funList = arrayOf(n1,n2,n3,....nt)//声明并初始化长度t的数组

//由于Smart Casts ,编译器能够推出funList的元素类型。当然,我们也能手动指定:

val funList = arrayOf<T>(n1,n2,...nt)

//与array类似,我们可以这样定义原始类型的数组
val xArray = intArrayOf(1,2,3)

要注意的是,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
2
3
4
5
6
open class Fruit()
class Apple(): Fruit()
class Banana(): Fruit()

//定义一个水果盘子:
class FruitPlate<T: Fruit>()

上述的FruitPlate中T被限定了只能是 Fruit 类及其子类类型,其他类则不被允许。这种约束我们叫做上界约束,和Java的语法类似,只不过java 用extends关键字。假如我们要求水果盘子不一定装水果,有时候还能空着,那应该怎么办呢?我们可以在泛型参数类型后面加一个”?”即可:

1
class FruitPlate<T: Fruit?>(val t: T)

**如果,泛型有多个条件,怎么办?比如,有一把刀,只能用来切长在地上的水果,我们可以用 where 关键字这样实现:

1
2
3
4
5
6
7
interface Ground{}

class Watermelon(): Fruit(), Ground

fun <T> cut(t: T) where T: Fruit, T: Ground {
print("you can cut me")
}

这个where关键字就限定了多个条件,水果以及长在地上。

泛型的背后: 类型擦除

Java 为何无法声明一个泛型数组

我们看个简单的例子,Apple是Fruit的子类:

1
2
3
4
5
6
Apple[] appleArray = new Apple[10];
Fruit[] fruitArray = appleArray;//允许的
fruitArray[0] = new Banana();//编译通过,运行报错

List<Apple> appleList = new ArrayList<>();
List<Fruit> fruitList = appleList;//不允许

为什么数组可以这么做,而List就不行呢?关键的一点,数组是协变的,而List是不变的,换句话说,Object[] 是所有对象数组的父类,而 List 却不是 List的父类!我们知道Java中的List会类型擦除,具体表现如下:

1
2
3
4
5
6
println(appleArray.getClass());
println(appleList.getClass());

//运行结果
class [Ljavat.Apple;
class java.util.ArrayList

Kotlin与Java这个机制是一样的,也会存在类型擦除。但与Java不同的是,Kotlin中的数组是支持泛型的,当然也不再协变

1
2
val appleArray = arrayOfNulls<Apple>(3);
val anyArray: Array<Any?> = appleArray //不允许

这又是为什么呢?

向后兼容的罪

Java的list使用类型擦除是为了兼容老版本。既然类型擦除了,为什么我们在使用泛型的时候,能够进行类型检查,类型自动转呢?这是因为类型检查这些操作在编译器编译前就检查了,所以类型擦除不影响它。然后,我们可以发现,List 的get方法其实也是通过强制转换类型来实现的!

类型擦除的矛盾

通常情况使用泛型我们不在意它的擦除,但是在序列化/反序列化的时候,我们就需要知道类型了,咋办?既然编译后会擦除泛型参数类型,那么我们是不是可以主动指定参数类型来达到运行时获取泛型参数类型效果呢?看下例子:

1
2
3
4
5
6
7
8
9
10
11
//接着上面的Plate的代码写
open class Plate<T> (val t: T, val clazz: Class<T>) {
fun getType(){
print(clazz)
}
}

//使用
val applePlate = Plate(Apple(), Apple::class.java)

applePlate.getType()//会打印 class Apple

上述方法可以解决很多问题了,但是它无法获取泛型的类型,比如:

1
val type = ArraList<String>::class.java//不被允许

有没有其他方式呢?有,可以利用匿名内部类:

1
2
3
4
5
6
7
8
9
val list1 = ArraList<String>()
val list2 = object: ArrayList<String>(){}//匿名内部类

println(list1.javaClass.genericSuperclass)
println(list2.javaClass.genericSuperclass)

//结果
java.util.AbstractList<E>
javaUtil.ArrayList<java.lang.String>

竟然可以了,原理是啥呢?其实,泛型类型擦除并不是真的将全部的类型信息都擦除,还是会将类型信息放在对应 Class 的常量池的。所以,我们能通过相应的方式来获取这个类型信息,使用匿名内部类就可以实现这种需求。我们着手来设计一个能获取所有类型信息的泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
open class GenericsToken<T> {
var type: Type = Any::class.java

init {
val superClass = this.javaClass.genericSuperclass
type = (superClass as ParameterizedType).getActualTypeArguments()[0]
}
}

fun main(args: Array<String>) {
val gt = object: GenericsToken<Map<String,String>>(){}
print(gt.type)
}

//结果
java.util.map<java.lang.String, ?extends java.lang.String>

匿名内部类在初始化的时候就会绑定父类或者接口的相应信息,这样就能通过获取父类或者父接口的泛型类信息来实现我们的需求,你可以用这样一个类来获取任何泛型的类型,我们常用的Gson也是使用了相同的设计。比如,我们在Kotlin中可以这样使用Gson来进行泛型类的反序列化:

1
2
3
val json = ...
val rType = object: TypeToken<List<String>>(){}.type
val stringList = Gson.fromJson<List<String>>(json,tType)

使用内联函数获取泛型

其实,Kotlin中除了上述方法外,还可以通过内联函数实现。内联函数在编译的时候,便会将相应函数的字节码插入调用的地方,也就是说参数类型也会被插入字节码中。下面我们就用内联函数实现一个可以获取泛型参数的方法:

1
2
3
inline fun <reified T> getType() {
return T::class.java
}

非常简单,只需要加上 reified 关键词即可。所以,我们可以在Kotlin中改进Gson的使用方式:

1
2
3
4
5
6
inline fun <reified T: Any> Gson.fromJson(json: String): T {
return Gson().fromJson(json, T::class.java)
}

//使用
val list = Gson.fromJson<List<Stirng>>(json)

这里是对Gson进行了扩展,实现很优雅。注意,Java不支持内联函数,所以在Kotlin中声明的普通内联函数可以在Java中调用,被当做常规函数了;而用reified来实例化参数类型的内联函数不能在Java 中调用,因为它永远需要内联的

打破泛型不变

略吧,看迷糊了,下次补上

谢谢你的鼓励