也许Kotlin对Java程序员最有吸引力的特性是其类型系统中对于可空性的表示,这是Java和Kotlin“纹理”不同的另一个方面。
在Java 8之前,Java依靠约定、文档和直觉来判断引用是否可空,例如,从集合中返回一个项的方法,我们可以推断其能够返回null,但是像addressLine3这样一个字符串,在没有任何信息的情况下,我们是用null还是空字符串来表示呢?
多年来,作者和同事达成了一个约定,即假定Java中的引用为非空,除非另有标记,所以我们可以将字段命名为addressLine3OrNull,或者将一个方法命名为previousAddressOrNull。在单一代码库中,这种方法足够有效(虽然它有点冗长,并且需要永远保持警惕,以避免NullPointerException异常)。
一些代码库选择使用@Nullable和@NotNullable注解,通常由代码正确性检查工具支持。Java 8于2014年发布,增强了对注解的支持,使得Checker Framework( https://oreil.ly/qGYlH )等工具可以静态检查更多内容,而不仅仅是null安全性。不过,更重要的是Java 8还引入了标准的Optional类型。
如今,许多JVM开发人员已经涉足Scala,他们对Optional类型(在Scala的标准库中名为Option)带来的好处感到欣慰:当值可能缺失时,使用Optional类型;当值不可能缺失时,则使用普通引用。Oracle对此进行了干扰,它告诉开发人员不要将Optional用于字段或参数值上,但是就像Java 8中引入的其他特性一样,Optional已经足够好用,并被采纳为Java的主流用法。
不同时代的Java代码可能或多或少地采用上述策略来处理缺失。当然也有可能创建一个代码库几乎不会出现NullPointerException,但事实上,这是一项艰苦的工作。Java受到null的限制,并被其不够完善的Optional类型所困扰。
相比之下,Kotlin更加支持null,将可选性(optionality)作为类型系统而不是标准库的一部分意味着Kotlin代码库在处理缺失值方面具有令人振奋的统一性,但并非十全十美:如果key没有值,那么Map<K,V>.get(key)将返回null,但是List<T>.get(index)在index没有值时会抛出IndexOutOfBoundsException。同样,Iterable<T>.first()抛出NoSuchElementException而不是返回null。这些缺陷通常是由于Kotlin希望与Java向后兼容造成的。
对于如何安全地使用null来表示可选的属性、参数和返回值,Kotlin自己的API中通常都有很好的例子,我们可以研究它们来学习。在体验了作为头等公民的可空性之后,重新回到缺乏这种支持的语言时你会感到不安,你会敏锐地意识到,一直以来,你只是回避了NullPointerException,依赖于约定找到了穿过雷区的安全途径。
函数式程序员可能会建议你使用Optional(也称为 Maybe )类型,而不是Kotlin中的可空性。我们不建议这样做,尽管它提供了一种选项:使用相同的(一元表达式)工具来表示潜在的缺失、错误、异步等。在Kotlin中不使用Optional的一个原因是你将失去专为支持可空性而设计的语言特性。在这方面,Kotlin与Scala的“纹理”是不同的。不使用包装类型(Optional类)来表示可选性的另一个原因是微妙且至关重要的。在Kotlin的类型系统中,T是T?的子类型。假如你有一个不为空的字符串,你总是可以在需要可空字符串的地方使用它。然而,T不是Optional<T>的子类型,如果你有一个字符串并希望将它赋值给一个可选变量,你首先需要将它包装在一个Optional中。更糟糕的是,如果你有一个返回Optional<String>的函数,并且后来发现了一种始终有返回值的方法,将函数的返回类型更改为String将破坏所有的调用端代码。如果返回类型是可空的String?,那么你可以在保持兼容性的同时将其加强为String(非空的)。这一点同样适用于数据结构的属性:可以通过可空性轻松地从可选性迁移到非可选性,但具有讽刺意味的是,使用Optional则不行。
作者喜欢Kotlin对可空性的支持,并且已经学会如何用它来解决很多问题。让自己摆脱对null的回避需要一段时间,一旦达成,你会发现一个表达性方面的全新维度,供你探索和发掘。
Travelator中没有这个功能似乎令人可惜,让我们看看如何使用Optional将Java代码迁移到Kotlin和nullable。