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赋值给一个新的变量,下面的例子展示了这些:
// create a map
val a = Map(1 -> "a")
// adding elements
val b = a + (2 -> "b")
val c = b ++ Map(3 -> "c", 4 -> "d")
val d = c ++ List(5 -> "e", 6 -> "f")
// current result:
d: Map[Int, String] = HashMap(5 -> e, 1 -> a, 6 -> f, 2 -> b, 3 -> c, 4 -> d)
// update where the key is 1
val e = d + (1 -> "AA")
// e: HashMap(5 -> e, 1 -> AA, 6 -> f, 2 -> b, 3 -> c, 4 -> d)
// update multiple elements at one time
val f = e ++ Map(2 -> "BB", 3 -> "CC")
val g = f ++ List(2 -> "BB", 3 -> "CC")
// g: HashMap(5 -> e, 1 -> AA, 6 -> f, 2 -> BB, 3 -> CC, 4 -> d)
// remove elements by specifying the keys to remove
val h = g - 1
val i = h -- List(1, 2, 3)
// i: HashMap(5 -> e, 6 -> f, 4 -> d)
当使用不可变Map,可以创建一个var变量,这样就可以不用创建不同名字的变量为结果Map赋值:
// reassign each update to the 'map' variable
var map = Map(1 -> "a")
map = map + (2 -> "b") // map: Map(1 -> a, 2 -> b)
map = map + (3 -> "c") // map: Map(1 -> a, 2 -> b, 3 -> c)
可变Map
要创建一个可变 Map,要么使用import语句将其带入作用域中,要么就在创建实例时指定scala.collection.mutable.Map类的完整路径。当作用域中有了可变的Map,接下来就可以使用其中的功能。下面的例子将展示一些基本功能:
// create an empty, mutable map
val m = scala.collection.mutable.Map[Int, String]()
// adding by assignment
m(1) = "a" // m: HashMap(1 -> a)
// adding with += and ++=
m += (2 -> "b") // m: HashMap(1 -> a, 2 -> b)
m ++= Map(3 -> "c", 4 -> "d") // m: HashMap(1 -> a, 2 -> b, 3 -> c, 4 -> d)
m ++= List(5 -> "e", 6 -> "f") // m: HashMap(1 -> a, 2 -> b, 3 -> c, 4 -> d,
// 5 -> e, 6 -> f)
// remove elements by specifying the keys to remove
m -= 1 // m: HashMap(2 -> b, 3 -> c, 4 -> d, 5 -> e, 6 -> f)
m --= List(2,3) // m: HashMap(4 -> d, 5 -> e, 6 -> f)
// updating
m(4) = "DD" // m: HashMap(4 -> DD, 5 -> e, 6 -> f)
讨论
和其他语言中的Map一样,Scala中的Map是键值对的集合。如果熟悉Java中的Map,Python的字典,或者Ruby的Hash,那么Scala中的Map就很容易理解了。只需要学习一些新的东西,包括Map类中可用的方法,Map适用的场景,比如创建一个有序的Map。
注意在Map的括号内创建一个元组所使用的语法:
"AL" -> "Alabama"
由于元组也能被声明为 ("AL", "Alabama"),所以也可以像这样创建Map:
val states = Map(
("AL", "Alabama"),
("AK", "Alaska")
)
为了清楚地表明正在使用可变的Map,有个技巧是可以在导入可变Map时给它指定一个别名,然后在使用时引用这个别名,如下所示:
import scala.collection.mutable.{Map => MMap}
// MMap is really scala.collection.mutable.Map
val m = MMap(1 -> 'a') // m: Map[Int, Char] = HashMap(1 -> a)
m += (2 -> 'b') // m: HashMap(1 -> a, 2 -> b)
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:
import scala.collection.SortedMap
val x = SortedMap(
2 -> "b",
4 -> "d",
3 -> "c",
1 -> "a"
)
将代码粘贴到REPL中,可以得到以下输出:
val x: scala.collection.SortedMap[Int, String]
= TreeMap(1 -> a, 2 -> b, 3 -> c, 4 -> d)
使用LinkedHashMap、VectorMap或者ListMap记住插入顺序
如果想要Map记住元素的插入顺序,可以使用LinkedHashMap、VectorMap或者ListMap。Scala中只有可变的LinkedHashMap,所以使用前先导入:
import collection.mutable.LinkedHashMap
当创建一个LinkedHashMap实例,它会按照元素的插入顺序存储元素:
val x = LinkedHashMap( // x: LinkedHashMap(1 -> a, 2 -> b)
1 -> "a",
2 -> "b"
)
x += (3 -> "c") // x: LinkedHashMap(1 -> a, 2 -> b, 3 -> c)
x += (4 -> "d") // x: LinkedHashMap(1 -> a, 2 -> b, 3 -> c, 4 -> d)
根据Scaladoc,VectorMap “使用基于vector/map的数据结构实现不可变的Map,从而保留元素的插入顺序...VectorMap的查找是常数时间复杂度,但代价是使用额外的内存,其他操作的性能通常较低。” 它与基本的不可变Map类似,但保留了元素的插入顺序:
import collection.immutable.VectorMap
val a = VectorMap( // a: VectorMap(10 -> a)
10 -> "a"
)
val b = a ++ Map(7 -> "b") // b: VectorMap(10 -> a, 7 -> b)
val c = b ++ Map(3 -> "c") // c: VectorMap(10 -> a, 7 -> b, 3 -> c)
也可以使用不可变的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:
val a = Map(1 -> "a") // a: Map[Int, String] = Map(1 -> a)
添加元素
使用 + 方法添加一个元素,在这个过程中将结果赋值给一个新的变量:
// add one element
val b = a + (2 -> "b") // b: Map(1 -> a, 2 -> b)
使用 ++ 方法添加两个或者更多的元素:
// add multiple elements
val c = b ++ Map(3 -> "c", 4 -> "d")
// c: Map(1 -> a, 2 -> b, 3 -> c, 4 -> d)
val d = c ++ List(5 -> "e", 6 -> "f")
// d: HashMap(5 -> e, 1 -> a, 6 -> f, 2 -> b, 3 -> c, 4 -> d)
更新元素
更新一个不可变Map的键值对,需要用 + 方法对键和值重新赋值,新值会替代旧值:
val e = d + (1 -> "AA")
// e: HashMap(5 -> e, 1 -> AA, 6 -> f, 2 -> b, 3 -> c, 4 -> d)
更新多个键值对,可以将新的元组值放在map或者sequence进行操作:
// update multiple elements at once with a Map
val e = d ++ Map(2 -> "BB", 3 -> "CC")
// e: HashMap(5 -> e, 1 -> a, 6 -> f, 2 -> BB, 3 -> CC, 4 -> d)
// update multiple elements at once with a List
val e = d ++ List(2 -> "BB", 3 -> "CC")
// e: HashMap(5 -> e, 1 -> a, 6 -> f, 2 -> BB, 3 -> CC, 4 -> d)
删除元素
使用 - 方法删除一个元素:
val e = d - 1 // e: HashMap(5 -> e, 6 -> f, 2 -> b, 3 -> c, 4 -> d)
使用 -- 方法删除多个元素:
val e = d -- List(1, 2, 3) // e: HashMap(5 -> e, 6 -> f, 4 -> d)
讨论
可以将不可变Map声明为var变量,然后将结果重新赋值给这个相同的变量。使用var,前面的例子如下所示:
// declare the map variable as a `var`
var a = Map(1 -> "a")
// add one element
a = a + (2 -> "b")
// add multiple elements
a = a ++ Map(3 -> "c", 4 -> "d")
a = a ++ List(5 -> "e", 6 -> "f")
// update where the key is 1
a = a + (1 -> "AA")
// update multiple elements at one time
a = a ++ Map(2 -> "BB", 3 -> "CC")
a = a ++ List(4 -> "DD", 5 -> "EE")
// remove one element by specifying its key
a = a - 1
// remove multiple elements
a = a -- List(2, 3)
上面例子中,由于a变量被定义成了var,所以在代码的每一步都可以被重新赋值。
14.4 为可变Map添加、更新或删除元素
问题
你想在可变的Map中添加、删除或者更新元素。
解决方案
可以这样对可变的Map添加元素:
使用赋值语法:map(key) = value
使用 += 方法
使用 ++= 方法
删除元素:
使用 -= 方法
使用 --= 方法
可以将键重新进行赋值来更新元素。讨论中展示了可以使用的其他方法,包括put、filterInPlace、remove和clear方法。
假设有如下的可变Map:
val m = scala.collection.mutable.Map[Int, String]()
可以通过给键指定值的方式添加一个元素:
m(1) = "a" // m: HashMap(1 -> a)
也可以使用 += 方法添加元素:
m += (2 -> "b") // m: HashMap(1 -> a, 2 -> b)
使用 ++= 从另一个集合添加多个元素:
m ++= Map(3 -> "c", 4 -> "d")
// m: HashMap(1 -> a, 2 -> b, 3 -> c, 4 -> d)
m ++= List(5 -> "e", 6 -> "f")
// m: HashMap(1 -> a, 2 -> b, 3 -> c, 4 -> d, 5 -> e, 6 -> f)
使用 -= 方法,通过指定元素的键从Map中删除一个元素:
m -= 1 // m: HashMap(2 -> b, 3 -> c, 4 -> d, 5 -> e, 6 -> f)
使用 --= 方法,根据键删除多个元素:
m --= List(2,3) // m: HashMap(4 -> d, 5 -> e, 6 -> f)
通过赋新值给元素的键来更新元素:
m(4) = "DD" // m: HashMap(4 -> DD, 5 -> e, 6 -> f)
讨论中展示了修改可变Map的更多方法。
讨论
解决方案中展示的是最常用的方法。也可以这些方法:
使用put方法添加一个键值对,或者替换已有的元素。
使用filterInPlace方法保留满足条件的元素。
使用remove方法删除对应的元素。
使用clear方法删除所有元素。
比如,给定一个可变Map:
val m = collection.mutable.Map(
"AK" -> "Alaska",
"IL" -> "Illinois",
"KY" -> "Kentucky"
)
下面的例子展示了如何使用这些方法:
// returns None if the key WAS NOT in the map
val x = m.put("CO", "Colorado")
// x: Option[String] = None
// m: HashMap(AK -> Alaska, IL -> Illinois, CO -> Colorado, KY -> Kentucky)
// returns Some if the key WAS in the map
val x = m.put("CO", "Colorado")
// x: Option[String] = Some(Colorado)
// m: HashMap(AK -> Alaska, IL -> Illinois, CO -> Colorado, KY -> Kentucky)
m.filterInPlace((k,v) => k == "AK")
// m: HashMap(AK -> Alaska)
// `remove` returns a Some if the key WAS in the map
val x = m.remove("AK")
// x: Option[String] = Some(Alaska)
// m: collection.mutable.Map[String, String] = HashMap()
// `remove` returns a None if the key WAS NOT in the map
val x = m.remove("FOO")
// x: Option[String] = None
// m: collection.mutable.Map[String, String] = HashMap()
m.clear // m: HashMap()
例子中的注释解释了像put和remove方法会返回Some和None值。
14.5 访问Map的值(避免异常)
问题
你希望访问存储在Map中的单个值时,避免抛出异常。比如,给定一个Map:
val states = Map(
"AL" -> "Alabama",
)
可以像访问数组元素的方式一样来访问一个Map中键所关联的值。
val s = states("AL") // s: Alabama
然而,如果Map没有包含所请求的键,会抛出java.util.NoSuchElementException异常:
val s = states("YO") // java.util.NoSuchElementException: key not found: YO
解决方案
为了避免异常,使用:
创建一个Map时使用withDefaultValue方法。
访问元素时使用getOrElse方法。
使用get方法返回一个Some或者None。
例如,给定一个Map:
val states = Map(
"AL" -> "Alabama",
"AK" -> "Alaska",
"AZ" -> "Arizona"
)
避免这个问题的一种方式是在创建Map时使用withDefaultValue方法:
val states = Map(
"AL" -> "Alabama"
).withDefaultValue("Not found")
顾名思义,该方法会创建一个默认值,如果键不存在,就会返回这个默认值。
val x = states("AL") // x: Alabama
val x = states("yo") // x: Not found
另一种方法是在尝试寻找值时使用getOrElse方法。当指定的键不存在时会返回默认值:
val s = states.getOrElse("yo", "No such state") // s: No such state
也可以使用get方法返回Option对象:
val x = states.get("AZ") // x: Some(Arizona)
val x = states.get("yo") // x: None
这三种方式都很不错,因为它们提供了不同的工作方式,可以根据喜欢的编程风格进行使用。
14.6 测试Map中键或值的存在
问题
你想判断Map中是否包含给定的键或者值。
解决方案
可以使用contains或者get方法测试Map中是否包含一个键。使用valuesIterator方法测试Map中是否包含一个值。
测试键
为了测试一个Map中是否包含一个键,使用contains方法是最直接的方式:
val states = Map(
"AK" -> "Alaska",
"IL" -> "Illinois",
"KY" -> "Kentucky"
)
states.contains("FOO") // false
states.contains("AK") // true
根据需要,也可以使用get方法返回一个None或者Some来判断:
states.get("FOO") // None
states.get("AK") // Some(Alaska)
比如,可以在match表达式中使用这种方式:
states.get("AK") match
case Some(state) => println(s"state = $state")
case None => println("state not found")
注意,前面小节已经展示了尝试直接访问一个不存在的键可能会抛出异常:
states("AL") // java.util.NoSuchElementException: key not found: AL
测试值
为了测试一个Map中是否包含一个值,使用valuesIterator方法搜索该值,结合contains方法测试:
states.valuesIterator.contains("Alaska") // true
states.valuesIterator.contains("Kentucky") // true
states.valuesIterator.contains("ucky") // false
这是可行的,因为(a)valuesIterator方法返回一个迭代器:
states.valuesIterator // Iterator[String] = <iterator>
然后(b)如果Map中存在这个值,contians方法会返回true。如果需要对Map的值执行更多搜索操作,也可以使用以下方法:
states.valuesIterator.exists(_.contains("ucky")) // true
states.valuesIterator.exists(_.matches("Ala.*")) // true
因为exists方法需要一个函数,所以可以将此技术用于更复杂的键值和自定义的算法。
讨论
像这样将方法连起来,一定要注意中间结果,尤其是比较大的集合。在上面的例子中,我原本使用了values方法去获取Map中所有的值,但是该方法返回的结果是一个新的集合。如果是一个大的Map,这将会消耗很多内存。相反,valuesIterator方法则返回一个轻量级的迭代器。
另见
14.5小节展示了访问Map的键时如何避免产生异常。
14.7小节介绍了values和valuesIterator方法。
14.7 从Map中获取所有的键和值
问题
你想要获取Map中所有的键或者值。
解决方案
键
为了获取键,可以使用这些方法:
使用keySet方法返回以Set的方式获取键。
使用keys方法获取一个Iterable对象。
使用keysIterator方法返回以迭代器的方式获取键。
这些方法的使用如下所示:
val states = Map("AK" -> "Alaska", "AL" -> "Alabama", "AR" -> "Arkansas"
states.keySet // Set[String] = Set(AK, AL, AR)
states.keys // Iterable[String] = Set(AK, AL, AR)
states.keysIterator // Iterator[String] = <iterator>
值
为了获取值,可以使用这些方法:
使用values方法获取一个Iterable对象。
使用valuesIterator方法获取一个Iterator对象。
这些方法的使用如下所示:
states.values // Iterable[String] = Iterable(Alaska, Alabama, Arkansas)
states.valuesIterator // Iterator[String] = <iterator>
讨论
如这些例子所示,keysIterator和valuesIterator方法都会从Map数据中返回一个迭代器。当操作一个大Map时,通常需要使用这些方法,因为它们不会创建一个新的集合;它们只提供一个迭代器来遍历现有元素。
还需注意,如果想要转换Map中值,可以使用view.mapValues或者transform方法,参阅14.9小节。
14.8 找到Map中最大(或最小)的键或者值
问题
你想在Map中查找最大或者最小的键或者值。
解决方案
使用Map的max和min方法,或者使用keysIterator或者valuesIterator的方式,取决于具体的需求。
两种查找最大最小键的方式将会在解决方案中进行展示,查找最大最小值的方式将会在讨论中进行展示。
为了展示使用Map键的方法,首先创建一个简单的Map:
val grades = Map(
"Al" -> 80,
"Kim" -> 95,
"Teri" -> 85,
"Julia" -> 90
)
在这个Map中,键的类型是String,哪个键是“最大的”取决于如何定义。通过调用Map的max和min方法找到String自然排序的最大或最小的键:
grades.max // (Teri,85)
grades.min // (Al,80)
因为 Teri 中的 T 是名字中字母排序最大的,所以作为最大值被返回了。Al 作为最小值被返回也是同样的道理。
也可以调用keysIterator方法返回一个scala.collection.Iterator对象,然后调用max和min方法:
grades.keysIterator.max // Teri
grades.keysIterator.min // Al
此外,调用keysIterator结合reduceLeft方法,同样能找到相同的最大和最小值:
scala> grades.keysIterator.reduceLeft((x,y) => if x > y then x else y)
val res2: String = Teri
scala> grades.keysIterator.reduceLeft((x,y) => if x < y then x else y)
val res3: String = Al
这种方式很灵活,因为一旦最大的定义变成最长字符串,可以直接调整为比较字符串的长度:
scala> grades.keysIterator.reduceLeft((x,y) => if x.length > y.length then x else y)
res4: String = Julia
讨论
想要从一个Map中获取最大和最小的值,可以使用valuesIterator方法返回一个迭代器,然后调用max或者min方法:
grades.valuesIterator.max // 95
grades.valuesIterator.min // 80
这是可以工作的,因为例子中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的方式获取最大和最小值:
grades.valuesIterator.reduceLeft(_ max _) // 95
grades.valuesIterator.reduceLeft(_ min _) // 80
使用reduceLeft的好处是可以使用自定义的算法比较任意的值类型,这代表了可能需要对更复杂的数据类型执行操作。下面的例子展示了如何将reduceLeft方法与复杂的算法结合使用:
// max
scala> grades.valuesIterator.reduceLeft((x,y) => if x > y then x else y)
val res5: Int = 95
// min
scala> grades.valuesIterator.reduceLeft((x,y) => if x < y then x else y)
val res6: Int = 80
现在已经知道了如何访问x和y的值,然后就可以创建任何自定义的算法进行比较。
另见
参阅13.10小节 “用reduce和fold方法遍历集合”
参阅14.10小节
14.9 遍历Map
问题
你想要遍历Map中的每个元素。
解决方案
遍历Map中的元素有几种不同的方法。假设有如下一个Map:
val ratings = Map(
"Lady in the Water"-> 3.0,
"Snakes on a Plane"-> 4.0,
"You, Me and Dupree"-> 3.5
)
一个不错的方式是使用for循环语句遍历所有的元素:
for (k,v) <- ratings do println(s"key: $k, value: $v")
使用foreach配合匿名函数,可读性也不错:
ratings.foreach {
case(movie, rating) => println(s"key: $movie, value: $rating")
}
下面的例子展示了如何使用元组语法来访问键和值字段:
ratings.foreach(x => println(s"key: ${x._1}, value: ${x._2}"))
注意:例子中的电影评分数据来自Programming Collective Intelligence by Toby Segaran(O’Reilly)一书。
讨论
注意在Scala 3中,可以用三种不同的方式编写带有匿名函数的foreach方法:
ratings.foreach {
(movie, rating) => println(s"key: $movie, value: $rating")
}
ratings.foreach {
case movie -> rating => println(s"key: $movie, value: $rating")
}
// works with Scala 2
ratings.foreach {
case(movie, rating) => println(s"key: $movie, value: $rating")
}
在每个例子中,花括号内的代码都充当匿名函数。
根据不同遍历Map的方式,你可以只访问Map中的键或者值。
键
如果只想访问Map中的键,可以使用keysIterator方法得到一个轻量级的迭代器来获得所有的键:
val i = ratings.keysIterator
// the iterator provides access to the map’s keys
i.toList // List(Lady in the Water, Snakes on a Plane, You, Me and Dupree)
keys方法返回一个Iterable对象,因此它会创建一个新的中间集合(如果是个大Map,可能会有性能问题),但是简单易用:
scala> ratings.keys.foreach((m) => println(s"$m rating is ${ratings(m)}"))
Lady in the Water rating is 3.0
Snakes on a Plane rating is 4.0
You, Me and Dupree rating is 3.5
值
如果想要遍历Map操作其中的值,可以使用定义在MapView特质中的mapValues方法。首先,在Map上调用view方法创建一个MapView,然后调用mapValues方法。它可以在遍历的每个值上应用一个函数,然后返回修改后的Map:
val a = Map(1 -> "ay", 2 -> "bee")
// a: Map[Int, String] = Map(1 -> ay, 2 -> bee)
val b = a.view.mapValues(_.toUpperCase).toMap // Map(1 -> AY, 2 -> BEE)
由于视图以非严格或懒惰的方式实现转换,因此在调用mapValues之前创建视图只会创建一个轻量级迭代器:
// `view` uses an iterator
a.view // MapView(<not computed>)
当一个Map特别大时,这种方式特别有效。可以参阅11.4小节“在集合上创建惰性视图”获取更多信息。
相反,可以使用values和map方法来解决这个问题:
a.values.map(_.toUpperCase) // List(AY, BEE)
但请注意,此过程的第一步将创建一个中间的Iterable对象:
a.values // Iterable(ay, bee)
因此,对于大的Map来说,这种方式将会产生额外的中间结果,导致性能或者消耗内存的问题。
转换方法
如果想要遍历Map的时候对值进行转换,另一个方式是使用transform方法可以从一个已知的Map中创建一个新的Map。和mapValues方法不同,transform方法可以同时使用键和值实现转换:
val map1 = Map(1 -> 10, 2 -> 20, 3 -> 30)
// use the map keys and values to create new values
val map2 = map1.transform((k,v) => k + v)
// map2: Map(1 -> 11, 2 -> 22, 3 -> 33)
对于更复杂的情况,还可以在初始的Map上创建一个视图,然后在该MapView上调用map方法,这样就可以访问Map的键和值:
val map3 = map1.view.map((k,v) => (k, k + v)).toMap
14.10 根据键或值对Map排序
问题
你想要按照键或者值对未排序的Map元素进行排序。
解决方案
假设有一个基本的不可变Map:
val grades = Map(
"Kim" -> 90,
"Al" -> 85,
"Melissa" -> 95,
"Emily" -> 91,
"Hannah" -> 92
)
可以使用sortBy方法按照键从低到高对Map进行排序,然后将结果存储在一个可变的LinkedHashMap或者不可变的VectorMap。两种方案如下所示:
import scala.collection.mutable.LinkedHashMap
// Version 1: sorts by key by accessing each tuple as '(k,v)'
val x = LinkedHashMap(grades.toSeq.sortBy((k,v) => k):_*)
// x: LinkedHashMap(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90,
// Melissa -> 95)
// Version 2: sorts by key using the tuple '._1' syntax
val x = LinkedHashMap(grades.toSeq.sortBy(_._1):_*)
// x: LinkedHashMap(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90,
// Melissa -> 95)
也可以使用sortWith方法传入自定义的算法,按照升序或者降序对键进行排序,然后将结果存储在一个LinkedHashMap中:
// sort by key, low to high
val x = LinkedHashMap(grades.toSeq.sortWith(_._1 < _._1):_*)
// x: LinkedHashMap(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90,
// Melissa -> 95)
// sort by key, high to low
val x = LinkedHashMap(grades.toSeq.sortWith(_._1 > _._1):_*)
// x: LinkedHashMap(Melissa -> 95, Kim -> 90, Hannah -> 92, Emily -> 91,
// Al -> 85)
这种语法将会在讨论中进行解释。
使用sortBy方法按照值对Map排序:
// value, low to high, accessing elements as `(k,v)`
val x = LinkedHashMap(grades.toSeq.sortBy((k,v) => v):_*)
// x: LinkedHashMap(Al -> 85, Kim -> 90, Emily -> 91, Hannah -> 92,
// Melissa -> 95)
// value, low to high, using the tuple `_` syntax
val x = LinkedHashMap(grades.toSeq.sortBy(_._2):_*)
// x: LinkedHashMap(Al -> 85, Kim -> 90, Emily -> 91, Hannah -> 92,
// Melissa -> 95)
也可以使用sortWith方法按照升序或者降序对值进行排序:
// sort by value, low to high
val x = LinkedHashMap(grades.toSeq.sortWith(_._2 < _._2):_*)
// x: LinkedHashMap(Al -> 85, Kim -> 90, Emily -> 91, Hannah -> 92,
// Melissa -> 95)
// sort by value, high to low
val x = LinkedHashMap(grades.toSeq.sortWith(_._2 > _._2):_*)
// x: LinkedHashMap(Melissa -> 95, Hannah -> 92, Emily -> 91, Kim -> 90,
// Al -> 85)
除了使用LinkedHashMap,也可以将结果存在一个不可变的VectorMap中。当影响到性能时,一定要测试一下这两种Map类型,看看哪种最适合实际的应用场景。
讨论
将解决方案分解成几个小的步骤有助于理解。首先,创建一个基本的不可变Map:
val grades = Map(
"Kim" -> 90,
"Al" -> 85,
"Melissa" -> 95,
"Emily" -> 91,
"Hannah" -> 92
)
接下来是grades.toSeq创建一个双元素元组的序列,也就是Seq[(String, Int)]:
val x = grades.toSeq
// x: ArrayBuffer((Hannah,92), (Melissa,95), (Kim,90), (Emily,91), (Al,85))
转换成Seq的原因是它有现成的排序方法:
// sort by key
val x = grades.toSeq.sortBy(_._1)
// x: Seq[(String, Int)] =
// ArrayBuffer((Al,85), (Emily,91), (Hannah,92), (Kim,90), (Melissa,95))
// sort by key
val x = grades.toSeq.sortWith(_._1 < _._1)
// x: Seq[(String, Int)] =
// ArrayBuffer((Al,85), (Emily,91), (Hannah,92), (Kim,90), (Melissa,95))
在上面的例子中,_._1
语法表示元组的第一个元素,也就是键。同样的,_._2
表示值。
一旦Map的数据按预想的方式完成排序,将其保存到一个LinkedHashMap、VectorMap或者ListMap中,保证排序的顺序:
val x = LinkedHashMap(grades.toSeq.sortBy(_._1):_*)
// x: scala.collection.mutable.LinkedHashMap[String,Int] =
// Map(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90, Melissa -> 95)
LinkedHashMap只有可变的版本,VectorMap是一个不可变的Map。还有一个不可变的ListMap,但只建议用于小的Map。可以按照不同的实际情况进行使用。
关于_*
代码中的 _* 部分需要花点时间去理解。它的作用是将数据转换,然后将其作为多个参数传给LinkedHashMap(或者VectorMap、ListMap)。如果曾经在Unix系统上使用过xargs命令,它工作方式与之类似,将一系列元素作为输入,每次将一个元素传递给下一个命令。
将代码分解成多行,可以更容易理解。sortBy方法返回一个Seq[(String, Int)],即一个元组序列:
val seqOfTuples = grades.toSeq.sortBy(_._1)
// seqOfTuples: Seq[(String, Int)] =
// List((Al,85), (Emily,91), (Hannah,92), (Kim,90), (Melissa,95))
不幸的是,不能直接使用元组序列来构造VectorMap、LinkedHashMap或者ListMap:
scala> VectorMap(seqOfTuples)
1 |VectorMap(seqOfTuples)
| ^^^^^
| Found: (seqOfTuples : Seq[(String, Int)])
| Required: (Any, Any)
但是由于VectorMap伴生对象的apply方法接受一个Tuple2的变长参数,可以调整seqOfTuples使用 _* 将Tuple2元素序列转换为一系列单独的Tuple2值。为VectorMap的apply方法提供了所需的输入:
val x = VectorMap(seqOfTuples: _*)
// x: scala.collection.immutable.VectorMap[String, Int] =
// VectorMap(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90, Melissa -> 95)
另一个了解 _* 如何工作的方式是自定义接受变长参数的方法。下面例子中的printAll方法接受一个参数,String类型的变长字段:
def printAll(strings: String*): Unit = strings.foreach(println)
然后像这样创建一个List:
// a sequence of strings
val fruits = List("apple", "banana", "cherry")
可以发现List不能被传入printAll方法;和前面的例子一样会引起错误:
scala> printAll(fruits)
1 |printAll(fruits)
| ^^
| Found: (fruits : List[String])
| Required: String
但是使用 _* 可以调整List,让它可以作用于printAll方法,像下面这样:
// this works
scala> printAll(fruits: _*)
apple
banana
cherry
如果有Unix背景,可以将 _* 看做是一个splat操作符。该操作符将序列的每个元素作为单独的参数传递给printAll方法,而不是将fruits当做单个的List参数传入。
14.11 过滤Map
问题
你想要过滤Map中包含的元素,要么直接修改可变的Map,要么是在不可变的Map上应用过滤算法生成一个新的Map。
解决方案
在可变的Map上使用filterInPlace方法选择需要保留的元素,使用filterKeys或者filterKeys或者filter方法过滤可变或者不可变Map的元素,不要忘记将结果赋值给一个新的变量。
可变Map
通过filterInPlace方法指定要保留的元素,过滤可变Map的元素:
val x = collection.mutable.Map(
1 -> 100,
2 -> 200,
3 -> 300
)
x.filterInPlace((k,v) => k > 1) // x: HashMap(2 -> b, 3 -> c)
x.filterInPlace((k,v) => v > 200) // x: HashMap(3 -> 300)
如上所见,filterInPlace原地修改了可变Map。如例子中隐含的匿名函数签名:
(k,v) => ...
算法可以测试每个元的键和值,决定哪些元素可以保留在Map中。
取决于“filter”的定义,也可以像remove和clear方法那样删除Map中的元素,可以参阅14.3小节。
可变和不可变Map
使用可变或者不可变Map时,用filterKeys方法配合断言选择保留Map中的哪些元素。使用这个方法时,首先调用Map中的view方法创建一个MapView,然后记得将过滤后的结果赋值给一个新的变量:
val x = Map(
1 -> "a",
2 -> "b",
3 -> "c"
)
val y = x.view.filterKeys(_ > 2).toMap // y: Map(3 -> c)
断言对于要在新集合中保留的元素返回true,对于不想保留的元素返回false。
注意使用这种方式,如果没有在结尾调用toMap方法,将会在REPL中看到如下输出:
scala> val y = x.view.filterKeys(_ > 2)
val y: scala.collection.MapView[Int, String] = MapView(<not computed>)
这里 MapView() 是因为view方法返回的是一个惰性视图,只有通过调用toMap等方法强制执行时,才能真正执行计算。(参阅11.4小节获取更多关于视图的信息。)
如果算法较长,可以定义一个匿名函数(或者方法),然后在调用filterKeys时使用,而不是用匿名函数的方式。比如,首先定义如下例子中的方法,当输入参数为1时返回true:
def only1(i: Int) = if i == 1 then true else false
然后将该函数传给filterKeys方法:
val x = Map(1 -> "a", 2 -> "b", 3 -> "c")
val y = x.view.filterKeys(only1).toMap // y: Map(1 -> a)
也可以让filterKeys配合一个Set指定需要保留的元素:
val x = Map(1 -> "a", 2 -> "b", 3 -> "c")
val y = x.view.filterKeys(Set(2,3)).toMap
// y: Map[Int, String] = Map(2 -> b, 3 -> c)
讨论中展示了其他过滤方法,例如如何根据Map的值进行过滤。
讨论
可以像13.7小节那样使用所有的过滤方法。例如,Map上的filter方法允许按键、值或者两者一起过滤Map中的元素。filter方法给断言提供了一个Tuple2对象,这样就可以像下面例子中一样访问键和值了:
// an immutable map
val a = Map(1 -> "a", 2 -> "b", 3 -> "c")
// filter by the key
val b = a.filter((k,v) => k > 1) // b: Map(2 -> b, 3 -> c)
// filter by the value
val c = a.filter((k,v) => v != "b") // c: Map(1 -> a, 3 -> c)
过滤的算法也可以使用一个tuple,像这样:
// filter by the key (t._1)
val b = a.filter((t) => t._1 > 1) // b: Map(2 -> b, 3 -> c)
// filter by the value (t._2)
val b = a.filter((t) => t._2 != "b") // b: Map(1 -> a, 3 -> c)
take方法可以“提取”(保留)Map中前N个元素:
val b = a.take(2) // b: Map(1 -> a, 2 -> b)
参阅13.7小节使用其他方法的例子,包括takeWhile,drop,slice等。
最后更新于
这有帮助吗?