![Spark大数据处理与分析](https://wfqqreader-1252317822.image.myqcloud.com/cover/233/36862233/b_36862233.jpg)
2.4 Scala编程
使用Scala和Spark是学习大数据分析的很好的组合。Scala允许将两种方法结合使用,即以面向对象编程(Object-Oriented Programming)和函数式编程(Functional Programming)两种风格编写代码。面向对象编程范例提供了一个全新的抽象层,可以通过定义实体(如具有相关属性和方法的类)对代码进行模块化,甚至可以利用继承或接口定义这些实体之间的关系。也可以将具有相似功能的相似类组合在一起,例如作为一个帮助器类,使项目立刻变得更加具有可扩展性。简言之,面向对象编程语言的最大优势是模块化和可扩展性。另外,在计算机科学中函数式编程是一种编程范例,是一种构建计算机程序结构和元素的独特风格。这种独特性有助于将计算视为对数学函数的评估,并避免状态更改和数据变化。因此,通过使用函数式编程概念,可以学习按照自己的风格进行编码,以确保数据的不变性。换句话说,函数式编程旨在编写纯函数,并尽可能地消除隐藏的输入和输出,以便代码尽可能地描述输入和输出之间的关系。
为了执行交互式数据清理、处理、修改和分析,许多数据科学家使用R或Python作为他们的工具,并尝试使用该工具解决所有的数据分析问题。因此,在大多数情况下,引入新工具可能非常具有挑战性,新工具具有更多的语法和新的学习模式集。Spark中包括了用Python和R编写的API,通过PySpark和SparkR分别允许使用Python或R编程语言调用Spark的功能组件。但是,大多数Spark书和在线示例都是用Scala编写的。可以说,数据科学家使用Scala语言学习Spark的开发,将胜于使用Java、Python或R编程语言,其原因包括:消除了数据转换处理开销;提供更好的性能;更好地理解Spark原理。
这意味着,您正在编写Scala代码以使用Spark及其API(即SparkR,SparkSQL,Spark Streaming,Spark MLlib和Spark GraphX)从集群中检索数据。或者,您正在使用Scala开发Spark应用程序,以在自己的计算机上处理该数据。在这两种情况下,Scala都是您真正的朋友,并将及时向您派息。
本节将讨论Scala中基本的面向对象功能,涵盖的主题包括:Scala中的变量;Scala中的方法、类和对象;包和包对象;特性和特征线性化。然后讨论模式匹配,这是来自功能编程概念的功能。此外,本节将讨论Scala中的一些内置概念,如隐式和泛型。最后,讨论将Scala应用程序构建到jar中所需的一些广泛使用的构建工具。
2.4.1 面向对象编程
Scala REPL是命令行解释器,可以将其用作测试Scala代码的环境。要启动REPL会话,只需在本教程提供的虚拟环境的命令中输入scala,之后将看到以下内容:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P77_22455.jpg?sign=1739297163-SbOvMwFQAezHPNivFG685h02th6qot7P-0-60e7f5d49e3189390539cf6bda28c0b6)
因为REPL是命令行解释器,所以需要输入代码,然后按Enter键执行就可以看到结果。进入REPL后,可以输入Scala表达式查看其工作方式。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P77_22456.jpg?sign=1739297163-Bpugpv3b7U2rjQvIKDsDlGFz5WXeOGvd-0-092719667e3f461628a4ab480f836f72)
如这些示例所示,在REPL内输入表达式,就会在下一行显示每个表达式的结果。
Scala REPL会根据需要创建变量,如果不将表达式的结果分配给变量,则REPL会自动创建以res为开头的变量,第一个变量是res0,第二个变量是res1,等等。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P77_22457.jpg?sign=1739297163-6LivZLiMY89UVWPXi40pULSXCJT9m9a1-0-682cbacfca4427262f5faa2f1cb8a694)
这些是动态创建的实际变量名,可以在表达式中使用它们。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P77_22458.jpg?sign=1739297163-gboQjbHskNkJNmc4kgaf662APNTYALF1-0-9ff1ff9e6efda0e6c759466e58f70b47)
上面简单介绍了Scala REPL的使用。在本书中,大部分例子都使用了Spark Shell工具,这就是Spark提供的REPL,只是在启动工具时加载了Spark程序包,可以直接在命令上调用。这里继续使用REPL进行实验。下面是一些表达式,可以尝试看看它们如何工作。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P77_22459.jpg?sign=1739297163-eVeVHEBkx5sRSxCdPPrlHZA0qD7GgT2k-0-23c97c0d08ab03d0a143d7f53d75e9a7)
Scala具有两种类型的变量:val类型创建一个不可变的变量,例如在Java中的final;var创建一个可变变量。下面是Scala中的变量声明:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P78_22462.jpg?sign=1739297163-nedzkQDfxjnN4H4vpvu2TF4KysgDIwss-0-1e6f206c6c9dc025c3573a9820c7e862)
这些示例表明,Scala编译器通常可以从“=”符号右侧的代码推断出变量的数据类型,所以变量的类型可以由编译器推断。如果愿意,还可以显式声明变量类型。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P78_22463.jpg?sign=1739297163-dFRT8TWg7cWA8vRUc4f2THlJF2nVLw41-0-1f7860957e52afd59bde2644d812ba2c)
在大多数情况下,编译器不需要查看显式类型,但是如果认为它们使代码更易于阅读,则可以添加它们。实际上,当使用第三方库中的方法时,特别是如果不经常使用该库或它们的方法名称不能使类型清晰时,可以帮助提示变量类型。
val和var之间的区别是:val使变量不变,var使变量可变。由于val字段不能改变,因此有些人将其称为值,而不是变量。当尝试重新分配val字段时,REPL显示会发生什么?
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P78_22464.jpg?sign=1739297163-u7FVIBx5QFjKrnyK8RjxCxIjTkT8dYu8-0-68ea87dfcc451e309686e46d92f5cbc6)
正如预期的那样,此操作失败并显示val的重新分配错误。相反,我们可以重新分配var。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P79_22467.jpg?sign=1739297163-KC6oBL0bMPmSrA7njFPBpcDiPkz6I5vq-0-033e8ca8b8c467d22b7a821600ecd9ca)
REPL与在IDE中使用源代码并非完全相同,因此在REPL中可以做一些事情,而在编写Scala应用程序中是做不到的。例如,可以使用val()方法在REPL中重新定义变量,如下所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P79_22468.jpg?sign=1739297163-DktTboC0BVtTZRf1zW6JZ4lsSEaFlQ3j-0-0e992d9b98666f9c0593e481c4f66c14)
而在scala应用程序代码中,不能使用val()方法重新定义变量,但是可以在REPL中重新定义。Scala带有标准数字数据类型。在Scala中,所有这些数据类型都是对象,不是原始数据类型。这些示例说明了如何声明基本数字类型的变量。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P79_22469.jpg?sign=1739297163-gemhOxqdzNUt1No1KLNATWusUZCDSdhi-0-4c54e41f2fcacd50a61296981222a038)
在前四个例子中,如果没有明确指定类型,数量1将默认为Int,所以,如果需要其他数据类型Byte、Long或者Short中的一种,则需要显式声明的类型。带小数的数字(如2.0)将默认为双精度,因此,如果需要单精度,则需要使用Float类型声明。
因为Int和Double是默认数字类型,所以通常在不显式声明数据类型的情况下创建它们。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P80_22471.jpg?sign=1739297163-MXLQWnw9sIJAWJenhGvzafckHoUcbPkW-0-8e3cbb8b5caaaa955d3dd8cdd177ef5d)
大多数情况下,Scala还包括BigInt类型和BigDecimal类型。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P80_22472.jpg?sign=1739297163-njfAQkCSNKi1OLi3qB5rVBGCHwBOAHgL-0-8d348589415db892fb7003c6213d225c)
BigInt和BigDecimal的一大优点是,它们支持用户习惯使用数值类型的所有运算符。Scala还具有String和Char数据类型,通常可以使用隐式形式进行声明。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P80_22474.jpg?sign=1739297163-cwg4CzYsLSFEjyCKR3MJG9KhcZX9q0Re-0-4a23e52a3881e819998eb1213c765356)
如上例所示,将字符串括在双引号中,将字符括在单引号中。Scala字符串具有很多功能,其中一个功能是Scala具有一种类似Ruby的方式合并多个字符串。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P80_22475.jpg?sign=1739297163-FaF8hxVoiSBManjIwslHZZAanONKLo5W-0-4fb1a2874b459ae2540a9a5a895df451)
可以按以下方式将它们附加在一起:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P81_22477.jpg?sign=1739297163-DbAS8KNlHFEOBp65ic2yrdM4Kz082vEY-0-550855cab2e5324746f4790fd8abb01a)
但是,Scala提供了以下更方便的形式:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P81_22478.jpg?sign=1739297163-z0tVLf58UnjFKJ2RZdiGTwG8plHQZbyE-0-6d616392592ec7bc6443e4c208622bbc)
这种形式创建了一种非常易读的方式来打印包含变量的字符串。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P81_22479.jpg?sign=1739297163-NUgGiNNGMnJB8BJCKl8ZFCAtOLSvVi7D-0-1b93f88c910fe4005b7019f562f5b89d)
如下所示,用户要做的是在字符串前加上字母s,然后在字符串内的变量名之前添加$符号,此功能称为字符串插值。Scala中的字符串插值提供了更多的功能,例如,还可以将变量名称括在花括号内。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P81_22480.jpg?sign=1739297163-ePd3eqlPJOuJlV0FNnwKKgheE8FTatXX-0-3e2bd2d14b047ccc90d665fe2d1743b6)
对于一些用户来说,这种格式较易读,但更重要的好处是可以将表达式放在花括号内,如以下REPL示例所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P81_22481.jpg?sign=1739297163-gAeJQRwuCP498JbvGE9orybOd0RWOBJn-0-22bff1ee9ab0a44a3b4284f1a483e209)
使用字符串插值可以在字符串前面加上字母f,以便在字符串内部使用printf样式格式,而且原始插值器不对字符串内的文字(如\n)进行转义。另外,还可以创建自己的字符串插值器。Scala字符串的另一个重要功能是可以通过将字符串包含在三个双引号中创建多行字符串。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P81_22482.jpg?sign=1739297163-Bgu8TQE3ks3Veg6DhZSI2NXpqtUISUMN-0-21db95eb69ad5796bb612fc505a49023)
当需要使用多行字符串时,这非常有用。这种基本方法的一个缺点是第一行之后的行是缩进的。解决此问题的简单方法是:在第一行之后的所有行前面加上符号“|”,并在字符串之后调用stripMargin()方法。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P82_22484.jpg?sign=1739297163-iWRLkRWvnfaEyXxIcJDEtrTVIgCsL9r8-0-e193b4ea0a6d69311c7fd16072dc4313)
下面看一下如何使用Scala处理命令行输入和输出。如前所述,可以使用命令println将输出写入标准输出,该函数在字符串后添加一个换行符,因此,如果不希望这样做,只需使用print。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P82_22485.jpg?sign=1739297163-qD5HNVwqW4dYTac8IqhldOa8CFTuGK19-0-21cc5701cc2c0bb27db4542eb62571d0)
因为println()是常用方法,所以同其他常用数据类型一样,不需要导入它。有几种读取命令行输入的方法,但是最简单的方法是使用scala.io.StdIn包中的readLine()方法。就像使用Java和其他语言一样,通过import语句将类和方法带入Scala的作用域。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P82_22486.jpg?sign=1739297163-xa7dlFg9XHQ6Xs7eSHZq5v1nLMYfwUyo-0-b0064f0e5318dc99db9100bd7fabbe9f)
import语句将readLine()方法带入当前范围,因此可以在应用程序中使用它。Scala具有编程语言的基本控制结构,包括条件语句(if/then/else)、for循环、异常捕获(try/catch/finally),它还具有一些独特的构造:match表达式、for表达式。我们将在以下内容中演示。一个基本的Scala if语句如下所示:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P82_22487.jpg?sign=1739297163-O8qLTP3E6qZkYu0rgPvu8IIV3okdD6tQ-0-c227387ab574f8f421eaca251b8a9226)
也可以这样编写该语句:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P82_22488.jpg?sign=1739297163-XW8LDYO9qAltdOhapz1KKtqLes1IYt66-0-b090ef6d855c33707b87da28ab704b97)
if/else结构如下所示:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P82_22489.jpg?sign=1739297163-vxUyy3UMx73FUPXZycYPsmAvqgvPXUHq-0-c839786ca214d86b44768e9f8152b728)
完整的Scala if/else-if/else表达式如下所示:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P83_22492.jpg?sign=1739297163-XDHhZlFwnQ3sIP4BdLCc28QBnmyAmUV7-0-e89bf121919124af664137d513140bcc)
Scala if构造总是返回结果,可以像前面的示例中那样忽略结果,但是更常见的方法(尤其是在函数编程中)是将结果分配给变量。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P83_22493.jpg?sign=1739297163-bMZfYM1Brz8tBzaqpOfGuClhIOPrh0ET-0-3ae31ae8f354e0c6814a09fee8c72b8a)
这意味着,Scala不需要特殊的三元运算符。Scala for循环可用于迭代集合中的元素。例如,给定一个整数序列,然后遍历它们并打印出它们的值,如下所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P83_22494.jpg?sign=1739297163-9EAhxNSLaVUavk16yM5OvkhvdfG2mSOw-0-281065b2d98d3261cf1cdee2c89d28f8)
上面的示例使用了整数序列,其数据类型为Seq[Int]。下面例子的数据类型为字符串列表List[String],使用for循环打印其值,就像前面的示例一样。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P83_22495.jpg?sign=1739297163-Agr5J0xIfMp94mjtPgNWODIES5ne5Jeb-0-60017b3fcb0ad9b98ab6748078bdd835)
Seq和List是线性集合的两种类型。在Scala中,这些集合类似于Array。为了遍历元素集合并打印其内容,还可以使用foreach()方法,对于Scala集合类,可用这个方法,例如,用foreach()打印先前的字符串列表。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P84_22498.jpg?sign=1739297163-QEvZki5orlWr87OUEkDt4cTrarnJybAu-0-e04e07eab3d6c3d16953be99b19a142f)
foreach()可用于大多数集合类,对于Map(类似于Java的HashMap),可以使用for()和foreach()。下面的例子使用Map定义电影名称和等级,分别使用for()和foreach()方法打印输出电影名称和等级。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P84_22499.jpg?sign=1739297163-rKuJz7wKDYPvsWCumGSBdUbkBxpVcqkA-0-59d341cf673382f6df49b0c478bc6479)
在此示例中,name对应Map中的每个键,rating是分配给每个name的值。一旦开始使用Scala,会发现在函数式编程语言for中,除了for循环外,还可以使用更强大的for表达式。在Scala中,for表达式是for结构的另一种用法。例如,给定以下整数列表,然后创建一个新的整数列表,其中所有值都加倍,如下所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P85_22502.jpg?sign=1739297163-miBq05tUmn5lg73Gp8A1Pqo35lrxaZAu-0-3b4ecc8f7964e6f3c8f0b254c3f42e03)
该表达式可以理解为:对于数字nums,列表中的每个数字n的值加倍,然后将所有新值分配给变量doubledNums。总而言之,for表达式的结果是将创建一个名为doubledNums的新变量,其值是通过将原始列表中nums的每个值加倍而创建的。可以对字符串列表使用相同的方法,例如给出以下小写的字符串列表,使用for表达式创建大写的字符串列表。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P85_22503.jpg?sign=1739297163-Kd6FZva9fjsy8p0PqSvCsm6OSAU54tfW-0-a6162600b4c35fdb88bf031c942b7c7b)
上面两个for表达式都使用yield关键字,表示使用所示算法在for表达式中迭代的现有集合产生一个新集合。如果要解决下面的问题,必须使用yield表达式。例如,给定这样的字符串列表:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P85_22505.jpg?sign=1739297163-3goFWBlbWhIj4CfI0wAuH8CuNu7GRLl3-0-89aaac732f44d25391d7d53f5ca19e7d)
假设要创建一个包含每个大写姓名的新列表。为此,首先删除每个名称开头的下画线,然后大写每个名称。要从每个名称中删除下画线,需要在每个String上调用drop(1),完成之后在每个字符串上调用大写方法,可以通过以下方式使用for表达式解决此问题。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P85_22506.jpg?sign=1739297163-bEYSlmMLRs34rMsA05ljoNjqAiYNKhU2-0-c3b43e4f69f57994b413b9964ce1f740)
该示例显示了一种比较烦琐的解决方案,因此可以看到,在yield之后使用了多行代码。但是,对于这个特定的示例,也可以使用更短的编写代码,这更像Scala风格。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P86_22509.jpg?sign=1739297163-sbETSe2UT8VSd3uTxBjUMuwMBW1udsfw-0-53e2b5f802bdb85dae31d7ddde47f31a)
还可以在算法周围加上花括号:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P86_22510.jpg?sign=1739297163-BuLpAEknPFB39CUaDctzHoGg55gZOROs-0-10362b8d1812fe4bc18961390bc9a246)
Scala还有一个match表达式的概念。在最简单的情况下,可以使用match类似Java switch语句的表达式。使用match表达式可以编写许多case语句,用于匹配可能的值。在示例中,将整数值1~12进行匹配。其他任何值都将落入最后一个符号“_”,这是通用的默认情况。match表达式很不错,因为它们也返回值,所以可以将字符串结果分配给新值。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P86_22511.jpg?sign=1739297163-2OPMquCK7VnYvKS3Uw3gGtVlq5bHNhR0-0-7a6d5ee7502b8d08ec1ed3b634e366b9)
另外,Scala还使将match表达式用作方法主体变得更容易。作为简要介绍,下面是一个名为convertBooleanToStringMessage()的方法,该方法接受一个Boolean值并返回String。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P87_22513.jpg?sign=1739297163-gpMoMZppRQWCkdpXbaiKSKvAtJza3sze-0-716e8dc6001612d8920e1938b9dea201)
这些示例说明了为它提供Boolean值为true和false时它是如何工作的。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P87_22514.jpg?sign=1739297163-SedjZbmCTGVenMwcJUUxLXgBFSc3zKwg-0-6758904329dc402978b7b2f618faa16c)
下面是第二个示例,它与上一个示例一样工作,将Boolean值作为输入参数并返回一条String消息。最大的区别是,此方法将match表达式用作方法的主体。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P87_22515.jpg?sign=1739297163-YyVzMGgCqMnmxdtW80Z102Ci0cTkzir0-0-67c87655af7ed333b36ff324ce8252c3)
该方法的主体只有两个case语句:一个匹配true;另一个匹配false。因为这些是唯一可能的Boolean值,所以不需要默认case语句,现在可以调用该方法,然后打印其结果。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P87_22516.jpg?sign=1739297163-LBzo426WguhvNye3uOsQSrzufjJU6hxM-0-ba4a5258c57eb9695c3a9b502a759090)
将match表达式用作方法的主体也是一种常见的用法。match表达式非常强大,下面演示可以使用match执行的其他操作。match表达式可以在单个case语句中处理多种情况,为了说明这一点,假设参数为0或空白字符串返回值为false,其他任何值返回为true,使用match表达式计算true和false,这一条语句(case 0|""=>false)让0和空字符串都可以评估为false。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P87_22517.jpg?sign=1739297163-tRPzCPDKG51bv1tABkr5bcrDzM1Ab8B1-0-8125251200744460c7b9fbafb47b9dcc)
因为将输入参数a定义为Any类型,这是所有Scala类的根,就像Java中的Object一样,所以此方法可与传入的任何数据类型一起使用。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P88_22519.jpg?sign=1739297163-EucJUwpYvIOYIykDJKvsWUmyMBP80QnF-0-0f166237ab119407303397f89401b1e9)
match表达式的另一个优点是,可以在case语句中使用if表达式进行强大的模式匹配。在此示例中,第二种和第三种情况语句均使用if表达式匹配数字范围。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P88_22520.jpg?sign=1739297163-MmoTsbcMjtVeNh2mZ9WLAKTfqTNulgMd-0-986a603cd037ae7fb33f33ce469f8944)
Scala不需要在if表达式中使用括号,但是如果使用,可以提高可读性。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P88_22521.jpg?sign=1739297163-ntk2jDvqGQVOMhxtAL10sCTGZymW7cze-0-5b180db2b95b962161040c1004d615dd)
为了支持面向对象编程,Scala提供了一个类构造,其语法比Java和C#之类的语言简洁得多,而且易于使用和阅读。这里有一个Scala的类,它的构造函数定义了firstName和lastName两个参数。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P88_22522.jpg?sign=1739297163-1nfCvDxJPUyozb2v4ChkGy40MnLIbn9I-0-a37879f82e5eb2ce49133df8ab4d4cb6)
有了这个定义,可以创建如下的新Person实例。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P89_22524.jpg?sign=1739297163-6bWoD69rEojN4FvdnDAE9TH4XPYj1N56-0-fc8717d02abdbc74cfc4719aeb0f177e)
在类构造函数中定义参数会自动在类中创建字段。在本示例中,可以这样访问firstName和lastName字段:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P89_22525.jpg?sign=1739297163-Ux6zWeRbKRhx8XnZtM9AtSHC5c3PHoNr-0-6b3884445cb0d231f8bdf04f4fd0c058)
在此示例中,由于两个字段都被定义为var字段,因此它们也是可变的,这意味着可以更改它们。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P89_22526.jpg?sign=1739297163-rWZzwGs5eLZQyw9J9c73oMwThN4NqPKx-0-542ff737a0ac9fc37e7239c134b2c613)
在上面的示例中,两个字段都被定义为var字段,这使得这些字段可变,还可以将它们定义为val字段,这使得它们不可变。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P89_22527.jpg?sign=1739297163-pEoHezpIw0UBq4ayhkJo733RydrdhLSb-0-5661b95bd3e53f6ac15d52734c68ab54)
如果使用Scala编写面向对象编程的代码,则将字段创建为var字段,以便对其进行改变。当使用Scala编写函数编程的代码时,一般使用用例类,而不是使用这样的类。
在Scala中,类的构造可以包括:构造参数;类主体中调用的方法;在类主体中执行的语句和表达式。在Scala类的主体中声明的字段以类似于Java的方式处理,它们是在首次实例化该类时分配的。下面的Person类演示了可以在类体内执行的一些操作。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P90_22529.jpg?sign=1739297163-yztUMIuaGTokM8NmNtna8K3oOSqnMhSo-0-f2edad4c8a97a3bd87eee426fc52ec7f)
Scala REPL中的以下代码演示了该类的工作方式。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P90_22530.jpg?sign=1739297163-clWUKvoD0qwTAEaCAToGkCjGsVyqQ6jF-0-66857ef0a5ad611fcfb3689dc28838ce)
在Scala中,方法一般在类内部定义(就像Java),但是也可以在REPL中创建它们。本节将显示一些方法示例,以便可以看到语法。下面是如何定义名为double的方法,该方法采用一个名为a的整数输入参数并返回该整数的2倍,方法名称和签名显示在=的左侧。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P91_22533.jpg?sign=1739297163-P2DF98rDAenb3QHLaGS2DaRVibjrjYeh-0-c0aec4a54e8250b8cc56c46a838ad582)
def是用于定义方法的关键字,方法名称为double,输入参数a的类型Int为Scala的整数类型。函数的主体显示在右侧,在此示例中,它只是将输入参数a的值加倍。将该方法粘贴到REPL之后,可以通过给它一个Int值调用它。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P91_22534.jpg?sign=1739297163-JfncV6CRqMCoipzPQnP4TchTKXx2noZ8-0-96a3a8b9bf1d4c8c2255c3d09f4e8d46)
上一个示例未显示该方法的返回类型,但是可以显示它。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P91_22535.jpg?sign=1739297163-PascuqqhvZ7IegGJRjuFrjd2w6ZZ5JDx-0-e4f474c5e64d4691201cb0e101c1d650)
编写这样的方法会显式声明该方法的返回类型。有些人喜欢显式声明方法的返回类型,因为它使代码更容易维护。如果将该方法粘贴到REPL中,将看到它的工作方式与之前的方法相同。为了显示一些更复杂的方法,以下是一个使用两个输入参数的方法。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P91_22536.jpg?sign=1739297163-uWOs3xTbll0F4EGn1gngRjryKjJ7fmCF-0-a6012c11da9f041421a4b116eb0bc02f)
当一个方法只有一行时,可以使用上面的格式,但是,当方法主体变长时,可以将多行放在花括号内。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P91_22537.jpg?sign=1739297163-m0QmxrsHviAipUY4jKtLRIfzGCjjTYsu-0-519a7c76b7236ca0e3ea2d4902ac635c)
Scala的特质是该种语言的一大特色,可以像使用Java接口一样使用它们,也可以像使用具有实际方法的抽象类一样使用它们。Scala类还可以扩展和混合多个特质。Scala还具有抽象类的概念,我们需要了解何时应该使用抽象类,而不是特质。一种使用Scala特质的方法就像原始Java的接口,在其中可以为某些功能定义所需的接口,但是没有实现任何行为。举一个例子,假设想编写一些代码模拟任何有尾巴的动物,如狗和猫。在Scala中,我们编写了一个特质启动该建模过程,如下所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P92_22540.jpg?sign=1739297163-AVGWJQlbrnAGoq0Q6Eg8UjVESBYqoBhm-0-71ddb3a93e059abb92d7fd546e07af8b)
该代码声明了一个名为TailWagger的特质,该特质指出,扩展TailWagger的任何类都应实现startTail()和stopTail()方法。这两种方法都没有输入参数,也没有返回值。可以编写一个扩展特质,并实现如下方法的类。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P92_22541.jpg?sign=1739297163-rHGpJXVJOk6VNazH9qCojz077xLWOBuy-0-4edcabcf32d9cab9e46e53ca3af0dc3b)
可以使用extends关键字创建扩展单个特征的类。这演示了如何使用扩展特质类实现其中的方法。Scala允许创建具有特质的非常模块化的代码。例如,可以将动物的属性分解为模块化的单元。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P93_22543.jpg?sign=1739297163-aW5xlLEXjDRg9WJ096ztxlv3PtEOn5YQ-0-918acef03f0374d6f52e0b082eff54c2)
一旦有了这些小片段,就可以通过扩展它们并实现必要的方法创建Dog类。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P93_22544.jpg?sign=1739297163-GnJ3ctcxEsqRYMgM8tGd77tmDZtLQoLY-0-73473cedb231fccf04224eb588cedc97)
注意:如何使用extends和with从多个特征创建类。
2.4.2 函数式编程
Scala允许将两种方法结合起来使用,以面向对象编程风格和函数式编程风格,甚至混合风格编写代码。如果之前学习过Java、C++或C#之类的面向对象编程语言,这有利于我们理解相关的概念。但是,由于函数式编程风格对于许多开发人员来说仍相对较新,所以理解起来会有难度,可以从简单的概念入手。
函数式编程是一种编程风格,强调只使用纯函数和不可变值编写应用程序。函数式程序员非常渴望将其代码视为数学中的函数公式,并且可以将它们组合成为一系列代数方程式。使用函数式编程更像是数据科学家通过定义数据公式解决问题,驱使他们仅使用纯函数和不可变值,因为这就是我们在代数和其他形式的数学中使用的方法。函数式编程是一个很大的主题,实际上通过本小节只是了解函数式编程,显示Scala为开发人员提供的一些用于编写功能代码的工具。首先使用Scala提供的函数式编程模式编写纯函数。纯函数的定义为:函数的输出仅取决于其输入变量;它不会改变任何隐藏状态;不会从外界读取数据(包括控制台、Web服务、数据库和文件等),也不会向外界写入数据。由于此定义,每次调用具有相同输入值的纯函数时,总会得到相同的结果,例如,可以使用输入值2无限次调用double函数,并且始终获得结果4。按照这个定义,scala.math._包中的此类方法就是纯函数,例如abs、ceil、max、min,这些Scala String()方法也是纯函数:isEmpty、length和substring。Scala集合类的很多方法也作为纯函数,包括drop、filter和map。
相反,以下功能不纯,因为它们违反了定义。与日期和时间相关的方法都不纯,如getDayOfWeek、getHour和getMinute,因为它们的输出取决于输入参数以外的其他项,它们的结果依赖于这些示例中某种形式的隐藏输入、输出操作和隐藏输入。通常,不纯函数会执行以下一项或多项操作:
(1)读取隐藏的输入,访问未显式传递为输入参数的变量和数据。
(2)写隐藏的输出。
(3)改变它们给定的参数。
(4)与外界进行某种读写。
当然,应用程序不可能完全与外界没有输入、输出,因此人们提出以下建议:使用纯函数编写应用程序的核心,然后围绕该核心编写不纯的包装,以与外界交互。用Scala编写纯函数是关于函数编程的较简单部分之一,只需使用Scala定义方法的语法编写纯函数。这是一个纯函数,将给定的输入值加倍。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P94_22546.jpg?sign=1739297163-szqi9iSeklHj5eaWaXdNz7Q16cIHI4Po-0-0c24af31183d18886be520e67a29aa2f)
纯函数是仅依赖于其声明的输入及其内部算法生成其输出的函数。它不会从外部世界(函数范围外的世界)中读取任何其他值,并且不会修改外部世界中的任何值。实际的应用程序包含纯功能和不纯功能的组合,通常的建议是使用纯函数编写应用程序的核心,然后使用不纯函数与外界进行通信。
尽管曾经创建的每种编程语言都可能允许我们编写纯函数,但是Scala另一个函数式编程的特点是可以将函数创建为变量,就像创建String和Int变量一样。此功能有很多好处,其中最常见的好处是可以将函数作为参数传递给其他函数,例如:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P95_22548.jpg?sign=1739297163-YxnNZcJ6WnFiDiBRBJtfGQqi91h5yMQF-0-2253c990d4196f8585c3aa25e6ed6096)
在这些示例中,匿名函数被传递到map和filter中,将常规函数传递给相同的map。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P95_22549.jpg?sign=1739297163-8HomhhWjEseVkXRJiYW00BGsOpgnQeaU-0-702389e0c90f382598f091e6539bf151)
如这些示例所示,Scala显然允许将匿名函数和常规函数传递给其他方法。这是优秀的函数式编程语言提供的强大功能。如果从技术术语角度介绍,将另一个函数作为输入参数的函数称为高阶函数。将函数作为变量传递的能力是函数式编程语言的一个显著特征,就像map和filter将函数作为参数传递给其他函数的能力,可以帮助用户创建简洁而又易读的代码。为了更好地体验将函数作为参数传递给其他函数的过程,可以在REPL中尝试以下几个示例。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P95_22550.jpg?sign=1739297163-qOkxq1NLCw612Yw7WTZxo0dvOFlgpZm7-0-b6bacd14b3fc465ebdaa3fa535bffa9f)
这些匿名函数中的任何一个都可以写为常规函数,因此可以编写如下函数。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P96_22553.jpg?sign=1739297163-UyqWLd8m4inxFXqkFMRIGDFZfHYri4X7-0-2a6eaa62793a292e137e7f645036fdf4)
这些使用常规函数的示例等同于这些匿名函数示例:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P96_22554.jpg?sign=1739297163-WbSjQXd3O5PTmpBlF6kOijvIh7UGvV2G-0-a6804deb3615d3fc178960fe82805bb1)
函数式编程就像编写一系列代数方程式一样,并且由于在代数中不使用空值,因此在函数式编程中不使用空值。Scala的解决方案是使用构造,例如Option/Some/None类。虽然第一个Option/Some/None示例不处理空值,但这是演示Option/Some/None类的好方法,因此从它开始。
想象一下,我们想编写一种方法简化将字符串转换为整数值的过程,并且想要一种优雅的方法处理当获取的字符串类似“foo”而不能转换为数字时可能引发的异常。对这种函数的首次猜测可能是这样的:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P96_22555.jpg?sign=1739297163-OSXT86EFeTT7BHW5jyAiTcLXfmpOuwG3-0-cf589d7df3cbdd051e012d57a40a0600)
此函数的思路是:如果字符串转换为整数,则返回整数;如果转换失败,则返回0。出于某些目的这可能还可以,但实际上并不准确。例如,该方法可能接收到“0”,也可能是“foo”,或者可能收到“bar”等其他无数字符串。这就产生了一个实际的问题:怎么知道该方法何时真正收到“0”,或何时收到其他字符?但是,使用这种方法无法知道。Scala解决这个问题的方法是使用三个类:Option、Some和None。Some与None类是Option的子类,因此解决方案是这样的:
(1)声明toInt返回一个Option类型。
(2)如果toInt收到一个可以转换为Int的字符串,则将Int包裹在Some中。
(3)如果toInt收到无法转换的字符串,则返回None。
解决方案的实现如下所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P97_22557.jpg?sign=1739297163-Zr4hWvGqi8rTODktAZXaRIy2KbRTus1Y-0-7cf8f9e6375d26ef688dc2cd9cc605d8)
这段代码可以理解为:当给定的字符串转换为整数时,返回Some包装器中的整数,例如Some(1);如果字符串不能转换为整数,则返回None值。以下是两个REPL示例,它们演示了toInt的实际作用。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P97_22558.jpg?sign=1739297163-16vhoHXal4yRtB3g0ZswsPoGz4pep9Wd-0-eb39b50ce4b870d005215ed94cf98a3c)
如上所示,字符串“1”转换为Some(1),而字符串“foo”转换为None。这是Option/Some/None方法的本质,用于处理异常(如本例所示),并且相同的技术也可用于处理空值,我们会发现整个Scala库类以及第三方Scala库都使用了这种方法。
现在,假设我们是toInt()方法的使用者,该方法返回Option[Int]的子类,所以问题就变成如何使用这些返回类型?根据需求,主要有两个答案:①使用match表达式;②使用表达式。还有其他方法,但是这是两个主要方法,特别是从函数式编程的角度看。一种可能是使用match表达式,如下所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P97_22559.jpg?sign=1739297163-w4rdn500lczmx2SqfFe7a0pCcYXpvXc1-0-bb2d4d4d6a80ee46aa35a0f65223b1b4)
在此示例中,如果x可以转换为Int,则case执行第一条语句;如果x不能转换为Int,则case执行第二条语句。另一个常见的解决方案是使用for/yield组合。为了证明这一点,假设将三个字符串转换为整数值,然后将它们加在一起。for/yield解决方案如下所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P98_22561.jpg?sign=1739297163-ablhjw4ofuC2ItdQOEPsv2hwOtqNKrdj-0-eb2a793ec24856a01746186cf39d64cf)
该表达式结束运行时,y将是以下两件事之一:
(1)如果所有三个字符串都转换为整数,则y将为Some[Int],即包装在Some内的整数。
(2)如果三个字符串中的任何一个都不能转换为内部字符串,则y将为None。
可以在Scala REPL中对此进行测试,输入三个字符串变量,y的值为Some(6)。另一种情况是将所有这些字符串更改为不会转换为整数的字符串,我们会看到y的值为None。考虑Option类的一种好方法是:将其看作一个容器,更具体地说,是一个内部包含0或1项的容器,Some是其中只有一件物品的容器,None也是一个容器,但是里面什么也没有。
因为可以将Some和None视为容器,所以可以将它们进一步视为类似于集合类。因此,它们具有应用于集合类的所有方法,包括map()、filter()、foreach()等,例如:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P98_22563.jpg?sign=1739297163-GN2Yhcc5e13MuMEQFQbGiytluqSlTazG-0-c27da269625b2afbbde71d1fae566e82)
第一个示例显示数字1,而第二个示例不显示任何内容。这是因为toInt("1")计算为Some(1),Some类上的foreach()方法知道如何从Some容器内部提取其中的值,因此将该值传递给println。同样,第二个示例不打印任何内容,因为toInt("x")计算为None,None类上的foreach()方法知道None不包含任何内容,因此不执行任何操作。
2.4.3 集合类
Scala集合类是一个易于理解且经常使用的编程抽象,可以分为可变集合和不可变集合。可变集合可以在必要时进行更改、更新或扩展,但是不可变集合不能更改。大多数集合类分别位于scala.collection、scala.collection.immutable和scala.collection.mutable包中。我们使用的主要Scala集合类见表2-1。
表2-1 我们使用的主要Scala集合类
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-T99_22137.jpg?sign=1739297163-FwMXDZQUWlPPq5OjTeQquIV6xfnIc4Kl-0-ca496e774e708fcdecdba0a5f868d3f9)
ArrayBuffer是一个可变序列,因此可以使用其方法修改内容,并且这些方法类似于Java序列上的方法。要使用ArrayBuffer,必须先将其导入。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P99_22565.jpg?sign=1739297163-erFIXewSDDXRtqqx0YJfQwQgNAe9tV7c-0-0d700eb78ee9f80d80189c296aa34c36)
将其导入本地范围后,会创建一个空的ArrayBuffer,可以通过多种方式向其中添加元素,如下所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P99_22566.jpg?sign=1739297163-ocadSAyXEUV2tQKWJ9LYVy962RtcB1G8-0-b9ce2784b378101a736fcbd5d7418d24)
这只是创建ArrayBuffer并向其中添加元素的一种方法,还可以使用以下初始元素创建ArrayBuffer,通过以下几种方法向此ArrayBuffer添加更多的元素。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P99_22567.jpg?sign=1739297163-WcV8IxCvaq9UQYuGTmCVFRYhSewZ8hkl-0-9477cc921a126d847dfcf63311f410ac)
还可以使用“-=”和“-=”方法从ArrayBuffer中删除元素。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P100_22569.jpg?sign=1739297163-24iniDttIK7qyq67t12ir2ZbtxV7abgE-0-86c316644b61bce452c89f86aaa9d020)
简要概述一下,可以将以下几种方法用于ArrayBuffer。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P100_22570.jpg?sign=1739297163-Oq8UcJlrQkcbD3SIC2A3CdoviznnW2zC-0-bcc3a3ae87c51bda4fe3bae401eb37f5)
List类是线性的、不可变的序列。这意味着,它是一个无法修改的链表,每当要添加或删除List元素时,都可以从一个现存的List中创建一个新元素List。这是创建初始列表的方法:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P101_22573.jpg?sign=1739297163-AbXUoELgw03v4WuTr37R6WCfDsgbi9Re-0-db44f7b458dc9a1fafc8c97589eb190b)
由于列表是不可变的,因此无法向其中添加新元素。相反,可以通过在现有列表之前或之后添加元素创建新列表,例如给定此列表:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P101_22574.jpg?sign=1739297163-yeG4TpAbrXl8qoyrmbOHYAYM565cLRrs-0-d898343fdc1197d2e4eca9bcec550fc0)
也可以将元素追加到List,但是由于List是单链接列表,因此实际上只应在元素之前添加元素;向其添加元素是一个相对较慢的操作,尤其是在处理大序列时。如果要在不可变序列的前面和后面添加元素,则需要使用Vector。由于列表是链接列表类,因此不应尝试通过大列表的索引值访问它们。例如,如果具有一个包含一百万个元素的列表,则访问myList(999999)之类的元素将花费很长时间,如果要访问这样的元素,则需要使用Vector或ArrayBuffer。下面的例子展示了如何遍历列表的语法,给定这样的List:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P102_22576.jpg?sign=1739297163-oeMq9SlnTe1ORsKaqJ67rW7J793Il2GY-0-72ddfabf669af6864a327f06bd70b06a)
这种方法的最大好处是,它适用于所有的序列类,包括ArrayBuffer、List、Seq和Vector等。确实还可以通过以下方式创建完全相同的列表:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P102_22577.jpg?sign=1739297163-s80eRzhG2oIvHEKvL3NeFROrlIbtsPoh-0-7832a72838c2d8cde5173f2d968c3a74)
这是有效的,因为一个List是以Nil元素结尾的单链列表。
Vector类是一个索引的、不变的序列,可以通过Vector元素的索引值非常快速地访问它们,例如访问listOfPeople(999999)。通常,除了对Vector进行索引和不对List进行索引的区别外,这两个类的工作方式相同。可以通过以下几种方法创建Vector:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P102_22578.jpg?sign=1739297163-flVrn91rZEVlpx8tk7Xu8DAB21gWXC8i-0-5d71f4f9c64b2253f69d3309a1ea5b92)
由于Vector是不可变的,因此无法向其中添加新元素,可以通过将元素追加或添加到现有Vector上创建新序列。例如给定此向量:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P102_22580.jpg?sign=1739297163-bWMv1NdHDORQoKNz9czFpVZhrwAaSVNH-0-da95b0e3d8b9421a9ae37e1316cf1f17)
也可以在前面加上这样的内容:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P102_22581.jpg?sign=1739297163-6wQLz6YRAveFMrtzgiMdMrCCNsoepDjc-0-24954e80533ccaae6aea187c98d2cab5)
因为Vector不是链表(如List),所以可以在它的前面和后面添加元素,并且两种方法的速度相似,循环遍历Vector元素,就像ArrayBuffer或List:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P103_22584.jpg?sign=1739297163-usVK4rT9NDLOWcAolM8AnyNvEcd25dd0-0-adf97b67ae9ce7463d32de2c1522feb2)
Map类文档将Map描述为由键值对组成的可迭代序列。一个简单的Map看起来像这样:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P103_22585.jpg?sign=1739297163-afJJMkD8v2pPlMS7Qve61Q5VAeDfo3Od-0-bf5ee538c806968ed6348b816fbf95d3)
Scala具有可变和不变的Map类。要使用可变的Map类,请首先导入它:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P103_22586.jpg?sign=1739297163-BtXGGnHHARG8NqozpaqsaK04FE8ImP4b-0-964917f0626955a1e141a77d689e623d)
然后可以创建一个像这样的Map:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P103_22587.jpg?sign=1739297163-1WwH36gPVgo4Z6nzqqUq3OQmZZKtD0FW-0-8aab6492342eca69bdc995c0e46a77bd)
现在,可以使用“+=”向Map添加一个元素,如下所示:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P103_22588.jpg?sign=1739297163-48GYWQf4ZrhAQLZe8rkO41f7nhxqpma4-0-8d0fdf922d35eb4d5b157a42b4e73b8b)
还可以使用“+=”添加多个元素:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P103_22589.jpg?sign=1739297163-XqrJzobSyJ6jV1IljIbFdi2tKH5fKwrq-0-e4875e6a9222b5a38fdbdfa70a818908)
可以使用“++=”从其他Map添加元素:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P104_22591.jpg?sign=1739297163-UHeJyYyvbmOntIMFX3zfvEYBgxqz3VqH-0-b20f28155c82286674f20030c8455eb1)
使用“-=”和“-=”并指定键值从Map中删除元素,如以下示例所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P104_22592.jpg?sign=1739297163-crDnJ2DIMKv0C7UmOOb7cbXdoLZ9WuXK-0-04ae91a91ba7b729b783021ad4602c31)
可以通过将Map元素的键重新分配为新值更新它们:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P104_22593.jpg?sign=1739297163-lsT4tW8DsZnBPrdJEcMJwqJVE8iWm92u-0-860b6c009869e8bf661ddecb4e9f5795)
有几种不同的方法可以迭代Map中的元素,给定一个样本Map:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P104_22594.jpg?sign=1739297163-rvQSbaU0R7IuMIhpzin0J0bpBlIc3ThT-0-a5794dd2db3507fcacc5a27653863dfc)
循环所有Map元素的一种好方法是使用以下的for循环语法:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P104_22595.jpg?sign=1739297163-KOc7GwtPcMvwS9FPUWcAYZKh0oCZKV9A-0-d9b609985768522bcf52df599658c8f5)
将match表达式与foreach()方法一起使用也很容易理解:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P105_22597.jpg?sign=1739297163-YfDHeal3OObTZFFF3mfVc98nfz766S0X-0-e37e5f1b3dfac9b812a9356b5c6e6df1)
Scala Set类是一个可迭代的集合,没有重复的元素。Scala具有可变和不变的Set类。要使用可变的Set,首先导入它:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P105_22598.jpg?sign=1739297163-H6RU4dJcjy1OhDG3BZbUCbQs6giUlO79-0-2c3dc113a3336f31d85841307623de7a)
可以使用“+=”“++=”将元素添加到可变的Set中,还有add()方法。这里有一些例子:
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P105_22599.jpg?sign=1739297163-bdPzcuAS6aN8Ad0rIEqWH3g3b3EXpDtF-0-fba2d123970e0bf0f39527fb7a10fd89)
如果尝试将值添加到其中已存在的集合中,则该尝试将被忽略。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P105_22600.jpg?sign=1739297163-yFGQHuP9nbwlOBbe46reBZGyiSQCQgNw-0-8ce7b7eb308496c6e6bcc568899a5f82)
Set还具有add()方法,如果将元素添加到集合中,则返回true;如果未添加元素,则返回false。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P105_22601.jpg?sign=1739297163-4pokLImaUIKU13TUC8UURnDgyWrDWtmX-0-3b9189db89ca3b9d7487a4eadae0af6f)
可以使用“-=”和“-=”方法从集合中删除元素,如以下示例所示。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P105_22602.jpg?sign=1739297163-Bl87ts4h2eWD3hgu6XsFzlAV39e4gckM-0-dcb7e4974d6c089a9f943140bb10bc63)
如上例所示,还有更多使用集合的方法,包括clear()和remove()。
![](https://epubservercos.yuewen.com/70A2A8/19549640908915106/epubprivate/OEBPS/Images/Figure-P106_22605.jpg?sign=1739297163-qnPjVauNa4L8mTqublUbzHQFnpKgJC5T-0-7d982a39160bb1e1dce29a4951088119)