14. 集合:Map的使用
在Scala中,Map(映射)是基于键值对(key/value)的集合,并且键是唯一的,就像Java的Map,Ruby的Hash,或者Python的字典。14.1小节介绍了Map的创建、不可变Map以及可变Map的基本使用。
介绍完Map的基本使用后,14.2小节将会选择不同的Map实现来满足特殊的场景和功能。接着,14.3和14.4小节将分别涵盖不可变和可变Map的添加、更新、删除元素的介绍。
Scala中的Map相比较于Java,最大的不同在于Scala中的Map默认是不可变的,如果以前没有用过不可变集合,在Map中尝试添加、删除、修改元素,将会感受到非常大的惊喜。
除了添加、删除、替换元素之外,还涵盖其他常用的Map功能,比如使用键和值(14.5至14.8小节)、以及遍历(14.9小节)、排序(14.10小节)和过滤(14.11小节)Map。
14.1 创建和使用Map
问题
你想要在Scala应用中创建Map并使用它,即一个包含键值对的数据结构,就像Java的Map,Python的字典,或者Ruby的Hash。
解决方案
有时候,你需要用到键值对的数据结构,在Scala中可以直接创建不可变和可变的Map类型。
不可变Map
使用不可变的Map,不需要使用任何import语句,直接创建一个Map实例:
val a = Map(
"AL" -> "Alabama",
"AK" -> "Alaska"
)这个例子创建了一个类型为 [String, String] 的不可变Map,表示key和value都是String类型。在第一个元素中,字符串AL是key(键),Alabama是value(值)。
一旦创建了一个不可变的Map,就可以指定要添加、更新和删除的元素,同时将生成的结果Map赋值给一个新的变量,下面的例子展示了这些:
当使用不可变Map,可以创建一个var变量,这样就可以不用创建不同名字的变量为结果Map赋值:
可变Map
要创建一个可变 Map,要么使用import语句将其带入作用域中,要么就在创建实例时指定scala.collection.mutable.Map类的完整路径。当作用域中有了可变的Map,接下来就可以使用其中的功能。下面的例子将展示一些基本功能:
讨论
和其他语言中的Map一样,Scala中的Map是键值对的集合。如果熟悉Java中的Map,Python的字典,或者Ruby的Hash,那么Scala中的Map就很容易理解了。只需要学习一些新的东西,包括Map类中可用的方法,Map适用的场景,比如创建一个有序的Map。
注意在Map的括号内创建一个元组所使用的语法:
由于元组也能被声明为 ("AL", "Alabama"),所以也可以像这样创建Map:
为了清楚地表明正在使用可变的Map,有个技巧是可以在导入可变Map时给它指定一个别名,然后在使用时引用这个别名,如下所示:
9.3小节对这个技巧进行了更多的介绍。
14.2 选择一种Map实现
问题
你需要选择合适的Map类来解决特定的问题。
解决方案
在Scala中,可用的Map类型很多,甚至还能使用Java的Map类。讨论中提供了大多数类的全部列表,但其中一些最受欢迎的类包括:
不可变 Map
一个基础的不可变map,不能保证其键返回的顺序
可变 Map
一个基础的可变map,不能保证其键返回的顺序
SortedMap
按照键的顺序返回其元素
LinkedHashMap
按元素插入的顺序返回元素
VectorMap
通过基于 vector/map 的数据结构中存储元素来保持插入顺序
WeakHashMap
根据Scaladoc,“如果键不再被强引用,则会删除键值对”
其他几个章节中详细介绍了基本的不可变和可变Map,因此这里不多做介绍。SortedMap和LinkedHashMap包含在小节14.10中,因此这里只简单接触一下。
基本的不可变和可变Map
如果不关心排序或者插入顺序,可以使用默认的基本的不可变Map或者14.1小节所示的可变Map。
SortedMap
如果想要按照键的顺序返回Map中的元素,可以使用SortedMap:
将代码粘贴到REPL中,可以得到以下输出:
使用LinkedHashMap、VectorMap或者ListMap记住插入顺序
如果想要Map记住元素的插入顺序,可以使用LinkedHashMap、VectorMap或者ListMap。Scala中只有可变的LinkedHashMap,所以使用前先导入:
当创建一个LinkedHashMap实例,它会按照元素的插入顺序存储元素:
根据Scaladoc,VectorMap “使用基于vector/map的数据结构实现不可变的Map,从而保留元素的插入顺序...VectorMap的查找是常数时间复杂度,但代价是使用额外的内存,其他操作的性能通常较低。” 它与基本的不可变Map类似,但保留了元素的插入顺序:
也可以使用不可变的ListMap,但这是一个有点特别的结构。在内部,元素像List一样存储,最新的元素存储在头部位置。根据ListMap的Scaladoc( https://oreil.ly/OrZpT ),“ListMap迭代器和遍历方法会按照键值对被首次插入的顺序访问,” 也就是从尾部到头部的访问顺序。因此,ListMap的性能可能很差,只适用于比较小的数据量。有关更多性能的详细信息,请参阅不可变的ListMap的Scaladoc。
讨论
表14-1展示了Scala基本的Map类和特质,并提供了相应的简单描述。括号中的文本摘自每个类的Scaladoc。
表14-1:基本的Map类和特质
collection.immutable.Map
如果不引入任何东西,这就是默认的,通常意义上的不可变Map。
collection.mutable.Map
基本Map的可变版本。
collection.immutable.VectorMap
“使用基于vector/map的数据结构实现不可变的Map,从而保留元素的插入顺序;查找是常数时间复杂度,但代价是使用额外的内存,其他操作的性能通常较低。”
collection.mutable.LinkedHashMap
所有遍历元素的方法都以插入的顺序访问元素。
collection.immutable.SortedMap
Map的键按照顺序返回。“键值对根据键的scala.math.Ordering定义排序规则。”
虽然这些都是最常见的Map,Scala还提供了更多的Map类型。详细见表14-2,括号中的文本摘自每个类的Scaladoc。
表14-2:更多的Map类和特质
collection.immutable.HashMap
“使用压缩的哈希数组映射的前缀树实现的不可变Map。”
collection.immutable.TreeMap
“当使用范围查询或需要按顺序遍历时,此类是最佳的。”
collection.mutable.WeakHashMap
java.util.WeakHashMap的包装类,“如果键不再被强引用,Map会移除这个键值对。”
collection.immutable.ListMap
“使用基于List的数据结构实现不可变Map”;一些操作的时间复杂度是O(n),“所以只适用于比较小的数据量。”
还有一些其他的Map类参考Scala集合库( https://oreil.ly/EB8AD ),“提供了Scala 2.13标准集合的各种新增内容。”
比如MultiDict类替代了旧的MultiMap类,“可以将键和一组值进行关联。”
还有SortedMultiDict类,是MultiDict的有序版本。
并行Map类
从Scala 2.13开始,并行的集合库( https://oreil.ly/WdrEV )被迁移到了一个新的JAR中,并且在Github上进行维护。库中内置了并行/并发的Map实现,比如collection.parallel.immutable.ParMap、collection.parallel.immutable.ParHashMap、collection.parallel.mutable.ParHashMap等等。详情可以参考链接中的Github项目。
另见
如果看重Map的性能,请参阅11.2小节。
14.3 为不可变Map添加、更新或删除元素
问题
你想要为不可变的Map添加、更新或者删除元素。
解决方案
无法在不可变的Map上原地更新元素,所以:
对于不同的目的使用正确的函数方法。
记得将结果赋值给一个新的变量。
为了更好地理解这种方式,下面的例子使用了一系列val变量定义的不可变Map。首先,创建一个赋值为val的不可变Map:
添加元素
使用 + 方法添加一个元素,在这个过程中将结果赋值给一个新的变量:
使用 ++ 方法添加两个或者更多的元素:
更新元素
更新一个不可变Map的键值对,需要用 + 方法对键和值重新赋值,新值会替代旧值:
更新多个键值对,可以将新的元组值放在map或者sequence进行操作:
删除元素
使用 - 方法删除一个元素:
使用 -- 方法删除多个元素:
讨论
可以将不可变Map声明为var变量,然后将结果重新赋值给这个相同的变量。使用var,前面的例子如下所示:
上面例子中,由于a变量被定义成了var,所以在代码的每一步都可以被重新赋值。
14.4 为可变Map添加、更新或删除元素
问题
你想在可变的Map中添加、删除或者更新元素。
解决方案
可以这样对可变的Map添加元素:
使用赋值语法:map(key) = value
使用 += 方法
使用 ++= 方法
删除元素:
使用 -= 方法
使用 --= 方法
可以将键重新进行赋值来更新元素。讨论中展示了可以使用的其他方法,包括put、filterInPlace、remove和clear方法。
假设有如下的可变Map:
可以通过给键指定值的方式添加一个元素:
也可以使用 += 方法添加元素:
使用 ++= 从另一个集合添加多个元素:
使用 -= 方法,通过指定元素的键从Map中删除一个元素:
使用 --= 方法,根据键删除多个元素:
通过赋新值给元素的键来更新元素:
讨论中展示了修改可变Map的更多方法。
讨论
解决方案中展示的是最常用的方法。也可以这些方法:
使用put方法添加一个键值对,或者替换已有的元素。
使用filterInPlace方法保留满足条件的元素。
使用remove方法删除对应的元素。
使用clear方法删除所有元素。
比如,给定一个可变Map:
下面的例子展示了如何使用这些方法:
例子中的注释解释了像put和remove方法会返回Some和None值。
14.5 访问Map的值(避免异常)
问题
你希望访问存储在Map中的单个值时,避免抛出异常。比如,给定一个Map:
可以像访问数组元素的方式一样来访问一个Map中键所关联的值。
然而,如果Map没有包含所请求的键,会抛出java.util.NoSuchElementException异常:
解决方案
为了避免异常,使用:
创建一个Map时使用withDefaultValue方法。
访问元素时使用getOrElse方法。
使用get方法返回一个Some或者None。
例如,给定一个Map:
避免这个问题的一种方式是在创建Map时使用withDefaultValue方法:
顾名思义,该方法会创建一个默认值,如果键不存在,就会返回这个默认值。
另一种方法是在尝试寻找值时使用getOrElse方法。当指定的键不存在时会返回默认值:
也可以使用get方法返回Option对象:
这三种方式都很不错,因为它们提供了不同的工作方式,可以根据喜欢的编程风格进行使用。
14.6 测试Map中键或值的存在
问题
你想判断Map中是否包含给定的键或者值。
解决方案
可以使用contains或者get方法测试Map中是否包含一个键。使用valuesIterator方法测试Map中是否包含一个值。
测试键
为了测试一个Map中是否包含一个键,使用contains方法是最直接的方式:
根据需要,也可以使用get方法返回一个None或者Some来判断:
比如,可以在match表达式中使用这种方式:
注意,前面小节已经展示了尝试直接访问一个不存在的键可能会抛出异常:
测试值
为了测试一个Map中是否包含一个值,使用valuesIterator方法搜索该值,结合contains方法测试:
这是可行的,因为(a)valuesIterator方法返回一个迭代器:
然后(b)如果Map中存在这个值,contians方法会返回true。如果需要对Map的值执行更多搜索操作,也可以使用以下方法:
因为exists方法需要一个函数,所以可以将此技术用于更复杂的键值和自定义的算法。
讨论
像这样将方法连起来,一定要注意中间结果,尤其是比较大的集合。在上面的例子中,我原本使用了values方法去获取Map中所有的值,但是该方法返回的结果是一个新的集合。如果是一个大的Map,这将会消耗很多内存。相反,valuesIterator方法则返回一个轻量级的迭代器。
另见
14.5小节展示了访问Map的键时如何避免产生异常。
14.7小节介绍了values和valuesIterator方法。
14.7 从Map中获取所有的键和值
问题
你想要获取Map中所有的键或者值。
解决方案
键
为了获取键,可以使用这些方法:
使用keySet方法返回以Set的方式获取键。
使用keys方法获取一个Iterable对象。
使用keysIterator方法返回以迭代器的方式获取键。
这些方法的使用如下所示:
值
为了获取值,可以使用这些方法:
使用values方法获取一个Iterable对象。
使用valuesIterator方法获取一个Iterator对象。
这些方法的使用如下所示:
讨论
如这些例子所示,keysIterator和valuesIterator方法都会从Map数据中返回一个迭代器。当操作一个大Map时,通常需要使用这些方法,因为它们不会创建一个新的集合;它们只提供一个迭代器来遍历现有元素。
还需注意,如果想要转换Map中值,可以使用view.mapValues或者transform方法,参阅14.9小节。
14.8 找到Map中最大(或最小)的键或者值
问题
你想在Map中查找最大或者最小的键或者值。
解决方案
使用Map的max和min方法,或者使用keysIterator或者valuesIterator的方式,取决于具体的需求。
两种查找最大最小键的方式将会在解决方案中进行展示,查找最大最小值的方式将会在讨论中进行展示。
为了展示使用Map键的方法,首先创建一个简单的Map:
在这个Map中,键的类型是String,哪个键是“最大的”取决于如何定义。通过调用Map的max和min方法找到String自然排序的最大或最小的键:
因为 Teri 中的 T 是名字中字母排序最大的,所以作为最大值被返回了。Al 作为最小值被返回也是同样的道理。
也可以调用keysIterator方法返回一个scala.collection.Iterator对象,然后调用max和min方法:
此外,调用keysIterator结合reduceLeft方法,同样能找到相同的最大和最小值:
这种方式很灵活,因为一旦最大的定义变成最长字符串,可以直接调整为比较字符串的长度:
讨论
想要从一个Map中获取最大和最小的值,可以使用valuesIterator方法返回一个迭代器,然后调用max或者min方法:
这是可以工作的,因为例子中Map的值是Int类型,含有一个隐式的Ordering。根据Scala的官方文档( https://oreil.ly/cYPQe ),“隐式的方法 Int => Ordered[Int] 会通过scala.Predef.intWrapper被自动的引入。”
相反,如果Map中的值类型没有提供一个Ordering,这个方式将会行不通。参阅13.14,“排序一个集合”,和参阅12.11,“排序数组”,获取很多关于scala.math.Ordered或者scala.math.Ordering特质的实现。
同样的,也可以使用reduceLeft的方式获取最大和最小值:
使用reduceLeft的好处是可以使用自定义的算法比较任意的值类型,这代表了可能需要对更复杂的数据类型执行操作。下面的例子展示了如何将reduceLeft方法与复杂的算法结合使用:
现在已经知道了如何访问x和y的值,然后就可以创建任何自定义的算法进行比较。
另见
参阅13.10小节 “用reduce和fold方法遍历集合”
参阅14.10小节
14.9 遍历Map
问题
你想要遍历Map中的每个元素。
解决方案
遍历Map中的元素有几种不同的方法。假设有如下一个Map:
一个不错的方式是使用for循环语句遍历所有的元素:
使用foreach配合匿名函数,可读性也不错:
下面的例子展示了如何使用元组语法来访问键和值字段:
注意:例子中的电影评分数据来自Programming Collective Intelligence by Toby Segaran(O’Reilly)一书。
讨论
注意在Scala 3中,可以用三种不同的方式编写带有匿名函数的foreach方法:
在每个例子中,花括号内的代码都充当匿名函数。
根据不同遍历Map的方式,你可以只访问Map中的键或者值。
键
如果只想访问Map中的键,可以使用keysIterator方法得到一个轻量级的迭代器来获得所有的键:
keys方法返回一个Iterable对象,因此它会创建一个新的中间集合(如果是个大Map,可能会有性能问题),但是简单易用:
值
如果想要遍历Map操作其中的值,可以使用定义在MapView特质中的mapValues方法。首先,在Map上调用view方法创建一个MapView,然后调用mapValues方法。它可以在遍历的每个值上应用一个函数,然后返回修改后的Map:
由于视图以非严格或懒惰的方式实现转换,因此在调用mapValues之前创建视图只会创建一个轻量级迭代器:
当一个Map特别大时,这种方式特别有效。可以参阅11.4小节“在集合上创建惰性视图”获取更多信息。
相反,可以使用values和map方法来解决这个问题:
但请注意,此过程的第一步将创建一个中间的Iterable对象:
因此,对于大的Map来说,这种方式将会产生额外的中间结果,导致性能或者消耗内存的问题。
转换方法
如果想要遍历Map的时候对值进行转换,另一个方式是使用transform方法可以从一个已知的Map中创建一个新的Map。和mapValues方法不同,transform方法可以同时使用键和值实现转换:
对于更复杂的情况,还可以在初始的Map上创建一个视图,然后在该MapView上调用map方法,这样就可以访问Map的键和值:
14.10 根据键或值对Map排序
问题
你想要按照键或者值对未排序的Map元素进行排序。
解决方案
假设有一个基本的不可变Map:
可以使用sortBy方法按照键从低到高对Map进行排序,然后将结果存储在一个可变的LinkedHashMap或者不可变的VectorMap。两种方案如下所示:
也可以使用sortWith方法传入自定义的算法,按照升序或者降序对键进行排序,然后将结果存储在一个LinkedHashMap中:
这种语法将会在讨论中进行解释。
使用sortBy方法按照值对Map排序:
也可以使用sortWith方法按照升序或者降序对值进行排序:
除了使用LinkedHashMap,也可以将结果存在一个不可变的VectorMap中。当影响到性能时,一定要测试一下这两种Map类型,看看哪种最适合实际的应用场景。
讨论
将解决方案分解成几个小的步骤有助于理解。首先,创建一个基本的不可变Map:
接下来是grades.toSeq创建一个双元素元组的序列,也就是Seq[(String, Int)]:
转换成Seq的原因是它有现成的排序方法:
在上面的例子中,_._1 语法表示元组的第一个元素,也就是键。同样的,_._2 表示值。
一旦Map的数据按预想的方式完成排序,将其保存到一个LinkedHashMap、VectorMap或者ListMap中,保证排序的顺序:
LinkedHashMap只有可变的版本,VectorMap是一个不可变的Map。还有一个不可变的ListMap,但只建议用于小的Map。可以按照不同的实际情况进行使用。
关于_*
代码中的 _* 部分需要花点时间去理解。它的作用是将数据转换,然后将其作为多个参数传给LinkedHashMap(或者VectorMap、ListMap)。如果曾经在Unix系统上使用过xargs命令,它工作方式与之类似,将一系列元素作为输入,每次将一个元素传递给下一个命令。
将代码分解成多行,可以更容易理解。sortBy方法返回一个Seq[(String, Int)],即一个元组序列:
不幸的是,不能直接使用元组序列来构造VectorMap、LinkedHashMap或者ListMap:
但是由于VectorMap伴生对象的apply方法接受一个Tuple2的变长参数,可以调整seqOfTuples使用 _* 将Tuple2元素序列转换为一系列单独的Tuple2值。为VectorMap的apply方法提供了所需的输入:
另一个了解 _* 如何工作的方式是自定义接受变长参数的方法。下面例子中的printAll方法接受一个参数,String类型的变长字段:
然后像这样创建一个List:
可以发现List不能被传入printAll方法;和前面的例子一样会引起错误:
但是使用 _* 可以调整List,让它可以作用于printAll方法,像下面这样:
如果有Unix背景,可以将 _* 看做是一个splat操作符。该操作符将序列的每个元素作为单独的参数传递给printAll方法,而不是将fruits当做单个的List参数传入。
14.11 过滤Map
问题
你想要过滤Map中包含的元素,要么直接修改可变的Map,要么是在不可变的Map上应用过滤算法生成一个新的Map。
解决方案
在可变的Map上使用filterInPlace方法选择需要保留的元素,使用filterKeys或者filterKeys或者filter方法过滤可变或者不可变Map的元素,不要忘记将结果赋值给一个新的变量。
可变Map
通过filterInPlace方法指定要保留的元素,过滤可变Map的元素:
如上所见,filterInPlace原地修改了可变Map。如例子中隐含的匿名函数签名:
算法可以测试每个元的键和值,决定哪些元素可以保留在Map中。
取决于“filter”的定义,也可以像remove和clear方法那样删除Map中的元素,可以参阅14.3小节。
可变和不可变Map
使用可变或者不可变Map时,用filterKeys方法配合断言选择保留Map中的哪些元素。使用这个方法时,首先调用Map中的view方法创建一个MapView,然后记得将过滤后的结果赋值给一个新的变量:
断言对于要在新集合中保留的元素返回true,对于不想保留的元素返回false。
注意使用这种方式,如果没有在结尾调用toMap方法,将会在REPL中看到如下输出:
这里 MapView() 是因为view方法返回的是一个惰性视图,只有通过调用toMap等方法强制执行时,才能真正执行计算。(参阅11.4小节获取更多关于视图的信息。)
如果算法较长,可以定义一个匿名函数(或者方法),然后在调用filterKeys时使用,而不是用匿名函数的方式。比如,首先定义如下例子中的方法,当输入参数为1时返回true:
然后将该函数传给filterKeys方法:
也可以让filterKeys配合一个Set指定需要保留的元素:
讨论中展示了其他过滤方法,例如如何根据Map的值进行过滤。
讨论
可以像13.7小节那样使用所有的过滤方法。例如,Map上的filter方法允许按键、值或者两者一起过滤Map中的元素。filter方法给断言提供了一个Tuple2对象,这样就可以像下面例子中一样访问键和值了:
过滤的算法也可以使用一个tuple,像这样:
take方法可以“提取”(保留)Map中前N个元素:
参阅13.7小节使用其他方法的例子,包括takeWhile,drop,slice等。
最后更新于
这有帮助吗?