第10步 使用集和映射
由于Scala想让你同时享有函数式和指令式编程风格的优势,其集合类库特意对可变和不可变的集合进行了区分。举例来说,数组永远是可变的;列表永远是不可变的。Scala还提供了集(set)和映射(map)的可变和不可变的不同选择,但使用同样的简称。对集和映射而言,Scala通过不同的类继承关系来区分可变和不可变版本。
例如,Scala的API包含了一个基础的特质来表示集,这里的特质与Java的接口定义类似。(你将在第11章了解到更多关于特质的内容。)在此基础上,Scala提供了两个子特质(subtrait),一个用于表示可变集,另一个用于表示不可变集。
在图3.2中可以看到,这3个特质都叫作Set。不过它们的完整名称并不相同,因为它们分别位于不同的包。Scala API中具体用于表示集的类,如图3.2中的HashSet类,分别扩展自可变或不可变的特质Set。(在Java中“实现”某个接口,而在Scala中“扩展”或“混入”特质。)因此,如果想要使用一个HashSet,则可以根据需要选择可变或不可变的版本。创建集的默认方式如示例3.5所示。
图3.2 Scala集的类继承关系
示例3.5 创建、初始化并使用一个不可变集
在示例3.5的第一行,定义了一个新的名称为jetSet的var,并将其初始化为一个包含两个字符串——"Boeing"和"Airbus"的不可变集。由这段代码可知,在Scala中可以像创建列表和数组那样创建集:通过调用Set伴生对象的名称为apply的工厂方法。在示例3.5中,实际上调用了scala.collection. immutable.Set的伴生对象的apply方法,返回了一个默认的、不可变的集的对象。Scala编译器推断出jetSet的类型为不可变的Set[String]。
要向集中添加新元素,可以对集调用+方法,传入这个新元素。无论是可变的还是不可变的集,+方法都会创建并返回一个新的包含了新元素的集。在示例3.5中,处理的是一个不可变的集。可变集提供了一个实际的+=方法,而不可变集并不直接提供这个方法。
本例的第二行,即“jetSet += "Linear"”在本质上是如下代码的简写:
因此,在示例3.5的第二行,实际上是将jetSet这个var重新赋值成了一个包含"Boeing"、"Airbus"和"Linear"的新集。示例3.5的最后一行打印出这个集是否包含"Cessna"。(正如你预期的那样,它将打印false。)
如果你想要的是一个可变集,则需要做一次引入(import),如示例3.6所示。
示例3.6 创建、初始化并使用一个可变集
示例3.6的第一行引入了scala.collection.mutable。import语句允许在代码中使用简称,而不是更长的完整名。这样一来,当你在第三行用到mutable.Set的时候,编译器就知道你指的是scala.collection.mutable. Set。在那一行,将movieSet初始化成一个新的包含字符串"Spotlight"和"Moonlight"的新的可变集。接下来的一行通过调用集的+=方法将"Parasite"添加到可变集里。前面提到过,+=实际上是一个定义在可变集上的方法。只要你想,也完全可以不用ovieSet += "Parasite"这样的写法,而是将其写成movieSet.+= ("Parasite")。[8]
虽然由可变和不可变集的工厂方法生成的默认集的实现对于大多数情况来说都够用了,但是偶尔可能也需要一类特定的集。幸运的是,语法上面并没有大的不同。只需要简单地引入需要的类,然后使用其伴生对象上的工厂方法即可。例如,如果你需要一个不可变的HashSet,则可以:
Scala的另一个有用的集合类是Map。与集类似,Scala也提供了映射的可变和不可变的版本,用类继承关系来区分。如图3.3所示,映射的类继承关系与集的类继承关系很像。在scala.collection包里有一个基础的Map特质,还有两个子特质,都叫Map,可变的那个子特质位于scala.collection. mutable,而不可变的那个子特质位于scala.collection.immutable。
图3.3 Scala映射的类继承关系
Map的实现,如图3.3中的HashMap,扩展自可变或不可变的特质。与数组、列表和集类似,可以使用工厂方法来创建和初始化映射。
示例3.7展示了一个可变映射的具体例子。示例3.7的第一行引入了可变的Map特质。接下来定义了一个名称为treasureMap的val,并初始化成一个空的,且以整数为键、以字符串为值的可变映射。这个映射之所以是空的,是因为执行了名称为empty的工厂方法并指定了Int作为健类型,String作为值类型。[9]在接下来的几行,通过->和+=方法向映射添加了键/值对(key/value pair)。正如前面演示过的,Scala编译器会将二元(binary)的操作,如1 -> "Go to island.",转换成标准的方法调用,即(1).->("Go to island.")。因此,当你写1-> "Go to island."时,实际上是对这个值为1的整数调用->方法,传入字符串"Go to island."。可以在Scala的任何对象上调用这个->方法,它将返回包含键和值两个元素的元组。[10]然后将这个元组传递给treasureMap指向的那个映射对象的+=方法。最后一行将打印出treasureMap中键2对应的值。运行这段代码以后,变量step2将会指向"Find big X on ground."。
示例3.7 创建、初始化并使用一个可变映射
如果你更倾向于使用不可变的映射,则不需要任何引入,因为默认的映射就是不可变的,如示例3.8所示。
示例3.8 创建、初始化并使用一个不可变映射
由于没有显式引入,当你在示例3.8中的第一行提到Map时,得到的是默认的那个scala.collection.immutable.Map。接下来将5组键/值元组传递给映射的工厂方法,返回一个包含了传入的键/值对的不可变映射。如果运行示例3.8中的代码,它将打印出“IV”。