1.3 为什么要用Scala
Scala究竟是不是你的菜?这个问题需要你自己观察和判断。我们发现除了伸缩性,其实还有很多因素让人喜欢Scala编程。本节将介绍其中最重要的四点:兼容性、精简性、高级抽象和静态类型。
Scala是兼容的
从Java到Scala,Scala并不需要你从Java平台全身而退。它允许你对现有的代码增加价值(在现有基础之上“添砖加瓦”),这得益于它的设计目标就是与Java的无缝互调。[9]Scala程序会被编译成JVM字节码,其运行期性能通常也与Java程序相当。Scala代码可以调用Java方法,访问Java字段,从Java类继承,实现Java接口。要实现这些,并不需要特殊的语法、显式的接口描述或胶水代码(glue code)。事实上,几乎所有Scala代码都重度使用Java类库,而程序员们通常察觉不到这一点。
关于互操作性还有一点要说明,那就是Scala也重度复用了Java的类型。Scala的Int是用Java的基本类型int实现的,Float是用Java的float实现的,Boolean是用Java的boolean实现的,等等。Scala的数组也被映射成Java的数组。Scala还复用了Java类库中很多其他类型,比如,Scala的字符串字面量"abc"是一个java.lang.String,而抛出的异常也必须是java.lang.Throwable的子类。
Scala不仅会复用Java的类型,也会对Java原生的类型进行“再包装”,让这些类型更好用。比如,Scala的字符串支持toInt或toFloat这样的方法,可以将字符串转换成整数或浮点数。这样就可以写为str.toInt而不是Integer.parseInt(str)。如何在不打破互操作性的前提下实现呢?Java的String类当然没有toInt方法了!事实上,Scala对于此类因高级类库设计和互操作性之间的矛盾而产生的问题有一个非常通用的解决方案:Scala允许定义丰富的扩展,当代码中选中了(类型定义中)不存在的成员时,扩展方法的机制就会被启用。[10]在上述示例中,Scala首先在字符串的类型定义上查找toInt方法,而String类定义中并没有toInt这个成员(方法),不过它会找到一个将Java的String转换成Scala的StringOps类的隐式转换,StringOps类定义了这样一个成员(方法)。因此在真正执行toInt操作之前,上述隐式转换就会被应用。
我们也可以从Java中调用Scala的代码。具体的方式有时候比较微妙,因为就编程语言而言,Scala比Java的表达更丰富,所以Scala的某些高级特性需要加工后才能映射到Java。更多细节请参考《Scala高级编程》[11]。
Scala是精简的
Scala编写的程序通常都比较短。很多Scala程序员都表示,与Java相比,代码行数相差可达10倍之多。更为保守地估计,一个典型的Scala程序的代码行数应该只有用Java编写的、同样功能的程序的代码行数的一半。更少的代码不仅仅意味着打更少的字,也让阅读和理解代码更快,缺陷也更少。更少的代码行数,归功于如下几个因素。
首先,Scala的语法避免了Java程序中常见的一些样板(boilerplate)代码。比如,在Scala中分号是可选的,通常大家也不写分号。Scala的语法噪音更少还体现在其他几个方面,比如,可以比较一下分别用Java和Scala来编写类和构造方法。Java的类和构造方法通常类似这样:
而在Scala中,你可能更倾向于写成如下的样子:
对于这段代码,Scala解释器会生成带有两个私有实例变量(一个名称为index的Int和一个名称为name的String)和一个接收这两个变量初始值的参数的构造方法的类。这个构造方法会用传入的参数值来初始化它的两个实例变量。简单来说,用更少的代码做到了与Java本质上相同的功能。[12]Scala的类写起来更快,读起来更容易,而最重要的是,它比Java的类出错的可能性更小。
Scala的类型推断是让代码精简的另一个帮手。重复的类型信息可以被删除,这样代码就更加紧凑、可读。
不过可能最重要的因素是有些代码根本不用写,类库都帮你写好了。Scala提供了大量的工具来定义功能强大的类库,让你可以捕获那些公共的行为,并将它们抽象出来。例如,类库中各种类型的不同切面可以被分到不同的特质中,然后以各种灵活的方式组装、混合在一起。又如,类库的方法也可以接收用于描述具体操作的参数,这样一来,事实上你就可以定义自己的控制结构。综上所述,Scala让我们能够定义出抽象级别高,同时用起来又很灵活的类库。
Scala是高级的
程序员们一直都在应对不断增加的复杂度。要保持高效的产出,就必须理解当前处理的代码。许多走下坡路的软件项目都是因为受到过于复杂的代码的影响。不幸的是,重要的软件通常需求都比较复杂。这些复杂度并不能被简单地规避,必须对其进行妥善的管理。
Scala给你的帮助在于提升接口设计的抽象级别,让你更好地管理复杂度。举例来说,假设你有一个String类型的变量name,你想知道这个String是否包含大写字母。在Java 8之前,你可能会编写这样一段代码:
而在Scala中,你可以这样写:
Java代码将字符串当作低级别的实体,在循环中逐个字符地遍历。而Scala代码将同样的字符串当作更高级别的字符序列,用前提(predicate)来查询。很显然,Scala代码要短得多,并且(对于受过训练的双眼来说)更加易读。因此,Scala对整体复杂度预算的影响较小,让你犯错的机会也更少。
这里的前提_.isUpper是Scala的函数字面量。[13]它描述了一个接收字符作为入参(以下画线表示),判断该字符是否为大写字母的函数。[14]
Java 8引入了对lambda和流(stream)的支持,让你能够在Java中执行类似的操作。具体代码如下:
虽然与之前版本的Java相比有了长足的进步,但是Java 8的代码依然比Scala代码更啰唆。Java代码这种额外的“重”,以及Java长期以来形成的使用循环的传统,让广大Java程序员们虽然可以使用exists这样的新方法,但是最终都选择直接写循环,并安于这类更复杂代码的存在。
另一方面,Scala的函数字面量非常轻,因此经常被使用。随着对Scala的深入了解,你会找到越来越多的机会定义自己的控制抽象。你会发现,这种抽象让你避免了很多重复代码,让你的程序保持短小、清晰。
Scala是静态类型的
静态的类型系统根据变量和表达式所包含和计算的值的类型来对它们进行归类。Scala与其他语言相比,一个重要的特点是它拥有非常先进的静态类型系统。Scala不仅拥有与Java类似的、允许嵌套类的类型系统,还允许用泛型(generics)对类型进行参数化(parameterize),用交集(intersection)组合类型,以及用抽象类型(abstract type)隐藏类型的细节。[15]这些特性为我们构建和编写新的类型打下了坚实的基础,让我们可以设计出既安全又好用的接口。
如果你喜欢动态语言,如Perl、Python、Ruby或Groovy,那么也许会觉得奇怪,我们为什么把静态类型系统当作Scala的强项。毕竟,我们常听到有人说,没有静态类型检查是动态语言的一个主要优势。对静态类型最常见的批评是程序因此变得过于冗长繁复,让程序员不能自由地表达他们的意图,也无法实现对软件系统的某些特定的动态修改。不过,这些反对的声音并不是笼统地针对静态类型这个概念本身的,而是针对特定的类型系统的,人们觉得这些类型系统过于啰唆,或者过于死板。举例来说,Smalltalk的发明人Alan Kay曾经说过:“我并不是反对(静态)类型,但我并不知道哪个(静态)类型系统用起来不是一种折磨,因此我仍喜欢动态类型。”[16]
通过本书,我们希望让你相信Scala的类型系统并不是“折磨”。事实上,它很好地解决了静态类型的两个常见的痛点:通过类型推断规避了过于啰唆的问题,通过模式匹配,以及其他编写和组合类型的新方式避免了死板。扫清了这些障碍,大家就能更好地理解和接受静态类型系统的好处。其中包括:程序抽象的可验证属性、安全的重构和更好的文档。
程序抽象的可验证属性。静态类型系统可以证明某类运行期错误不可能发生。例如,它可以证明:布尔值不能和整数相加;私有变量不能从其所属的类之外被访问;函数调用时的入参个数不会错;字符串的集只能添加字符串。
目前的静态类型系统也有一些无法被检测到的错误。比如,不会自动终止的函数、数组越界或除数为0等。它也不能检查你的程序是不是满足它的规格说明书(假设确实有规格说明书的话)。有人据此认为静态类型系统实际上没什么用。他们说,既然这样的类型系统只能检测出简单的错误,而单元测试提供了更广的测试覆盖范围,为什么还要用静态类型系统检查呢?我们认为这些说法没有抓住问题的本质。虽然静态类型系统不可能完全取代单元测试,但是它能减小单元测试需要覆盖的范围,因为对于那些常规属性的检查,静态类型系统已经帮我们做了。不过,正如Edsger Dijkstra所说,测试只能证明错误存在,而不能证明没有错误。[17]因此,虽然静态类型带来的保障可能比较简单,但是这些是真正的保障,不是单元测试能够提供的。
安全的重构。静态类型系统提供了一个安全网,让你有十足的信心和把握对代码库进行修改。假设我们要对方法添加一个额外的参数,如果是静态类型语言,则可以执行修改,重新编译,然后简单地订正那些引起编译错误的代码行即可。一旦完成了这些修改和订正,我们就能确信所有需要改的地方都改好了。其他很多简单的重构也是如此,比如,修改方法名或者将方法从一个类移到另一个类。在所有这些场景里,静态类型检查足以确保新系统会像老系统那样运行起来。
更好的文档。静态类型是程序化的文档,解释器会检查其正确性。与普通的文档不同,类型标注永远不会过时(主要包含类型标注的源代码通过了编译)。不仅如此,解释器和集成开发环境(IDE)也可以利用类型标注来提供更好的上下文相关的帮助。比如,IDE可以通过对表达式的静态类型判断,查找该类型下的所有成员,将它们显示出来,供我们选择。
虽然静态类型通常对程序文档有用,但是有时候它的确比较烦人,让程序变得杂乱无章。通常来说,有用的文档是那些让读代码的人较难仅通过代码推断出来的部分。比如,下面这样的方法定义:
让读者知道f的参数是String,是有意义的。而在下面这个示例中,至少两组类型标记中的一组是多余的:
很显然,只需要说一次x是以Int为键,以String为值的HashMap就足够了,不需要重复两遍。
Scala拥有设计精良的类型推断系统,让你在绝大多数通常被认为冗余的地方省去类型标注或声明。在之前的示例中,如下两种写法也是等效的:
Scala的类型推断可以做得很极致。事实上,完全没有类型标注的Scala代码也并不少见。正因如此,Scala程序通常看上去有点像是用动态类型的脚本语言编写的。这一点对于业务代码来说尤其明显,因为业务代码通常都是将预先编写好的组件黏合在一起的;而对于类库组件来说就不那么适用了,因为这些组件通常都会利用那些相当精巧的类型机制来满足各种灵活的使用模式的需要。这是很自然的一件事。毕竟,构成可复用组件的接口定义的各个成员的类型签名必须被显式给出,因为这些类型签名构成了组件和组件使用者之间最基本的契约。