Scala实用指南

978-7-115-48356-0
作者: [美] 文卡特·苏帕拉马尼亚姆(Venkat Subramaniam)
译者: 何品沈达
编辑: 杨海玲

图书目录:

详情

本书循序渐进地介绍了Scala编程语言的多个方面。本书共分为4个部分:第一部分详细介绍Scala的一些基础知识,并和Java中的相关概念进行了参照;第二部分进一步介绍Scala的一些中级知识,以及与Java的一些差异点;第三部分介绍在Scala中如何进行并发编程,并务实地介绍Akka套件;第四部分通过实战练习对前面的知识进行综合应用。

图书摘要

版权信息

书名:Scala实用指南

ISBN:978-7-115-48356-0

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [美] 文卡特•苏帕拉马尼亚姆(Venkat Subramaniam)

译    何 品 沈 达

责任编辑 杨海玲

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Copyright Ó2015 The Pragmatic Programmers, LLC. Original English language edition, entitled Pragmatic Scala: Create Expressive, Concise, and Scalable Applications.

Simplified Chinese-language edition Copyright Ó 2018 by Posts & Telecom Press. All rights reserved.

本书中文简体字版由The Pragmatic Programmers,LLC授权人民邮电出版社独家出版,未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。


本书是为想要快速学习或者正在学习Scala编程语言的Java开发者写的,循序渐进地介绍了Scala编程语言的多个方面。

本书共分为4个部分:第一部分详细介绍Scala的一些基础知识,并和Java中的相关概念进行了参照,方便读者快速上手Scala;第二部分进一步介绍Scala的一些中级知识,以及与Java的一些差异点,方便读者编写出更简洁的代码;第三部分介绍在Scala中如何进行并发编程,并务实地介绍Akka套件;第四部分通过实战练习对前面的知识进行综合应用,并系统地介绍如何与Java进行互操作。此外,附录部分还包括一些额外指引。

本书的目标读者是对JVM平台上的语言以及函数式编程感兴趣的程序员。阅读本书不需要读者熟悉Scala编程语言,但需要读者具备Java、面向对象编程的背景知识。因为本书以一种非常务实的方式组织内容,所以读者无法学到Scala的所有内容,但是足以应付日常工作,如果想要更全面地学习Scala以及其背后的一些设计理念,则最好辅以其他图书。


为Java程序员提供了节奏明朗、易于阅读和实用的Scala指南,涵盖了这一强大的多范式编程语言的多个重要方面,确保读者可以快速上手Scala,并变得富有生产力。

——Ramnivas Laddad,AspectJ in Action一书的作者,演说家和咨询师

在本书中,作者提供了坚实的基础,以帮助你在一本完整而简洁的书中学习Scala,你可以(也应该)从头到尾地阅读。书中探讨了Scala中所有你需要熟悉的最重要主题,从REPL开始介绍,然后讲到用Scala进行函数式编程、使用Actor处理并发以及与Java的互操作能力。你一定会迫不及待地打开自己的编辑器或者IDE,来探索本书中众多引人入胜的有趣例子!

——Scott Leberknight,Fortitude科技公司软件架构师

在充满了Twitter、博客和小视频的世界里,长篇大论依然还有一席之地。它给了老师足够的时间来引入具备挑战性且复杂的话题。而且,平心而论,Scala本身就是一个具备挑战性且复杂的话题。请借此机会,让Venkat带你熟悉Scala编程语言、函数式编程、并发、测试策略以及更多的主题吧。

——Brian Sletten,Bosatsu咨询公司总裁

我为我所有的Scala课程都推荐本书有两个原因:它轻松易读地涵盖了所有的Scala基础知识,并循序渐进地讨论了一些高级特性。对于任何学习Scala的人来说,这都是需要的一本书。

——Daniel Hinojosa,程序员、讲师、演说家、Testing in Scala一书的作者

我最喜欢Venkat的一点是,他可以通过对话的风格来介绍复杂的概念或者未知的话题,从读者熟悉的概念开始,循序渐进。这本书也不例外。我强烈推荐给任何想要学习Scala,尤其是有Java背景的读者。

——Ian Roughley,nToggle公司工程总监


接到同事沈达请我为本书作序的邀请时很惊喜,惊的是我近半年没使用Scala进行编程了,担心自己已经丢失了对最新Scala特性的跟踪,喜的是速读本书后我发现,本书竟是如此熟悉和容易上手,完全刷新了我对Scala各个特性的记忆,并使我有了使用Scala写一个小工具的冲动。

Scala是一门极简且高效的程序设计语言,也是一门复杂的语言,更是一门千人千面的语言。使用Scala可以进行面向对象的声明式编程,也可以进行函数式编程;可以进行业务代码的编制,也可以进行元程序的编制(定义程序的程序);可以开发大规模的服务应用,亦可进行类似shell的脚本编程;可以使用共享变量的并发编程模式,当然也可以采用基于Actor的消息机制的高并发编程模式。一个新人进入Scala的世界,学习和训练的路径有很多,而本书更多的是将语言最好的实践经验总结出来并呈现给读者,让读者能够高效简洁地使用Scala语言来实现复杂的功能。

本书对Scala各种特性进行了循序渐进的讲解,从Java引入(有Java基础的程序员会更容易入手),到使用Scala进行面向对象编程,进而介绍函数式编程的概念,随后讲解Scala最擅长的并发领域编程使用,从不可变性引出基于消息的Actor编程模型,最后通过Scala的实战讲解了如何编写单元测试。书中穿插了丰富的示例,令我惊讶的是某些部分使用了形象的图形去解释程序结构,非常通俗易懂。

早些年我在翻译FunctionalProgramminginScala一书的过程中,体会到函数式编程的强大,也体会到函数式编程的复杂,Scala标准库(如Collection)的编写过程践行了函数式编程的大量核心抽象,读者如果感兴趣可以自行查看。本书使用通俗方式和示例介绍了Scala函数式编程方面最实用的实现(如高阶函数、柯里化等),这些特性都对编写简洁、高效的程序十分有帮助,相信很多读者在日常工作中都会用到。

最后,Scala程序员是幸福的,因为Scala给了我们去编写优雅的代码的能力;Scala程序员也是辛苦的,因为Scala的学习曲线是有的,而且如何权衡和使用语言强大丰富的特性需要更多的实践和思考。

曹宝,挖财大数据负责人


我第一次接触Scala,还是在2012年,因为项目中用到了Play框架,好奇心驱使我要看一看源代码,我“不自量力”地打开一看才发自己完全看不懂。这种挫败感在今日依然记忆犹新。自此开始了我学习Scala并进一步了解Akka的过程,毕竟,我的初衷只是为了看懂Play的源代码而已。

我个人学习Scala的路线比较曲折。为了学习Scala,我又相继学习了Clojure、Haskell和Elixir等编程语言,虽然这些编程语言我都不算特别深入,但是的确对我学习Scala有莫大的帮助。因为Scala是一门多范式语言,并且非常灵活,其中的知识点也异常多,在看了多本相关图书,并做了不少动手练习之后,我才有点儿“初窥门径”的感觉。

在我熟悉了Scala,学习了Akka,并将它们应用到生产实践中之后,我才发现,实际上,很多“费脑”的代码在我们的日常业务开发中几乎不会用到,只是设计库的话可能会用得多一点,而这些认知,我当年并没有,以至于学习和练习了太多“无用”的技能,并极大地推迟了我体味Scala的“乐趣”的时间。随着编程经验的增多,以及在北京参加Scala线下聚会的讨论,我觉得,Scala不是难,而是很难,难在缺乏一本浅近易学、循序渐进的图书。社区有时候弥漫的风气会让你觉得:代码写得太平实,就不能表现出真正的实力。因此,无论国内还是国外,大家热衷分享的都是一些第一眼看过去不知所云,第二眼看过去竟会让你不知所措的代码片段。而我要说的是,这并非日常,也并不值得推崇。

Scala是一门简洁的高级编程语言,同时结合了面向对象编程(OOP)和函数式编程(FP)两种编程范式,Scala强大的静态类型系统和编译器,让我们可以在编写高性能的复杂应用程序时,提前避免错误的发生,而JVM、JavaScript以及Native的运行时又让我们可以“一次投入,多平台受益”。得益于Scala和Java的良好互操作性,我们可以方便自然地使用和编写用于JVM生态的库。随着Java 8以及Scala 2.12的发布,这样的便捷性已得到了进一步的增强。

我本打算写一系列叫作“Scala快车道”的书,让更多的人能够感受到使用Scala编程的高效和快乐。不过已经有了这本优秀、务实的入门图书,所以我就毛遂自荐来翻译了。我要特别感谢沈达,在翻译本书的过程中他比我付出得更多,并极大地提高了译稿的质量,当然还要感谢陈涛、张江锞、林炜翔、宋坤和周逸之等对本书进行的审阅,他们都怀着极大的热情帮助我们进一步提高了本书的质量。

我们把书中的代码都放在了GitHub上(https://github.com/ReactivePlatform/Pragmatic- Scala),这样方便大家下载和使用,默认使用的是原书文件夹的方式,而且还有一个名为sbt的分支,以方便大家在IDE中直接使用。我希望本书可以方便大家快速入门,并在项目中实践起来,同时适度地利用Scala的自由性和灵活性,编写简洁、平实和富有表现力的代码,让Scala更容易在团队之间交流,让更多人受益于这种简洁和表现力。

当然,我最应该感谢的是我的爱人和女儿们,感谢她们的体谅和支持,她们是我一切动力的来源。

何品

2018年3月于杭州


我是在开始学习Java的同时开始接触Scala的,在此之前饶有兴致地学过Scheme,也看过几章《Haskell趣学指南》,因此对Scala中的一些函数式编程的概念并不陌生。我喜欢Scheme那种简洁之美,但是很遗憾,使用Scheme构建应用程序往往缺砖少瓦,困难重重。而Haskell给人一种繁复艰深的感觉,阅读和编写Haskell代码的心智负担比较大。Scala是一门理想的语言,既满足了编程语言爱好者不灭的好奇心,又恰到好处地弥补了Java语言所缺失的简洁和表达力。得益于与Java良好的互操作,使用Scala可以站在Java庞大的生态之上,迅速构建出应用程序。

Scala的美在于精巧的内核,Scala的丑陋在于复杂的实现。作为程序员,我们不可能只品尝精巧的美而忽视复杂的丑陋。本书的长处在于克制,恰到好处地引导Java程序员进入Scala的世界,也指明了深入学习的路径。对已经熟悉Scala的程序员来说,本书也可以作为编写易读Scala代码的指南。Scala是多范式的,从实际工作的角度,我个人比较推崇编写贴近Java风格的Scala代码,并适度地利用Scala的语言特性简化代码,我认为这也是本书一以贯之的主题。

因为Java语言表现力有限,所以我们需要使用各种设计模式提高代码的抽象能力,固化编码逻辑。Scala这门语言在设计之初就借鉴了大量现存的语言特性,并吸取了许多设计模式中的精华,因此表现力非常强大。就我个人所了解的,Spark Catalyst源代码中利用抽象语法树的模式匹配做执行计划的优化,直观明了,大大降低了SQL执行计划优化器开发的门槛。很难想象,使用C或者C++,如何才能够编写出易于阅读、易于维护的等价实现。

Scala太灵活了,在学习的过程中难以避免会遇到不少艰深的小技巧,也会遇到各种陷阱。因此,一方面我们编码需要克制,另一方面我们需要加深自己对JVM上代码运行机制的理解。王宏江的博客是不可多得的学习资料,能够帮助我们拨开语言特性的迷雾,直击代码运行的本质。本书与其如出一辙,也有不少深入JVM字节码的分析,模仿这种分析方式,结合GitHub上的Scala标准库源代码,我们能够提升自己诊断问题的能力,加深对这门语言的理解。

本书诚如其名—实用。在内容的编排上,本书除了对语言本身的提炼,也同时介绍了Akka和单元测试,这对工程实践来说有极大的帮助。对还没有参与过真正工程开发的读者来说,掌握单元测试是必要的。在阅读大型开源项目的时候,从单元测试入手,可以窥得项目的设计轮廓和API完整的使用方法。越是优秀的开源项目,其单元测试越是完整、易读。在翻译本书的时候,个人还没有接触过Akka,审阅合译者翻译的本书第13章之后,我理解了Actor模型中隔离的可变性。在最近的工作中,这些知识和Akka的文档,帮助我发现了一个使用Akka的开源软件中对IO操作和Actor模型误用而导致的性能问题。

Scala官方也提供了Gitter的中文聊天室,贴代码比较方便,任何Scala相关的问题都可以在聊天室交流。我(@sadhen)和何品(@hepin1989)都在聊天室中。

最后,感谢在翻译过程中挖财诸位同事在工作上的帮助,也非常感谢我的领导曹宝开明的管理风格和一贯以来对技术好奇心和驱动力的鼓励。当然,也非常感谢合作译者何品大哥,何品大哥对技术的执着和热情、在开源社区的参与度、技术深度和流畅严谨的译笔,都深深地感染着我鼓励着我。

沈达

2018年3月于杭州城西


当我签约写这本书的时候,我对要面对的挑战知之甚少。由于颈部受伤,日常的例行工作都变得难以为继。在花了好几个月的时间恢复健康之后,我决定退出本书的编写工作。Pragmatic Programmers出版社没有以出版方的身份作出回应,而是以朋友和家人的身份来帮助我。我以前找的是出版社,现在我知道,我找到了真正的朋友。感谢Susannah Pfalzer、Dave Thomas、Andy Hunt以及所有其他帮助本书出版的团队成员。

我由衷地感谢本书的技术审稿人。感谢Scott Leberknight——他每次审我的书,我都收获颇丰。感谢Daniel Hinojosa、Rahul Kavale、Anand Krishnan、Ted Neward、Rebecca Parsons、Vahid Pazirandeh和Ian Roughley在本书出版过程中付出的宝贵时间和投入——我真心地感谢你们所做的一切。本书中任何的纰漏都是我的。

感谢本书还在预览状态时就购买本书的每个人。感谢David Dieulivol和Chris Searle提交勘误。

Jackie Carter的鼓励、支持、意见和建议使我获益匪浅。和她互动绝对轻松愉悦,并且极具指导力量。感谢你Jackie,感谢你所做的一切。你使编写本书的过程非常愉快。

如果没有我的妻子Kavitha以及儿子Karthik和Krupa的支持,我就不能完成这一切,感谢你们。


很高兴见到你对Scala感兴趣。感谢你选择本书来学习和练习这门编程语言,你将感受到在一种编程语言中融合面向对象和函数式编程这两种编程范式所带来的巨大优势。

Java生态系统是目前用于开发和部署企业级应用最强大的平台之一。Java平台几乎无所不在并且用途广泛;它类库丰富,可以在多种硬件上运行,并且衍生出了200多种基于此平台的编程语言。

我有幸学过并在工作中用过十几种编程语言,而且还为其中一些写过书。我觉得,编程语言就像各种型号的汽车——它们各执所长,帮助我们掌控平台的方向。现如今,程序员能够自由选择乃至混合使用多种编程语言完成应用程序,着实令人欣喜。

典型的企业级应用受困于各种问题——烦琐的代码难以维护,可变性增加了程序出错的可能,而共享的可变状态也让并发编程的乐趣变成了炼狱。我们一再深陷主流编程语言拙劣抽象能力的泥潭中。

Scala是编译成JVM字节码的最强大的编程语言之一[1]。它是静态类型的,简洁且富有表现力,而且它已经被各种组织用于开发高性能、具有伸缩性、即时响应性和回弹性的应用程序。

[1] JVM上主流的编程语言是Java、Scala、Clojure、Groovy和Kotlin。——译者注

这门编程语言引入了合理的特性并规避了一些陷阱。Scala及其类库让我们能够更多地关注问题领域,而不是陷入各种底层基础设施(如多线程与同步)实现细节的泥沼之中。

Scala被设计成用于创建需要高性能、迅速响应和更具回弹性的应用。大型企业和社交媒体需要对庞大的数据进行高频的处理,Scala正是为了满足这些需求而创造的。

Scala被用于在多个领域(包括电信、社交网络、语义网和数字资产管理)中构建应用程序。Apache Camel利用Scala灵活的DSL创建路由规则。Play和Lift是两个使用Scala构建的强大的Web开发框架。Akka则是一个用Scala构建的卓越类库,用于创建具有高即时响应性、并发性的反应式应用程序。这些类库和框架都充分利用了Scala的特性,如简洁性、表现力、模式匹配和并发。

Scala是一门强大的编程语言,但我们需要专注于Scala中最有价值的关键部分,才能通过它来获得生产效率。本书旨在帮助你学习Scala的精粹,让你高效产出,完成工作,并创建实用的应用程序。

Scala提供了两种不同的编程风格,以帮助你创建实用的应用程序。  

Scala并不拘泥于一种编程风格。我们可以面向对象编程,也可以使用函数式风格,甚至可以结合两者的优点将它们混合使用。

面向对象编程是Java程序员熟悉的舒适区。Scala是面向对象和静态类型的,并在这两方面都比Java走得更远。对于初学Scala的我们,这是个好消息,因为我们在面向对象编程上多年的投入不会浪费,而是化作宝贵的经验红利。在创建传统的应用程序时,我们可以倾向于使用Scala提供的面向对象风格。我们可以像使用Java那样编写代码,利用抽象、封装、继承尤其是多态的能力。与此同时,当这些能力无法满足需求时,我们也并不受限于这种编程模型。

函数式编程风格越来越受关注,而Scala也已支持这种风格。使用Scala,我们更容易趋向不可变性,创建纯函数,降低不可预期的复杂度,并且应用函数的组合和惰性求值(lazy evaluation)策略。在函数式风格的助益下,我们可以用Scala创建高性能的单线程和多线程应用程序。

Scala从其他编程语言(尤其是Erlang)中借鉴了许多特性。Scala中基于Actor的并发模型就深受在Erlang中大行其道的并发模型启发。类似地,Scala中的静态类型和类型推断(type inference)也是受到别的编程语言(如Haskell)的影响。其函数式编程的能力也是借鉴了一些函数式编程的先导者们的长处。

Java 8引入了lambda表达式和强大的Stream API后(可以参考Functional Programming in Java: Harnessing the Power of Java 8 Lambda Expressions[Sub14]一书),使用Java也能编写函数式风格的代码了。这对Scala或者JVM上的其他编程语言并不会是威胁,反而会缩小这些编程语言之间的隔阂,使程序员接纳或者在这些编程语言间切换变得更加轻松。

Scala能和Java生态系统无缝衔接,我们能够在Scala中使用Java类库了。我们可以完全使用Scala构建应用程序,也可以将其与Java以及JVM上的其他编程语言混合使用。于是,Scala代码既可以像脚本一样小巧,也可以像成熟的企业级应用一样庞大。

本书的目标读者是有经验的Java程序员。我假定读者了解Java语言的语法和API。我还假定读者有丰富的面向对象编程能力。这些假定能够保证读者可以快速习得Scala的精粹并将其运用于实际的应用程序之中。

已经熟悉其他编程语言的开发者也可以使用本书,但是最好辅以一些优秀的Java图书。

已经在一定程度上熟悉Scala的程序员可以使用本书学习那些他们还没有机会探索的语言特性。熟悉Scala的程序员可以使用本书在他们的组织中培训同事。

我写本书的目的是让读者能够在最短的时间内上手Scala,并使用它写出具有伸缩性、即时响应性和回弹性的应用。为了做到这一点,读者需要学习很多知识,但也有很多知识读者并不需要了解。如果读者的目的是想学习关于Scala编程语言的所有知识,那么总是会有一些知识无法在本书中找到。有其他一些关于Scala的图书在深度上做得很出色。读者在本书中学到的是那些必须了解的关键概念,目的是为了快速开始使用Scala。

我假定读者对Java相当熟悉。因此,读者无法在本书中学习到编程的基本概念。但是,我并没有假定读者已经了解了函数式编程或者Scala本身——这是读者将会在本书中学习的内容。

我写本书是为了那些忙碌的Java开发者,所以我的目的是让读者能够快速适应Scala,并尽早使用Scala来构建自己应用程序的一部分。读者将会看到书中的概念介绍节奏相当快,但是会附带大量示例。

学习一门编程语言的方式有很多,但没有比尝试示例代码(多多益善)更好的方式了。在阅读本书的同时,请键入示例代码,运行并观察结果,按照自己的思路修改它们、做各种实验、拆解并拼装代码。这将是最有趣的学习方式。

使用自动的脚本,本书中的代码示例使用下面的Scala版本运行过:

Welcome to Scala 2.12.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_172).

花几分钟时间为自己的系统下载合适版本的Scala。这有助于运行本书中的代码示例(从而避免Scala版本导致的细节困扰)。

读者可以从出版社网站[2]上本书的页面下载到所有示例代码。读者也可以提供反馈,直接提交勘误,或者在论坛上评论和提问。

下面是能够帮助读者开始阅读本书的若干网络资源:直接访问Scala的官方网站可以下载Scala。读者可以在其文档页面找到Scala标准库的文档。

让我们攀登Scala这座高峰吧。

[2] https://www.epubit.com


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供如下资源:

要获得以上配套资源,请在异步社区本书页面中点击 ,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/ selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


本书的第一部分将帮助Java程序员更加容易地适应Scala,读者将了解:


Scala是一门强大的编程语言:不需要牺牲强大的静态类型检查支持,就可以写出富有表现力而又简洁的代码。

你可以使用Scala构建任意应用程序,小至小工具,大至完整的企业级应用。你可以使用熟悉的面向对象风格编程,也可以随时切换到函数式风格。Scala并不会强迫开发人员使用唯一的风格编程,开发人员可以从自己熟悉的基础开始,并在适应后,利用更多其他特性,从而使自己变得更高产,使自己的程序更高效。

让我们快速探索Scala的一些特性,然后看一看用Scala写成的一个实用示例。

Scala是Scalable Language的简称,是一门混合了面向对象编程的函数式编程语言。它由Martin Odersky创造,并于2003年发布了第一个版本。下面是Scala的一些关键特性:

我们将在本书中细致地学习上面的每一个特性。

开始接触Scala时,你将会发现Scala与Java的第一个差异是,Scala能用更少的代码做更多的事情。你写的每一行代码都充溢着Scala简洁而强大的优点。你开始使用Scala的关键特性,熟读之后,这些特性便会让你的日常编程变得相当高效——Scala简化了日常编程。

让我们快速浏览一个示例,以了解Scala的强大和优势。在这个例子中,我们将会用到很多特性。即使此刻你并不熟悉Scala语法,也请在阅读的同时输入代码并编译运行。代码写得越多,熟悉得也就越快。

如果你还没有安装Scala,可参考附录A中的步骤。下面是第一个代码示例。

Introduction/TopStock.scala

1   val symbols = List("AMD", "AAPL", "AMZN", "IBM", "ORCL", "MSFT")
2   val year = 2017
3   
4   val (topStock, topPrice) =
5     symbols.map { ticker => (ticker, getYearEndClosingPrice(ticker, year)) }
6            .maxBy { stockPrice => stockPrice._2 }
7   
8   printf(s"Top stock of $year is $topStock closing at price $$$topPrice")

如果这是你第一次看到Scala代码,不要因语法分心,现阶段应专注于代码的整体结构。

这段代码从指定的股票代码列表中计算出股价最高者。让我们把这段代码拆开来逐步理解。

先看代码的主体部分。在第1行,symbols指向一个不可变的股票代码列表;在第2行,year是一个不可变的值;在第5行和第6行,使用了两个功能强大的专用迭代器——map()函数和maxBy()函数。在Java中,我们习惯用“方法”这个术语来指代类的成员,而“函数”这个术语通常用于指代不属于类的过程(procedure)。然而,在Scala中这两个术语可交换使用。

这两个迭代器分别行使了两种独立的职责。首先,我们使用map()函数遍历股票代码,以创建一个由股票代码及其2017年收盘价格组成的“对”或“元组”为元素的列表。最终结果的元组列表形式为List((股票代码1,价格1),(股票代码2,价格2),...)

第二个迭代器处理第一个迭代器的结果。maxBy()函数是一个从列表中取出最大值的专用迭代器。因为该列表中的值是元组(对),所以我们需要告诉maxBy()函数如何比较两个值。在maxBy()函数附带的代码块中,我们指定了一个包含两个元素的元组,我们感兴趣的是第二个属性(代码块中的_2)——价格。这段代码十分简洁,但却做了不少事情。图1-1将这些动作进行了可视化。

图1-1

如图1-1所示,map()函数将指定的函数或者操作(在这里是获取价格)应用到每一个股票代码上,并创建一个以股票代码及其价格为元素的结果列表;然后maxBy()函数在结果列表上计算得到价格最高的股票代码。

上述代码没有给出getYearEndClosingPrice()函数,接下来我们来看一看它。

Introduction/TopStock.scala

case class Record(year: Int, month: Int, date: Int, closePrice: BigDecimal)

def getYearEndClosingPrice(symbol: String, year: Int): BigDecimal = {
  val url = s"https://raw.githubusercontent.com/ReactivePlatform/" +
          s"Pragmatic-Scala-StaticResources/master/src/main/resources/" +
          s"stocks/daily/daily_$symbol.csv"

  val data = io.Source.fromURL(url).mkString
  val maxClosePrize = data.split("\n")
    .filter(record => record.startsWith(s"$year-12"))
    .map(record => {
      val Array(timestamp, open, high, low, close, volume) = record.split(",")
      val Array(year, month, date) = timestamp.split("-")
      Record(year.toInt, month.toInt, date.toInt, BigDecimal(close.trim))
    })
    .sortBy(_.date)(Ordering[Int].reverse)
    .take(1)
    .map(_.closePrice)
    .head
   maxClosePrize
}

即使你现在还不熟悉语法,这段代码也应该很容易阅读。在这个简短而亲切的函数中,我们向Web服务发送请求,并收到CSV格式的股票数据。然后我们解析这些数据,提取并返回年终收盘价。现在不用在意接收到的数据的格式,它对我们在这里所关注的焦点而言并不重要。在第15章中,我们将重温这个例子,并提供与Web服务通信相关的所有细节。

要运行前面的例子,可以将上述两段代码保存到一个名为TopStock.scala的文件中,并且使用以下命令:

scala TopStock.scala

将会看到这样的结果:

Top stock of 2017 is AMZN closing at price $1169.4700

花几分钟时间研读这段代码,以确保自己了解这是如何工作的。在研究代码的过程中,要查看该方法是如何做到计算出最高价格而又无须显式更改任何变量或对象的。这整段代码完全只处理不可变的状态,一旦创建,没有变量或对象会被更改。因此,如果你要并行运行这段代码,不必担心任何同步和数据竞争问题。

我们已经从网络上获取了数据,做了一些比较,并产生了所需的结果——这是非常重要的工作,但它只需要几行代码。即使我们新增一些需求,这段Scala代码还是能保持简洁且富有表现力。让我们来看一看。

在这个例子中,我们从Web获取每个股票代码的数据,这涉及多次访问网络的调用。假设网络延迟是d秒,而我们要分析n支股票,那么顺序代码大概需要n × d秒。因为代码中最大的延迟在于访问网络来获取数据,所以如果我们并行地执行代码以获取不同股票代码的数据,那么我们可以将时间缩短到大约d秒。Scala使得将顺序代码改成并行模式变得很简单,只需一个很小的改动:

symbols.par.map { ticker => (ticker, getYearEndClosingPrice(ticker, year)) }
       .maxBy { stockPrice => stockPrice._2 }

我们插入了对par的调用,就是这么简单。这段代码现在已经是在并行地处理每一个股票代码,而不是顺序迭代。

我们来强调一下这个例子的一些优点。

这些优点已经让我们肩头的负担减轻不少。例如,我们几乎不费吹灰之力就将代码并发化了。有关线程多么令人头疼的详尽论述,参考Brian Goetz的Java Concurrency in Practice[1][Goe06]一书。使用Scala可以专注于应用程序逻辑,而不用关心底层。

我们看到了Scala在并发上的优势。与此同时,Scala也为单线程应用提供了诸多便利。Scala赋予了我们自由,让我们可以随心选择或者同时混合使用命令式风格和无赋值操作的纯函数式风格。在Scala中,有了混用这两种风格的能力,我们就可以在单线程作用域中使用最合适的风格。而对于多线程或并发安全问题,我们倾向于使用函数式风格。

Java中的原始类型在Scala中被看作对象。例如,2.toString()在Java中将产生编译错误,但在Scala中是有效的——我们在Int的实例上调用了toString()方法。同时,为了提供良好的性能以及与Java互操作能力,在字节码级别上,Scala将Int的实例映射到int的表示上。

Scala编译成了字节码,这样我们就可以使用运行Java程序的方式来运行Scala程序,也可以用脚本的方式运行它。Scala也可以很好地与Java互操作。我们可以从Scala类扩展出Java类,反之亦然。我们也可以在Scala中使用Java类,或者在Java中使用Scala类。我们甚至可以混合使用多种编程语言[2],成为真正的多编程语言程序员。

Scala是一门静态类型的编程语言,但与Java不同,它的静态类型更加合理——Scala会尽可能地使用类型推断。我们可以依靠Scala本身来推断出类型,并将结果类型应用到其余代码中,而不是重复又冗余地指定类型。我们不应该为编译器工作,而应该让编译器为我们工作。例如,当我们定义var i = 1时,Scala将立即推断出变量i的类型为Int。如果此时我们尝试将一个字符串赋值给这个变量,如i = "haha",那么Scala就会抛出错误信息,如下所示:

sample.scala:2:
error: type mismatch;
 found   : String("haha")
 required: Int
i = "haha" // 编译错误
    ^
one error found

在本书后面,我们将看到类型推断是如何在这种简单定义以及函数参数和返回值上起作用的。

Scala提倡简洁。在语句结尾放置一个分号是Java程序员的习惯。而Scala解放了程序员的右手小拇指——句末分号在Scala中是可选的。而且,这只是一个开始。在Scala中,根据上下文,点操作符(.)和括号都是可选的。因此,我们可以编写s1 equals s2来替代s1.equals(s2)。通过去除分号、点号和括号,代码获得了较高的信噪比,使得编写领域特定语言变得更加容易。

Scala最有趣的特点之一是伸缩性。我们可以享受到函数式编程构造与功能强大的库之间的良好互操作性,以创建高度伸缩性的并发应用程序,并充分利用多核处理器上的多线程能力。

Scala真正的美在于它的精简。与Java、C#和C++相比,Scala中内置的核心规则十分精简。剩下的部分,包括操作符,都属于Scala标准库。这种区别影响深远。因为Scala本身做得越少,所以我们就可以发挥越多。它具有极佳的扩展性,它的标准库就是一个样例。

这一节中的代码展示了,我们仅用几行代码就可以完成许多任务。这种简洁性部分来源于函数式编程的声明式风格——接下来让我们对此做更进一步的研究。

函数式编程(Functional programming,FP)已经存在了数十年,但是它终于获得了很大的关注。如果你主要使用面向对象编程(Object Oriented Programming,OOP),那么你得付出一些努力才能适应函数式编程,而Scala能将这种负担减轻不少。

Scala本质上是一门混合型编程语言,我们既可以使用命令式风格也可以使用函数式风格,这是把双刃剑。其优点在于,当使用Scala编写代码时,我们可以先使其工作,然后再做优化。对于刚刚接触函数式编程的程序员,他们可以先用命令式风格写好代码,然后再将代码重构成函数式风格。另外,如果一个特定的算法使用命令式风格实现真的比较好,那么很容易就可以用Scala编写出来。然而,如果团队滥用这种灵活性,随意组合编程范式的话,那么这种灵活性就可能会变成一种诅咒。首席开发人员的仔细监督对于帮助维护一种适合团队的一致编程风格可能是必要的。

函数式编程提倡不可变性、高阶函数和函数组合。这些特性合在一起就能使代码简洁、富有表现力、易于理解和修改。不可变性还有助于减少那些由于状态改变而悄然滋生的错误。

让我们花几分钟,通过和命令式风格的代码对比来获得对函数式编程的感觉。

下面是一段命令式风格的Java代码[3],用于从给定日期开始的一系列温度中计算出最大值:

// Java代码
public static int findMax(List<Integer> temperatures) { 
  int highTemperature = Integer.MIN_VALUE;
  for(int temperature : temperatures) {
    highTemperature = Math.max(highTemperature, temperature);
  }
  return highTemperature;
}

Scala也支持命令式风格,下面是Scala版本的代码。

Introduction/FindMaxImperative.scala

def findMax(temperatures: List[Int]) = {
  var highTemperature = Integer.MIN_VALUE
  for (temperature <- temperatures) {
    highTemperature = Math.max(highTemperature, temperature)
  }
  highTemperature
}

我们创建了可变变量highTemperature,并在循环中持续修改它。我们必须确保正确地初始化可变变量,并在正确的地方把它们修改为正确的值。

函数式编程是一种声明式风格,我们只要指定做什么而不用指定如何去做。XSLT、规则引擎和ANTLR这些工具都普遍使用声明式风格。让我们把前面的代码用不带可变参数的函数式风格重写一下。

Introduction/FindMaxFunctional.scala

def findMax(temperatures: List[Int]) = {
  temperatures.foldLeft(Integer.MIN_VALUE) { Math.max }
}

上面的代码体现了Scala的简洁性和函数式编程风格。

我们创建了一个以不可变温度值集合作为参数的函数findMax()。在括号和左大括号之间的=告诉Scala去推断这个函数的返回类型,在本例中是Int

在这个函数中,集合的foldLeft()方法在集合的每一个元素上应用函数Math.max()java.lang.Math类的max()方法接受两个参数,并算出两者中的较大者。这两个参数在前面的代码中被隐式传递。max()方法的第一个隐式参数是前一次计算出的最高值,而第二个参数是集合在被foldLeft()方法遍历时的当前元素。foldLeft()方法取出调用max()方法的结果,也就是当前最高值,并将它传递到下一次对max()方法的调用中,以便和下一个元素进行比较。foldLeft()方法的参数是最高温度的初始值。

图1-2用一些温度的样本值帮助我们将这个例子中findMax()函数的作用机制可视化。

图1-2展示了findMax()函数调用foldLeft()方法并作用到temperatures列表上的过程。foldLeft()方法首先将给定的函数Math.max()作用于初始值Integer.MIN_VALUE和列表中的第一个元素23。两者中的较大者23将会和列表中的第二个值一起被传入Math.max()方法。这次计算的结果是27,也就是两者中的较大值,它将和列表中的最后一个值相比较,还是使用Math.max()方法。在方框中的这些操作序列就是foldLeft()方法的内部作用机制。findMax()方法最终将返回foldLeft()方法产生的结果。

图1-2

这个示例中的代码相当密集,需要花几分钟深入学习。

foldLeft()方法需要费一些气力才能掌握——让我们通过另一个心算练习来理解它。做一个一分钟的假设,把集合中的元素看成排成一行的人,我们要算出其中最年长者的年龄。我们在一张纸条上写上0并交给该行上的第一个人。第一个人扔掉纸条(因为他的年龄比0大),并在一个新纸条上写下他的年龄20,然后把纸条传递给下一个人。第二个人的年龄比20小,所以他只需把纸条传递给下一个人。第三个人的年龄是32,他扔掉纸条并新写一张继续传递下去。我们从该行上最后那个人的纸条上获得的就是最年长者的年龄。将这个计算动作序列可视化将有助于了解foldLeft()内部的作用机制。

看前面代码的感觉就像把红牛一饮而尽。Scala代码高度简洁而紧凑,你必须花费一些精力来学习。一旦这样做了,你就能从它强大的功能和丰富的表现力中受益。

我们来看一看函数式风格的另一个例子。假设我们想要一个列表,其元素是原始列表中的值的两倍。要实现这个功能,我们只需发出让所有元素都翻倍的指令,让Scala自己做循环即可,而不是循环遍历每一个元素,如下所示。

Introduction/DoubleValues.scala

val values = List(1, 2, 3, 4, 5)

val doubleValues = values.map(_ * 2)

关键字val可读作不可变的(immutable)。它告诉Scala,变量valuesdoubleValues一旦创建就不能更改。

虽然看起来不像一个函数,但是_ * 2就是一个函数。它是一个匿名函数(anonymous function),也就是说一个只有主体但没有名字的函数。下划线(_)表示传递给此函数的参数。函数本身作为参数值传递给了map()函数。map()函数迭代遍历集合,并把集合中的每一个元素作为参数值来调用匿名函数。总体的结果就是:一个由元素值是原始列表中元素值的两倍的元素所组成的新列表。

函数是Scala中的一等公民,这也解释了为什么我们能够把函数当作常规的参数和变量,在这个例子中,函数就是求一个数的两倍。

虽然我们获得了一个列表,其中元素的值都是原始列表中元素值的两倍,但是我们并没有修改任何变量或对象。这种不可变的方式是让函数式编程成为并发编程的理想编程风格的关键。在函数式编程中,函数是纯的,它们产生的输出只依赖于它们所接收到的输入,并且它们不会影响任何全局和局部变量的状态,也不会受任何全局或局部的状态影响。

编程中采用不可变性有明显的好处,但是复制对象而不是改变它们难道不会导致性能糟糕并增加内存使用吗?如果不小心,确实会这样。但是Scala依赖于一些特殊的数据结构来提供良好的性能和高效的内存使用[4]。例如,Scala列表是不可变的,因此复制一个列表以在列表的头部增加一个额外的元素将复用已经存在的列表。因此,复制一个列表以将元素插入到列表的开头,在时间复杂度和空间复杂度上都是O(1)。同样,Scala的Vector用一种名为Tries的特殊不可变数据结构实现。在设计上,它就能够高效复制集合,以常数级的时间复杂度和空间复杂度来改变集合中的任意元素。

通过本章中的几个示例,我们得以一窥Scala高度简洁和富有表现力的本质。这些简短的例子以走马观花的方式介绍了几个特性,包括函数式风格、易用的并发、集合的使用、酷炫的迭代器、不可变性编程、元组的使用。我们学习了变量和值的定义、静态类型检查以及类型推断。最重要的是,我们也看到了Scala是多么简洁且富有表现力。

我们已经快速接触了一些概念,在本书的其余部分,我们将对这其中的每一个概念都做更加深入的探讨。在下一章中我们将开始编译并运行Scala代码。

[1] 中文版书名为《Java并发编程实战》。——译者注

[2] 这里指使用多种JVM上的编程语言,如Kotlin、Groovy和Eta等。——译者注

[3] 如果使用Java 8的 Stream API将会优雅得多,这里主要是为了对照。——译者注

[4] 这里指的是持久化数据结构。——译者注


令人惊喜的是,无论是创建一个简短的脚本还是一个完整的企业级应用,都可以轻松地用Scala代码实现并运行。你可以使用任何IDE,也可以只使用轻量级的编辑器。

在本章中,我们将学习如何从命令行快速运行Scala脚本以及如何编译包含Scala代码的多个文件。如果你想探究Scala的运行机制,例如,推断出来的变量类型是什么,那么你随时可以快速跳入REPL,Scala将以交互的方式显示有用的细节。没有比通过尝试一些例子来学习Scala更好的方法了,所以请在阅读的同时输入代码并运行。让我们开始使用这个最有意思的交互式工具——REPL。

相当多的编程语言都提供了REPL(read-eval-print loop)工具,使用REPL可以便捷地键入代码片段,并以交互方式立即看到代码运行结果。除了执行代码片段外,REPL往往还提供一些在运行时不方便获取的细节。这使得REPL成为一个特殊工具,可以用来做试验,也可以用来学习Scala推断变量或函数类型的方法。

名为scala[1]的命令就是Scala的REPL,也是尝试这种编程语言最快捷的方式。使用这个工具我们就可以开始把玩小巧的代码片段。它不仅仅是一个学习工具,在大型应用的开发中也非常有用。你可以在REPL中快速尝试一些代码原型,然后使用世界上最好的技术——复制和粘贴——从REPL复制代码到自己的应用程序中。

要启动REPL,应在命令行(在终端窗口或命令提示符下)键入scala。启动后会打印出一些介绍信息,紧跟着一个提示符:

Welcome to Scala 2.12.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_172).
Type in expressions for evaluation. Or try :help.

scala>

在提示符下,键入val number = 6,然后按下回车键。Scala shell会响应,表明它根据赋给它的值6推断出变量numberInt类型的:

scala> val number = 6
number: Int = 6

现在尝试重新给number赋值,Scala会反馈如下错误:

scala> number = 7
<console>:11: error: reassignment to val
      number = 7
             ^

Scala提示说不能对不可变变量number进行重新赋值。但是在控制台中,可以重新定义不可变变量和可变变量。例如,shell会默默地接受以下内容:

scala> val number = 7
number: Int = 7

在同一个作用域中[2]重新定义不可变变量和可变变量只在交互式的shell中可行,在真实的Scala代码和脚本中行不通——这种灵活性使在shell中做试验很方便,同时,在应用程序代码中规避错误。

我们已经看到Scala如何推断出类型为Int。让我们看另外一个例子,在本例中变量的类型被推断为List

scala> val list = List(1, 2, 3)
list: List[Int] = List(1, 2, 3)

在编写应用程序代码的任何时候,如果不确定表达式会被推断成什么(类型),都可以快速在shell中尝试。

在shell中,使用向上箭头可以唤出上一次键入的命令。我们甚至可以在shell中找回之前启动shell时使用过的命令。

在输入一行命令时,按Ctrl+A可以转到行首,按Ctrl+E可以转到行尾。

只要收到回车键,shell就会执行所输入的内容。如果没有完整输入然后按下回车键,例如,在写一个方法定义的过程中,shell就会显示竖线(|)提示输入更多代码。例如,让我们在两行上定义一个方法isPalindrome(),然后两次调用这个方法并查看结果:

scala> def isPalindrome(str: String) =
     |   str == str.reverse
isPalindrome: (str: String)Boolean

scala> isPalindrome("mom")
res0: Boolean = true

scala> isPalindrome("dude")
res1: Boolean = false

scala> :quit

键入:quit退出shell。

除了键入所有代码,还可以使用:load选项从文件加载代码到shell中。例如,要加载名为script.scala的文件,应键入:load script.scala。在加载那些已经预先写好的函数和类,并在交互模式下做试验时,这个选项非常有用。

使用shell能方便地对小段代码做试验,你很快就会找到运行保存在文件中的代码的简易方法——你将在下一节中习得相关知识。

scala命令有两种运行模式,即交互式的shell或者批处理模式。如果不提供任何参数,可以看到,这个命令会启动交互式shell。但是,如果提供一个文件名,那么它可以在一个独立的JVM中运行这些代码。

我们提供的文件可以是脚本文件或目标文件,也就是由编译器生成的.class文件。在默认情况下,我们可以让工具猜测给定文件的类型,也可以使用-howtorun选项显式指定将其视为脚本文件或目标文件。要传入Java属性,应使用-Dproperty=value格式。让我们创建一个文件并使用命令运行它。

下面是一个名为HelloWorld.scala的文件的内容。

FirstStep/HelloWorld.scala

println("Hello World, Welcome to Scala")

使用命令scala HelloWorld.scala执行此脚本[3],命令和输出如下:

>scala HelloWorld.scala
Hello World, Welcome to Scala

程序的输入参数都写在命令中文件名的后面。不需要额外的编译步骤,直接就可以运行脚本文件中的代码,十分便捷。可以使用这种方式来编写与系统维护或者管理任务相关的代码,运行方式十分便捷,例如,可以在喜欢的IDE中运行这些代码,也可以直接使用命令行,还可以作为持续集成脚本链中的一部分。

即使在使用scala命令时没有显式调用编译器,代码也会经过严格的编译和类型检查。scala工具将给定的脚本编译成内存中的字节码,然后执行它。

回想一下,在Java中,任何独立的程序都需要一个具有static void main方法的类。该规则也适用于Scala程序,因为它们都在JVM上运行。但是,Scala并不强制我们实现main()方法。相反,将脚本转写成具有传统main()方法的Main类很麻烦。所以,当我们运行脚本时,我们实际上在运行这个自动合成的Main类的JVMmain()方法实例。在文件名前使用-savecompiled选项[4],就可以看到在执行scala命令时生成的字节码,scala命令会把这些字节码保存到一个JAR文件中。

现在,我们已经知道如何通过scala命令运行存储在文件中的Scala代码,接下来我们来看看如何不显式使用这个命令而是直接运行一个独立的脚本。

大多数操作系统都支持shebang语法[5]来运行任意脚本。我们可以使用这种方法运行含有Scala代码的独立文件。只要系统上安装了Scala,采用这种方式无须显式调用scala命令并且无缝工作。

在类Unix系统上,在脚本中设置shebang前缀如下。

FirstStep/hello.sh

#!/usr/bin/env scala
println("Hello " + args(0))

键入并运行chmod + x hello.sh以确保文件hello.sh具有可执行权限,然后在命令行上键入如下命令以运行:

./hello.sh Buddy

Buddy是传递给脚本的参数。下面是运行结果:

Hello Buddy

在Windows中经过配置可以做到在一个单独的.scala文件上双击运行的效果。为此,要在Windows资源管理器中双击扩展名为.scala的脚本文件。Windows会反馈无法打开该文件,并要求你从已安装程序列表中选择一个程序。浏览Scala的安装位置,并选中scala.bat。现在,就可以在Windows资源管理器中双击它来运行该程序,也可以在命令提示符中输入去掉.scala后缀后的文件名以运行该程序。

在Windows资源管理器中双击脚本时,会看到一个弹出窗口,它会显示执行结果,并迅速关闭。为了不让弹出窗口消失,可以指定执行这个文件的程序为一个运行Scala脚本并暂停的.bat文件。为此,可右键单击Scala脚本,选择“打开方式...”,然后浏览并选择指定.bat文件。

下面是一个例子。

FirstStep/RunScala.bat

echo off

cls
call scala %1
pause

双击HelloWorld.scala,根据我们的设置,会自动运行RunScala.bat文件,将出现图2-1所示的输出。

到目前为止,我们已经研究了如何用命令行运行Scala程序,但也可以在IDE中运行Scala程序。

Java开发人员大量使用IDE来开发应用程序。主流IDE——Eclipse、IntelliJ IDEA、NetBeans——都有辅助Scala开发的插件。它们提供了与Java编程环境类似的功能——语法高亮、代码补全、调试、合理缩进等。此外,我们可以在同一个项目中混合使用Scala和Java代码,并相互引用。

我们只需给自己最喜爱的IDE安装合适的插件即可。如果使用轻量级编辑器(如Sublime Text和TextMate),也可以安装Scala相关的插件[6]

到目前为止,我们已经以脚本方式运行了Scala代码,并且避免了显式编译。随着程序规模变大,例如,超过一个文件或者有多个类,就必须编译它们。我们来看看这些步骤。

如果代码由多个文件组成,或者想发布字节码而不是源代码,就需要显式编译代码。下面讲一讲如何写一段Scala代码并用scalac编译器编译。在下面的例子中,我们在一个扩展了App特质(trait)且名为Sampleobject中定义了一小段可执行代码——你很快就会了解到Scala的单例对象和特质。App指示编译器生成必需的main()方法,以使Sample成为起始类。

FirstStep/Sample.scala

object Sample extends App {
  println("Hello Scala")
}

使用scalac Sample.scala命令编译这段代码,然后使用scalajava命令运行它。如果使用scala命令,要键入scala Sample。如果使用java命令,则需要为scala-library.jar指定classpath。下面是一个在我的Mac上试验过的例子,使用scalac工具编译后,首先用scala工具运行程序,然后用java工具运行程序:

> scalac Sample.scala
> scala Sample
Hello Scala
> java -classpath /opt/scala/current/lib/scala-library.jar:. Sample
Hello Scala

这里有一个小技巧:可以用current作为符号链接指向你的机器上Scala的安装位置。使用符号链接就可以不用设置PATH和classpath,轻松切换Scala版本。只需要更改符号链接,就可以更改版本。你可以在自己的机器上创建一个类似的符号链接,也可以不用current,用其他合适的目录名指向当前自己正在使用的Scala版本。

在Windows上,我们则需要将classpath设置为scala-library.jar文件的完整路径。

在本章中,我们学会了运行Scala的命令——在shell中运行了一些示例代码,了解了如何运行独立脚本,并学习了如何编译Scala代码。我们已经准备好深入学习Scala,在下一章中,我们将从熟悉的Java基础开始,逐渐切换到Scala。

[1] 我们还可以使用`sbt console`命令进入REPL。——译者注

[2] 实际上,下一次重新定义的变量属于新的作用域,该作用域中相同名称的变量将会隐藏之前的定义。——译者注

[3] 推荐使用amm或者SBT提供的scalas来运行Scala脚本。实际上,我们并不会经常使用Scala脚本。如果读者对Scala脚本感兴趣,可以在之后学习Ammonite Script。——译者注

[4] 简化选项为`-save`。——译者注

[5] 在计算机科学中,shebang(也称为hashbang)是一个由井号和叹号构成的字符序列`#!`,其出现在文本文件第一行的前两个字符的位置。在文件中存在shebang的情况下,类Unix操作系统的程序载入器会分析shebang后的内容,将这些内容作为解释器指令,并调用该指令,并将载有shebang的文件路径作为该解释器的参数。——摘自维基百科

[6] 新版本的SBT 1.1.x支持LSP,为这些轻量级编辑器提供了很好的支持。——译者注


你可以在使用Scala的同时运用自己的Java技能。在某些方面Scala与Java类似,但在许多其他方面又彼此不同。Scala青睐纯面向对象,但它又尽可能将类型和Java的类型对应起来。Scala在支持熟悉的命令式编程风格的同时,也支持函数式编程风格。因此,你可以使用最熟悉的风格立即开始编程,而不用承受陡峭的学习曲线。

在本章中,我们将从熟悉的基础开始,使用Java代码,然后转向Scala。打开最喜欢的编辑器,让我们来编写一些Scala代码。

Java代码中通常充斥着很多样板代码——getter、setter、访问修饰符、处理受检异常的代码等。这些样板还在不断增多,使代码不断膨胀。在后面我们会了解到,Scala编译器做了一些额外的工作,这样就不用耗费精力编写并维护那些本可以生成的代码上了。

Scala具有非常高的代码密度——输入少量代码就可以完成许多功能。作为对比,我们来看一个Java代码的例子。

FromJavaToScala/Greetings.java

// Java代码
public class Greetings {
  public static void main(String[] args) {
    for(int i = 1; i < 4; i++) {
      System.out.print(i + ",");     
    } 
    System.out.println("Scala Rocks!!!");
  }
}

下面是输出:

1,2,3,Scala Rocks!!!

使用Scala可以省去这段代码中的不少东西。首先,它不关心是否使用分号;其次,在这个简单的例子中,把代码写在Greetings类中并没有实际的作用,所以可以摆脱这种做法;再次,不需要指定变量i的类型。Scala很聪明,可以推断出变量i是一个整数;最后,可以使用println而不使用System.out.前缀。下面是上述Java代码使用Scala简化后的代码。

FromJavaToScala/Greet.scala

for (i <- 1 to 3) {
  print(s"$i,")
}

println("Scala Rocks!!!")

要运行此脚本,可以在命令行中键入scala Greet.scala,或者在IDE中运行。

看到的输出结果如下:

1,2,3,Scala Rocks!!!

我们不用+来拼接打印信息,而是用字符串插值(语法形如s"...${expression}..."),这使代码更具表现力也更简洁。我们将在3.7节中讨论更多细节。

Scala的循环结构非常轻巧。我们只需指定索引i的值从1循环到3。箭头的左边(<-)定义了一个val,右边是一个生成器表达式。每次迭代都会创建一个新的val,并使用所生成的值中的元素相继对其进行初始化。

Scala减少了样板代码,也提供了一些语法上的便利。

在前面的代码中,我们使用了val。我们可以使用valvar定义变量。使用val定义的变量是不可变的,即初始化后不能更改。然而,那些使用var定义(不推荐使用)的变量是可变的,可以被改任意次。

不可变性(immutability)是作用在变量上,而不是作用在变量所引用的实例上的。例如,如果我们编写了val buffer = new StringBuffer(),就不能改变buffer的引用。但是,我们可以使用StringBuffer的方法(如append()方法)来修改所引用的实例。故而,对于一个只有val引用的对象,不能假定它是完全不可变的。

另一方面,如果我们使用不可变类如String定义了一个变量,如val str = "hello",就既不能改变引用也不能改变引用所指向的实例的状态。

使用val定义所有的字段,并且只提供允许读但不允许更改实例状态的方法,就可以使一个类的实例不可变。

在Scala中,应尽可能多地使用val,因为它可以提升不可变性,从而减少错误,也可以增益函数式风格。

Greet.scala代码中,所生成的区间包括下界(1)和上界(3)。可以通过until()方法而不是to()方法来排除区间的上界。

FromJavaToScala/GreetExclusiveUpper.scala

for (i <- 1 until 3) {
  print(s"$i,")
}

println("Scala Rocks!!!")

运行上述代码后可以看到如下输出结果:

1,2,Scala Rocks!!!

to()是一个方法,这一点很容易被忽略。to()方法和until()方法实际上都是RichInt上的方法——我们将在3.2节中讨论富封装器(rich wrapper)。变量i的类型为Int,被隐式转换为RichInt,因此在这个变量上可以调用这个方法。这两个方法都返回Range的实例。因此,1 to 3等价于1.to(3),但是前者更优雅。

在Scala中,如果方法没有参数,或者只有一个参数,就可以省略点号(.)和括号。如果一个方法带多个参数,则必须使用括号,但点号仍然是可选的。

我们已经看到了这种灵活性的好处:a + b其实是a.+(b),而1 to 3其实是1.to(3)

可以利用这种轻量级的语法来创建阅读流畅的代码。例如,假定我们在一个类Car中定义了一个turn()方法:

def turn(direction: String) //...

我们就可以使用如下轻量级语法调用上面这个方法:

car turn "right"

通过删除点号和括号,我们减少了代码中的噪声。

在前面的例子中,当我们迭代循环时,似乎对变量i做了重新赋值。但是,变量i不是var,而是val。在每次循环过程中,我们都创建一个不同的名为ival变量。我们不会在循环中不经意地改变变量i的值,因为变量i是不可变的。在这里我们已经悄悄地向函数式风格迈了一步,下面让我们更进一步。

我们也可以使用偏向函数式风格的foreach()方法来实现循环。

FromJavaToScala/GreetForEach.scala

(1 to 3).foreach(i => print(s"$i,"))

println("Scala Rocks!!!")

下面是输出结果:

1,2,3,Scala Rocks!!!

上面这个例子很简洁,且没有赋值操作。我们使用了Range类的foreach()方法。这个方法接受函数值作为参数。所以,在括号中,我们提供了一个接受一个参数的代码主体,在这个例子中参数为i。=>符号将左侧的参数列表与右侧的实现分开。

Scala能推断类型,而且完全面向对象,但是它并没有对原始类型做特殊处理。这样就可以使用和Java一致的方式处理所有数据类型;而且,Scala是在没有损失性能的情况下做到了这一点的。

Java的世界观是分裂的——其原始类型(如intdouble)和对象截然不同。从Java 5开始,利用自动装箱(autoboxing)机制,可以将原始类型视为对象。然而,Java的原始类型不允许方法调用,如2.toString()。另外,自动装箱还涉及类型转换的开销,会带来一些负面的影响。

和Java不同,Scala将所有的类型都视为对象。这就意味着,和调用对象上的方法一样,也可以在字面量上进行方法调用。在下面的代码中,我们创建了一个Scala中的Int的实例,并将它传给java.util.ArrayListensureCapacity()方法,其参数类型为Java的原始类型int

FromJavaToScala/ScalaInt.scala

class ScalaInt {
  def playWithInt(): Unit = {
    val capacity: Int = 10
    val list = new java.util.ArrayList[String]
    list.ensureCapacity(capacity)
  }
}

在这里,Scala默默地将Scala.Int视为Java基本类型int。这是纯粹的编译期转换,故而在运行时没有性能损失。你可以定义val capacity = 10,然后让Scala进行类型推断,在这里,我们显式地指定了类型是为了演示与Java中int的兼容性。

1.to(3)或者1 to 3中,需要用类似的“魔法”,以便可以在Int上调用to()方法。因为Int不能直接处理这种请求,所以Scala会自动应用intWrapper()方法将Int转换为scala.runtime.RichInt,然后调用RichInt上的to()方法。我们将在5.5节中探讨隐式类型转换。

诸如RichIntRichDoubleRichBoolean这些类,可称为富包装类(rich wrapper class)。它们为Scala中的Java原始类型和String提供了便于使用的方法[1]

隐式转换和原始类型上的类型映射使Scala代码变得很简洁。这还只是一个开始——Scala的元组和多重赋值能力还带来了更多细节上的改善。

在Java中,方法可以接受多个参数,但是只能返回一个结果。在Java中返回多个结果需要使用拙劣的变通方案。例如,为了返回用户的姓、名和电子邮箱地址,我们不得不引入Person类,或者返回一个String数组或一个ArrayList。Scala的元组,与多重赋值(multiple assignment)结合,可以将返回多个值变成小菜一碟。

元组是一个不可变的对象序列,创建时使用逗号分隔。例如,("Venkat", "Subramaniam", "venkats@agiledeveloper.com")表示一个3个对象的元组。

可以将元组中的多个元素同时赋值给多个val或者var,如下所示:

FromJavaToScala/MultipleAssignment.scala

def getPersonInfo(primaryKey: Int) = {
  // 假定primaryKey是用来获取用户信息的主键
  // 这里响应体是固定的
  ("Venkat", "Subramaniam", "venkats@agiledeveloper.com")
}

val (firstName, lastName, emailAddress) = getPersonInfo(1)

println(s"First Name: $firstName")
println(s"Last Name: $lastName")
println(s"Email Address: $emailAddress")

下面是执行这段代码后的输出结果:

First Name: Venkat
Last Name: Subramaniam
Email Address: venkats@agiledeveloper.com

如果将这个方法的结果赋值给更少或者更多的变量,那么Scala将会发现这个问题并报错。Scala会在对源代码的编译过程中报错,如果是通过脚本运行,则会在编译阶段报错。举例来说,在下面的代码中,如果我们将方法调用的结果赋值给比元组中数量更少的变量:

FromJavaToScala/MultipleAssignment2.scala

def getPersonInfo(primaryKey: Int): (String, String, String) = {
  ("Venkat", "Subramaniam", "venkats@agiledeveloper.com")
}

val (firstName, lastName) = getPersonInfo(1)

那么Scala将会报错,如下所示:

MultipleAssignment2.scala:5: error: constructor cannot be instantiated to
expected type;
 found   : (T1, T2)
 required: (String, String, String)
val (firstName, lastName) = getPersonInfo(1)
    ^
one error found

除了直接赋值,还可以直接访问元组中的单个元素。例如,如果运行val info = getPersonInfo(1),那么随后就可以采用info._1这种语法形式访问其中的第一个元素,第二个元素则是info._2,以此类推。

下划线加数字这种模式,如_1,表示我们在元组中想访问的元素的索引或位置。与集合不同,访问元组的索引是从1开始的。另一个和集合的差异点在于,如果指定的索引越界,则会在编译期而不是在运行时出错。

有些程序员会抱怨使用下划线来索引元组不太优雅,认为使用点号和下划线的组合笨拙且难以阅读。如果不喜欢下划线,那么有一种很简单的处理方法——克服这种偏见。

元组不仅可以用于多重赋值。在并发编程时,Actor之间也将元组以数据值列表的形式作为消息进行传递,而且元组的不可变性正好契合这种场景。简洁的语法使消息发送端的代码保持简洁。在消息接收端,我们可以使用模式匹配来简洁地接收和处理消息,具体参见9.1.3节。

方法和函数能够返回多重值这个特性非常方便,除此之外,Scala也对参数值的传递做了一些额外的支持。

参数的定义和参数值的传递在任何编程语言中都是最常见的编程任务。Scala提供了一些便利的特性来定义变长参数、声明参数的默认值以及定义命名参数。

println()这样的方法可接受变长参数值。可以传递零个、一个或者多个参数值给这样的方法。在Scala中,你可以方便地创建接受变长参数值的函数。

我们可以设计接受变长参数值的方法。但是,如果我们有多个参数,那么只有最后一个参数可以接受变长参数值。我们可以在最后一个参数类型后面加上星号,以表明该参数(parameter)可以接受可变长度的参数值(argument)。下面以函数max()为例。

FromJavaToScala/Parameters.scala

def max(values: Int*) = values.foldLeft(values(0)) { Math.max }<a href='#anchor32' id='ac32'><sup>[2]</sup></a>

调用max()函数的示例如下所示。

FromJavaToScala/Parameters.scala

max(8, 2, 3)

在上面的例子中,我们只给函数传递了3个参数值,我们也可以传递更多参数值。下面看一个例子。

FromJavaToScala/Parameters.scala

max(2, 5, 3, 7, 1, 6)

当参数的类型使用一个尾随的星号声明时,Scala会将参数定义成该类型的数组。让我们用下面的例子来进行验证。

FromJavaToScala/ArgType.scala

def function(input: Int*): Unit = println(input.getClass)

function(1, 2, 3)

运行这段代码,得到的参数类型如下:

class scala.collection.mutable.WrappedArray$ofInt

可以用任意数量的参数调用max()函数。因为参数类型是数组,所以我们可以使用迭代器来处理接收到的参数的集合。将数组而非离散值作为参数值传入好像很吸引人,但是并不能这样做。例如,下面这个例子:

val numbers = Array(2, 5, 3, 7, 1, 6)
max(numbers) // 类型匹配错误

上面的代码将会产生如下编译错误:

CantSendArray.scala:5: error: type mismatch;
 found   : Array[Int]
 required: Int
max(numbers) // 类型匹配错误
    ^
one error found

类型不兼容是导致这个错误的主要原因。这个参数很像数组,但不是字面上的数组类型,而参数值现在是一个数组。然而,如果我们有一组值,那么我们更希望直接传递数组。我们可以使用数组展开标记(array explode notation),像这样:

val numbers = Array(2, 5, 3, 7, 1, 6)
max(numbers: _*)

参数名后面的一系列符号告诉编译器将数组展开成所需的形式,以传送变长参数值。

现在我们已经知道了如何将变长参数值传递给方法,接下来让我们来看一下另外一个非常棒的功能——参数默认值。

使用Scala,在调用方法或者构造器时,可以很方便地省去最常用的或者说合理的默认值。

在实际工作中,如果我们假定头等邮资(first-class postage)是最常见的邮资选项,那么我们可以这样对邮局的工作人员说“请寄一下这封信”,而不是这样“请使用头等邮资寄一下这封信”。如果请求寄信的时候没有提到邮资,那么工作人员就会默认是头等邮资。

在Java中,我们可以用重载方法的方式省略一个或者多个参数,以达到灵活的效果。从调用者的角度看,这样已经能够很好地工作了,但是重载需要更多的精力和代码,而且会导致代码重复,因此,这样做很容易出错。而在Scala中,可以很容易使用参数默认值避免这些问题。

下面是一个使用参数默认值的例子。

FromJavaToScala/DefaultValues.scala

def mail(destination: String = "head office", mailClass: String = "first"): Unit =
  println(s"sending to $destination by $mailClass class")

mail()方法的两个参数都有默认值。如果一个参数在调用中省去,那么它的默认值就会起作用。

下面是调用mail()方法的几个样例。

FromJavaToScala/DefaultValues.scala

mail("Houston office", "Priority")
mail("Boston office")
mail()

在第一次调用中,我们给所有参数都提供了参数值。在第二次调用中,我们省去了第二个参数,然后在第三次调用中,忽略了所有的参数。从输出结果中我们可以看到,编译器已经为省去的参数补上了默认值:

sending to Houston office by Priority class
sending to Boston office by first class
sending to head office by first class

为省略的参数补上默认值这个操作是在编译时完成的。不过在重载方法的时候,需要特别小心。如果一个方法在基类中用了一个默认值,而在其派生类的相应重载方法中却使用了另一个默认值,就会让人感到困惑,到底选用哪个默认值。

对于多参数的方法,如果对于其中一个参数,你选择使用它的默认值,你就不得不让这个参数后面的所有参数都使用默认值。例如,在上面的例子中,不能使用参数destination的默认值,并对参数mailClass进行显式传值。这种限制的原因在于,被省去的参数所使用的默认值是由参数的位置决定的。接下来我们可以看到,如何利用Scala提供的另一项灵活特性打破这一限制。

Scala的类型检查能够防止向方法传入错误类型的参数值。然而,对于接受多个参数且类型相同的方法,传递参数值的时候容易让人丈二和尚摸不着头脑。例如,pow(2, 3)中的2到底是幂还是基数?

所幸的是,在这种情况下,我们可以通过对参数命名的方式使代码流畅而富于表现力,例如,前面的例子中的调用可以改写为power(base = 2, exponent = 3)

让我们使用命名参数来改写3.4.2节中的mail()方法。

FromJavaToScala/Named.scala

mail(mailClass = "Priority", destination = "Bahamas office")

在调用方法时,我们用目标参数的名字显式指定了参数的值。用了命名参数,就可以不用管参数的顺序了。为了说明这一点,在上一个例子中,我们先给mail()方法的第二个参数赋值。

使用命名参数时,必须注意以下几点。

需要注意的是,以上几点并没有强调参数的顺序。对于有默认值的多参数方法,只要传递参数值时指定名称,那么(在省略某个参数后)接下来的参数都必须使用默认值的限制就不存在了。例如,我们可以这样写:

mail(mailClass = "Priority")

调用mail()函数的时候,因为我们已经指定了mailClass参数的值,编译器会要求destination参数有默认值。然而,正如前面的代码指出的,我们可以通过给参数命名的方式越过这种限制。因此,混用默认值和命名参数的方式要比使用默认值和位置参数的方式更加灵活。

在3.4.2节中,我们已经学习了参数的默认值——如果没有给某个参数传递值,那么Scala将会传递一个默认值。这很好,因为这样我们就不用给那些直观明显或者可以推断出默认值的参数赋值了。但是默认值是由函数的创建者决定的,而不是由调用者决定。Scala还提供另外一种赋默认值的方法,可以由调用者来决定所传递的默认值,而不是由函数的定义者来决定。

我们来看一个利用隐式参数的例子。我们随身携带着各种智能手机和移动设备,它们总是需要连接不同的网络:家庭网络、办公网络、机场候机厅的公共网络等。我们的操作是相同的——连接到一个网络,但是我们所连接的网络依赖于我们所处的环境。我们不想每一次都去指定网络,这很无聊。与此同时,我们也不希望每一次都是同一个默认值生效。这时,我们可以用一个名为隐式参数的特殊参数来解决这个问题。

函数的定义者首先需要把参数标记为implicit。针对这种场景,Scala要求我们把隐式参数放在一个单独的参数列表而非常规的参数列表中(6.4节将详细介绍Scala支持多参数列表)。如果一个参数被定义为implicit,那么就像有默认值的参数,该参数的值传递是可选的。然而,如果没有传值,Scala会在调用的作用域中寻找一个隐式变量。这个隐式变量必须和相应的隐式参数具有相同的类型,因此,在一个作用域中每一种类型都最多只能有一个隐式变量。

让我们创建一个使用了这种特性的例子。

FromJavaToScala/ImplicitParameters.scala

class Wifi(name: String) {
  override def toString: String = name
}

def connectToNetwork(user: String)(implicit wifi: Wifi): Unit = {
  println(s"User: $user connected to WIFI $wifi")
}

def atOffice(): Unit = {
  println("--- at the office ---")
  implicit def officeNetwork: Wifi = new Wifi("office-network")
  val cafeteriaNetwork = new Wifi("cafe-connect")

  connectToNetwork("guest")(cafeteriaNetwork)
  connectToNetwork("Jill Coder")
  connectToNetwork("Joe Hacker")
}

def atJoesHome(): Unit = {
  println("--- at Joe's home ---")
  implicit def homeNetwork: Wifi = new Wifi("home-network")

  connectToNetwork("guest")(homeNetwork)
  connectToNetwork("Joe Hacker")
}

atOffice()
atJoesHome()

connectToNetwork()函数拥有两个参数列表,一个是类型为String的常规参数,另一个是类型为Wifi的隐式参数。

atOffice()函数中,我们定义了Wifi类的两个实例,并将其中一个标记为implicit。我们调用了connectToNetwork()方法3次,但只在第一次调用时为参数wifi提供了值。其他两次调用编译器都会自动填入参数的值。如果参数有一个默认值,那么编译器会在函数的定义中寻找该值。然而,因为这里的参数wifi是隐式参数,所以编译器会在这个函数调用的作用域中寻找定义为implicit的值。

atJoesHome()函数中,我们只定义了一个Wifi实例,并标记为implicit。给connectToNetwork()方法传递参数是可选的。例如,客人可能想要知道自己连接到了哪个网络,但是这个网络很有可能就是常驻者日常隐式使用的[3]。在这种情况下,显式指定一个隐式定义的参数是正确的。

如果定义了一个隐式参数,那么调用者应该传递一个参数值给它,或者在作用域中已经有一个隐式参数的情况下就可以省去;否则编译器就会报错。

运行这段脚本可以观察到如下输出:

--- at the office ---
User: guest connected to WIFI cafe-connect
User: Jill Coder connected to WIFI office-network
User: Joe Hacker connected to WIFI office-network
--- at Joe's home ---
User: guest connected to WIFI home-network
User: Joe Hacker connected to WIFI home-network

输出结果表明,在省略参数值时,相应作用域中的隐式变量就会被使用。尽管在不同的函数中调用的是同一个函数,但是所传入的被省去的参数却不是同一个。虽然参数默认值和隐式参数都可以让调用者省去参数,但是编译器绑定到参数的值却完全不同。

Scala中的字符串就是java.lang.String。可以用Java的方式使用String。然而,Scala对字符串的处理提供了一些额外的便利。

Scala能够自动将String转化为scala.runtime.RichString。这种转换给String新增了一些有用的方法,如capitalize()lines()reverse()方法。

在Scala中创建一个跨行的字符串非常简单,完全不需要用乱七八糟的+=,只要将多行的字符串放在一对3个双引号之中("""...""")就可以了。这是Scala对所谓的here文档[4](或heredoc)的支持。我们创建一个跨行的字符串来举例说明。

FromJavaToScala/MultiLine.scala

val str = """In his famous inaugural speech, John F. Kennedy said
        "And so, my fellow Americans: ask not what your country can do
        for you-ask what you can do for your country." He then proceeded
        to speak to the citizens of the World..."""
println(str)

输出结果如下:

In his famous inaugural speech, John F. Kennedy said
       "And so, my fellow Americans: ask not what your country can do 
       for you-ask what you can do for your country." He then proceeded 
       to speak to the citizens of the World...

在上面的例子中,我们看到可以在多行的字符串中嵌入双引号。Scala将3个双引号中间的内容保持原样,在Scala中这种字符串被称为原始字符串。实际上,Scala是逐字处理字符串的,代码中的缩进也会被带入结果字符串中。我们可以使用RichString中的stripMargin()方法去除起始的空格,像这样。

FromJavaToScala/MultiLine2.scala

val str = """In his famous inaugural speech, John F. Kennedy said
        |"And so, my fellow Americans: ask not what your country can do
        |for you-ask what you can do for your country." He then proceeded
        |to speak to the citizens of the World...""".stripMargin
println(str)

stripMargin()方法将起始的管道符号(|)前面的空白或者控制字符都去掉了。在不是任何行的起始位置以外的其他位置,管道符号将会被保留。如果因为某些原因在一个应用程序中不能用管道符号,可以用stripMargin()方法接收另外一个指定的标记符。下面是前面这段代码的输出:

In his famous inaugural speech, John F. Kennedy said
"And so, my fellow Americans: ask not what your country can do 
for you-ask what you can do for your country." He then proceeded 
to speak to the citizens of the World...

在创建正则表达式的时候,原始字符串十分有用。例如,"""\d2:\d2"""就比"\\d2: \\d2"更容易输入和阅读。

heredoc对创建多行字符串非常有用,但我们在使用创建的字符串时,如println(),还经常需要拼接字符串。我们可以利用字符串插值规避那种杂乱无章的字符串拼接。

在Java中以输出或者消息的形式创建一个字符串非常混乱。例如,要创建一条这样的消息"A discount of 10% has been applied",其中的数值10来自一个名为discount的变量,就十分费力。我们可以这样写:

String message = "A discount of " + discount + "% has been applied"`;`

除了麻烦之外,代码还不易阅读。还可以这样写:

String message = String.format("A discount of %d% has been applied", discount);

但这同样烦琐。Scala提供了简洁而流畅的语法,使用表达式来创建字符串字面量。下面是在Scala中用于创建所需要的消息的等价方式:

val message = s"A discount of $discount% has been applied"

在双引号前面的s的意思是s插值器(s-interpolator),它会找到字符串中的表达式,并将其替换成对应的值。在字符串声明处的作用域中的任何变量都可以在表达式中使用。

字符串字面量中可以有零个或者多个内嵌表达式。如果表达式是最简单的一个变量,那么在它的前面加上美元符号($)。而对于更复杂的表达式,可以把它们放在大括号中,如下例所示:

var price = 90
val totalPrice = s"The amount of discount is ${price * discount / 100} dollars"

美元符号被用作表达式的分隔符,如果说字符串中正好有一个$符号,那么其还可以被用作转义符。为了演示这种用法,我们来看下面这个例子,其重写了前面的消息:

val totalPrice = s"The amount of discount is $$${price * discount / 100}"

在前面的例子中,变量price是不可变的。你可能会好奇这个变量如果可变,会产生什么样的结果。让我们深入挖掘一下。

val discount = 10
var price = 100
val totalPrice =
  s"The amount after discount is $$${price * (1 - discount / 100.0)}"
println(totalPrice)

price = 50
println(totalPrice)

我们在字符串插值之后更改了price的值。表达式的值会在插值的时候被捕获,变量的任何更改都不会影响或者改变字符串,正如我们在输出结果中看到的:

The amount after discount is $90.0
The amount after discount is $90.0

处理可变变量和字符串插值的时候要特别小心。我们可以通过避免使用可变变量,也可以在变量更改之后重新创建插值字符串,进而避免这种混乱。

s插值器只是用值去替换表达式,而不做任何格式处理。例如,我们可以在Scala的REPL中键入以下代码并观察行为:

val product = "ticket"
val price = 25.12
val discount = 10
println(s"On $product $discount% saves $$${price * discount / 100.00}")

表达式正确求值了,但是输出结果中小数点后有3位小数:

On ticket 10% saves $2.512

为了对输出做格式化,而不只是插值,可以使用f插值器(f-interpolator)。字符串的格式化和Java中printf函数遵循相同的规则,只是还可以和前面的例子一样嵌入表达式。我们用格式化的方式改一下前面的字符串:

println(f"On $product $discount%% saves $$${price * discount/100.00}%2.2f")

我们在最后的表达式后面带上格式2.2f,以控制输出到小数点后面两位。而且,我们必须用一个额外的%转义已有的那个百分号。我们没有在product或者discount变量后放置任何格式相关的符号,尽管我们本可以放相应的%s%d。如果没有指定格式,那么f插值器将会假定格式是%s,也就是说直接转化为字符串,这对于手头的字符串来说是非常好的默认行为,正如我们在输出结果中可以看到的:

On ticket 10% saves $2.51

Scala还有一个raw插值器(raw-interpolator),它会把其中的表达式换成值,但是会保留任何不可打印的字符,如换行符。除了这3个内置的插值器,你还可以创建自定义的插值器,但是这样做的前提是你必须了解隐式类(implicit class),我们会在5.5.2节中回顾这个话题。

字符串插值是Scala简化代码的又一种方式。Scala还有很多能够简化代码的地方。下面我们就来看一下如何利用合理的默认值减少代码和混乱。

Scala中有一些约定,可以让代码简洁且易于阅读、编写。下面是这些特性的示例。

除此之外,Scala默认会导入两个包、scala.Predef对象以及它们相应的类和成员。只用类名就可以从这些预导入的包中引用相应的类。Scala按照顺序导入下面的包和类:

因为java.lang已经自动导入,所以无须额外的导入就可以在脚本中使用通用的Java类型。例如,可以使用String,而且不用在前面加上包名java.lang作前缀或者导入它。

也可以直接使用Scala的类型,因为scala包中的一切都已经导入。

Predef对象中包含了类型、隐式转换以及在Scala中常用的一些方法。所以,既然已经默认导入,那么无须任何前缀或者导入,就可以直接使用那些方法和隐式转换。它们太方便了,以至于你开始相信它们是Scala的一部分,实际上它们是Scala标准库的一部分。

Predef对象还提供了一些类型的别名,如scala.collection.immutable.Setscala.collection.immutable.Map。因此,当使用Set或者Map的时候,实际使用的是Predef中对它们的定义,它们分别指向它们在scala.collection.immutable包中的定义。

Scala中合理的约定能够简化代码。下面我们看一下操作符的内部机制和一些默认行为。

技术上说,Scala没有操作符,所以操作符重载的意思就是重载诸如+-等符号。在Scala中,这些都是方法名。操作符利用了Scala宽松的方法调用语法——Scala不强制在对象引用和方法名中间使用点号(.)。

这两个特性结合在一起就给人一种操作符重载的幻觉。因此,当调用ref1 + ref2,实际上写的是ref1.+(ref2),是在ref1上面调用+()方法。

我们来看看下面这个例子,它在一个表示复数的Complex类上提供了+操作符。我们知道,复数有实部和虚部,它们在涉及负数的平方根的复杂方程式求解中非常有用。下面是Complex类。

FromJavaToScala/Complex.scala

class Complex(val real: Int, val imaginary: Int) {
  def +(operand: Complex): Complex = {
    new Complex(real + operand.real, imaginary + operand.imaginary)
  }

  override def toString: String = {
    val sign = if (imaginary < 0) "" else "+"
    s"$real$sign${imaginary}i"
  }
}

val c1 = new Complex(1, 2)
val c2 = new Complex(2, -3)
val sum = c1 + c2
println(s"($c1) + ($c2) = $sum")

如果执行上面的代码,我们会看到如下输出结果:

(1+2i) + (2-3i) = 3-1i

在第一个语句中,我们创建了一个名为Complex的类,定义了一个接收两个参数的构造器。我们使用了Scala富有表现力的语法创建了一个类,具体细节我们将会在4.1.2节中进行深入探讨。

+方法中,我们创建了一个新的Complex类的实例。结果的实部是两个操作数的实部的和,结果的虚部是两个虚部的和。在c1上调用+()方法即得到了表达式c1 + c2的结果,方法的参数是c2,也就是c1.+(c2)

Scala没有操作符这个事实非常有趣。然而,没有操作符并不能免去处理操作符优先级的需要。虽然看起来Scala中没有操作符,所以无法定义操作符的优先级,恐怕不是这样——像24 - 2 + 3 * 6这样的表达式在Java和Scala中都能够正确求值为40。Scala没有在操作符上定义优先级,但是它在方法上定义了优先级。

方法的第一个字符用来决定它们的优先级。如果在一个表达式中两个字符的优先级相同,那么在左边的方法优先级更高。下面是第一个字母的优先级从低到高的列表:

所有字符
|
^
&
< >
= !
:
+ -
* / %
所有其他的特殊字符

我们来看一个操作符或者说方法的优先级的例子。在下面的代码中,我们在Complex中定义了加方法和乘方法。

FromJavaToScala/Complex2.scala

class Complex(val real: Int, val imaginary: Int) {
  def +(operand: Complex): Complex = {
    println("Calling +")
    new Complex(real + operand.real, imaginary + operand.imaginary)
  }

  def *(operand: Complex): Complex = {
    println("Calling *")
    new Complex(
      real * operand.real - imaginary * operand.imaginary,
      real * operand.imaginary + imaginary * operand.real)
  }
  override def toString: String = {
    val sign = if (imaginary < 0) "" else "+"
    s"$real$sign${imaginary}i"
  }
}

val c1 = new Complex(1, 4)
val c2 = new Complex(2, -3)
val c3 = new Complex(2, 2)
println(c1 + c2 * c3)

我们在输出结果中看一下操作符重载的效果:

Calling *
Calling +
11+2i

在最后一行中,我们先在左边调用+(),然后调用*(),但是因为*()优先级更高,所以它会先执行。

从Java工程师的角度,我们已经看到了Scala代码的简洁和表现力。然而,Scala还藏着一些“惊喜”。早一点了解,将有助于处理其中的细微差别。我们接下来就看一下这些“惊喜”。

在你开始感受到Scala设计上的优雅和简洁时,你也应该注意到一些细微差别。例如,在处理赋值、等价性检查、函数返回值的时候,Scala和Java有语义上的不同。因为这些特性的处理与我们在Java中已经习惯的方式有显著的不同,很容易犯错。请花一点时间了解它们以避免各种“惊喜”。

在Java中,赋值操作(像a = b)的值就是a的值,因此像x = a = b这样的多重赋值就可以出现,但是在Scala中不能这样做。在Scala中赋值操作的结果值是一个Unit——大概等价于一个Void。从结果上讲,将这种值赋值给另外一个变量有可能造成类型不匹配。看一看下面这个例子。

FromJavaToScala/SerialAssignments.scala

var a = 1
var b = 2
a = b = 3 // 编译错误

当我们试着执行前面的代码时,就会得到如下编译错误:

SerialAssignments.scala:4: error: type mismatch;
 found   : Unit
 required: Int
a = b = 3  //编译错误
      ^
one error found

这种表现行为至少有那么一点儿恼人。

Java的==对原始类型和对象有着不同的含义。对于基本类型,==意味着基于值的比较,而对于对象,它意味着基于个体身份(即引用)的比较。所以,如果ab都是int,那么若两个变量的值相等,a==b就是true。但是,如果它们都是对象的引用,a==btrue当且仅当两个引用指向同一个实例,也就是说,两者是同一个身份。Java的equals()方法提供了对象间基于值的比较,前提是相应的类对它做了正确的重载。

Scala对==的处理和Java不同,它对所有类型都是一致的。在Scala中,==表示基于值的比较,而不论类型是什么。这是在类Any(Scala中所有类型都衍生自Any)中实现了final==()方法保证的。这一实现使用了旧有的equals()方法。

你必须重载equals()方法,以提供你自己对一个类等价性的实现。然而,做比说难很多。在继承层级结构中,要正确实现eqauls()方法不仅仅需要重载equals()方法以比较相关的字段,还需要重载hashCode()方法,Joshua Bloch的Effective Java__[Blo08]一书对此有所讨论。

对于基于值的比较,在Scala中,可以使用简洁的==而不是equals()方法。如果要对引用做基于身份[5]的比较,那么可以使用Scala中的eq()方法。我们来看一个使用了这两种比较方法的例子。

FromJavaToScala/Equality.scala

val str1 = "hello"
val str2 = "hello"
val str3 = new String("hello")

println(str1 == str2) // 等价于Java的str1.equals(str2)
println(str1 eq str2) // 等价于Java的 str1 == str2
println(str1 == str3)
println(str1 eq str3)

str1str2都指向同一个String实例,因为Java不会为第二个字符串字面量"hello"创建新的对象。然而,str3指向另一个新建的String实例。这3个引用指向的对象所拥有的值是相等的,都是"hello"。因为str1str2就是同一个对象(即引用相等),所以它们的值也相等。然而,str1str3只是在值上相等,并不指向同一个对象。下面的输出结果展示了上面的代码中使用==eq方法或操作符的语义:

true
true
true
false

Scala对==的处理对于所有类型的行为都是一致的,避免了在Java中使用==的语义混乱。但是,你必须注意到这和Java中的语义相差很大,以避免可能的失误。

在涉及语句或者表达式的终止时,Scala很厚道——分号(;)是可选的,这就能够减少代码中的噪声。我们可以在语句或者表达式的末尾放置一个分号,特别是,如果想要在同一行上放置多个语句或者表达式的话,但一定要小心。在同一行上写多个语句或者表达式可能会降低代码的可读性,就像下面这个例子:

val sample = new Sample; println(sample)

如果一行的末尾没有以一个中缀标记[6](如+*. )结尾,且不在括号或者方括号中,那么Scala会自动补上分号。如果下一行的起始处能够开始一个语句或者表达式,那么这一行的末尾也会自动补上分号。

然而,Scala在某些上下文中要求在{前有一个分号。如果没有写分号,那么最终效果可能会让人吃惊。让我们来看一个例子。

FromJavaToScala/OptionalSemicolon.scala

val list1 = new java.util.ArrayList[Int];
{
  println("Created list1")
}

val list2 = new java.util.ArrayList[Int] {
  println("Created list2")
}

println(list1.getClass)
println(list2.getClass)

输出结果如下:

Created list1
Created list2
class java.util.ArrayList
class Main$$anon$2$$anon$1

我们在定义list1的时候放置了一个分号,因此,紧随其后的{开启了一个新的代码块。然而,因为我们在定义list2的时候没有写分号,所以Scala会假定我们是在创建一个继承自ArrayList[Int]的匿名内部类。因此,list2指向一个匿名内部类的实例,而不是ArrayList[Int]的一个实例。因此,如果是想在创建一个实例之后新建一个代码块,就要写上分号。

Java强制写分号,但是Scala给了是否使用分号的自由——要用好这个特性。没有那些分号,代码会变得简洁且噪声更少。不使用分号,就能开始享受Scala优雅而轻量的语法。像前面所提到的例子那样,必须使用分号以避免潜在的歧义时要保留分号。

在Java中,我们使用return语句从方法返回结果,而这在Scala中却不是一个好的实践。``return语句在Scala中是隐式的,显式地放置一个return命令会影响Scala推断返回类型的能力。看一个例子。

FromJavaToScala/AvoidExplicitReturn.scala

def check1 = true
def check2: Boolean = return true
def check3: Boolean = true
println(check1)
println(check2)
println(check3)

在前面的代码中,Scala非常愉快地推断出了check1()方法的返回类型。但是,因为我们在方法check2()中使用了一个显式的return,所以Scala没有推断出类型。在这种情况下,我们就必须提供返回类型Boolean

即使你选择提供返回类型,也最好避免显式的return命令,check3()方法就是一个很好的示范——代码不嘈杂,然后你就会习惯Scala中的惯例——最后一个表达式的结果将会自动被返回。

Scala在封装这个领域也有一些惊喜——它对访问的边界做了细粒度的控制,远胜于Java中提供的功能。

Scala的访问修饰符(access modifier)和Java有如下不同点。

让我们通过一些例子来体会和Java的这些不同之处。

在不使用任何访问修饰符的情况下,Scala默认认为类、字段和方法都是公开的。如果想将一个成员标记为private或者protected,只要像下面这样用相应的关键字标记即可。

FromJavaToScala/Access.scala

class Microwave {
  def start(): Unit = println("started")
  def stop(): Unit = println("stopped")
  private def turnTable(): Unit = println("turning table")
}
val microwave = new Microwave
microwave.start() // 编译正确

这段代码中,start()stop()方法默认都是公开的,我们可以在Microwave类的任意实例中访问这两个方法。另外,我们将turnTable()方法显式定义为private,因此我们无法在这个类外面访问这个方法。如果我们像前面的例子那样尝试,就会得到如下错误:

Access.scala:9: error: method turnTable in class Microwave cannot be
accessed in this.Microwave
microwave.turnTable() // 编译错误
          ^
one error found

对于公开的字段和方法,可省略访问修饰符。而对于其他成员,要显式指定访问修饰符,以达到期望的访问控制效果。

在Scala中,protected让所修饰的成员仅对自己和派生类可见。对于其他类来说,即使正好和所定义这个类处于同一个包中,也无法访问这些成员。更进一步,派生类在访问protected成员的时候,成员的类型也需要一致。让我们用下面的例子做检验。

FromJavaToScala/Protected.scala

class Vehicle {
  protected def checkEngine() {}
}

class Car extends Vehicle {
  def start() { checkEngine() /* 编译正确 */ }
  def tow(car: Car) {
    car.checkEngine() // 编译正确
  }
  def tow(vehicle: Vehicle) {
    vehicle.checkEngine() // 编译错误
  }
}

class GasStation {
  def fillGas(vehicle: Vehicle) {
    vehicle.checkEngine() // 编译错误
  }
}

通过编译这段代码我们可以看到,在编译器的错误消息中,这些访问控制已经生效:

Protected.scala:12: error: method checkEngine in class Vehicle cannot be
accessed in automobiles.Vehicle
 Access to protected method checkEngine not permitted because
 prefix type automobiles.Vehicle does not conform to
 class Car in package automobiles where the access take place
    vehicle.checkEngine() // 编译错误
            ^
Protected.scala:17: error: method checkEngine in class Vehicle cannot be 
accessed in automobiles.Vehicle
 Access to protected method checkEngine not permitted because
 enclosing class GasStation in package automobiles is not a subclass of
 class Vehicle in package automobiles where target is defined
    vehicle.checkEngine() // 编译错误
            ^
two errors found

在上面的代码中,VehiclecheckEngine()方法是protected的,能够被Vehicle的任何实例方法访问到。我们可以在一个实例方法中访问这个方法,如派生类Carstart()方法中,也可以在一个Car的实例中访问这个方法,如Car类的tow()方法,但我们不能在Car的实例中通过Vehicle的实例访问这个方法,其他任意类也都不行,如GasStation,尽管GasStationVehicle在同一个包中。这种行为模式和Java中的protected是有区别的。Scala对protected成员的访问控制更加严格。

一方面,Scala在protected修饰符上的限制比Java更多;另一方面,Scala在设置访问可见性上面有很大的灵活度以及细粒度的控制。

可以为privateprotected修饰符指定额外的参数。故而,除了简单地将一个成员标记为private,还可以标记为private[AccessQualifier],其中AccessQualifier可以是任何封闭类名、一个封闭的包名或者是this(即实例级别的可见性)。

访问修饰符上的限定词告诉Scala,对于所有类该成员都是私有的,除了以下情况。

组合之后情况有点儿多,比较费脑力。下面这个细粒度访问控制的例子可以把这些细节都讲清楚。

FromJavaToScala/FineGrainedAccessControl.scala

package society {

  package professional {
    class Executive {
      private[professional] var workDetails = null
      private[society] var friends = null
      private[this] var secrets = null

      def help(another: Executive): Unit = {
        println(another.workDetails)
        println(secrets)
        println(another.secrets) // 编译错误
      }
    }

    class Assistant {
      def assist(anExec: Executive): Unit = {
        println(anExec.workDetails)
        println(anExec.friends)
      }
    }
  }

  package social {
    class Acquaintance {
      def socialize(person: professional.Executive) {
        println(person.friends)
        println(person.workDetails) // 编译错误
      }
    }
  }
}

编译这段代码将会产生如下错误:

FineGrainedAccessControl.scala:12: error: value secrets is not a member of
society.professional.Executive
        println(another.secrets) // 编译错误
                        ^
FineGrainedAccessControl.scala:28: error: variable workDetails in class 
Executive cannot be accessed in society.professional.Executive
        println(person.workDetails) // 编译错误
                       ^
two errors found

这个例子展示了不少Scala中的细微差别。在Scala中,我们可以定义嵌套包,类似于C++和C#中的嵌套命名空间。在定义包名时,我们可以遵循Java的风格——使用点,如society.professional,也可以使用C++或者C#的嵌套命名空间的风格。如果决定把从属于一个层次结构的包下面的多个比较小的类放在同一个文件中,那么遵循Java的风格就没有后者方便。

在上面的代码中,我们让Executive中的私有字段workDetails在封闭的包professional中可见。Scala就会允许这个包中的Assistant类中的方法访问这个字段。但是,在别的包里的Acquaintance类,就不能访问这个字段。

对于私有字段friends,我们让封闭包society下面的所有类都能访问。这就使得在Acquaintance类中可以访问字段friends,因为该类是在society包所包含的子包之中。

private默认的可见性是类级别的,在一个类的实例方法中,可以访问同一个类的任何实例中标记为private的成员。然而,Scala通过this限定符可以对privateprotected做细粒度的控制。例如,在前面的例子中,因为secrets已经被标记为private[this],所以实例方法只能在隐式实例下访问这个字段,也就是说,在这个实例中,这个字段不能在其他实例中访问。这也是我们在实例方法help()中能够访问secrets但是不能访问another.secrets的原因。同样,一个标记为protected[this]的字段可以在派生类的实例方法中访问,但是仅限于当前实例。

在本章中,我们从Java程序员的角度对Scala的特性做了走马观花式的介绍。我们看到了Scala和Java类似的地方,同时也了解了一些Scala独有的特性。

乍一看,Scala好像是简明版的Java——Java能做的Scala都能做,而且更简洁,语法也更丰富。除此之外,Scala还提供了一些Java中不支持的特性,如元组、多重赋值、命名参数、默认值、隐式参数、多行字符串、字符串插值以及更加灵活的访问修饰符。

在本章我们只是对Scala做了一些浅层次的挖掘,引出了Scala的一些关键优势。在下一章中,我们将了解到Scala对面向对象范式的支持。

[1] 这里指的是,我们在使用这些方法时,好像这些方法直接定义在了这些对象上一样,非常方便易用,实际上Scala通过隐式类型转换做到了这一点。同时,利用值类型,对性能几乎毫无影响。——译者注

[2] 这段代码实际上没有处理参数个数为0的情况,在调用`max()`时,实际上对`values(0)`的访问会导致`java.lang.IndexOutOfBoundsException`。——译者注

[3] 这里指隐藏了无线网络的SSID。——译者注

[4] here文档又称为heredoc、hereis、here字串或here脚本,是一种在命令行shell和编程语言里定义一个字符串的方法。它可以保存文字里面的换行或是缩排等空白字符。一些编程语言允许在字串里执行变量替换和命令替换。——摘自维基百科

[5] 即比较引用是否指向同一个对象。——译者注

[6] 中缀表示法(或者中缀记法)是一种通用的算术或逻辑公式表示方法,操作符是以中缀形式处于操作数的中间的(如3 + 4)。——摘自维基百科


相关图书

JUnit实战(第3版)
JUnit实战(第3版)
Kafka实战
Kafka实战
Rust实战
Rust实战
PyQt编程快速上手
PyQt编程快速上手
Elasticsearch数据搜索与分析实战
Elasticsearch数据搜索与分析实战
玩转Scratch少儿趣味编程
玩转Scratch少儿趣味编程

相关文章

相关课程