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,表示keyvalue都是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,因此这里不多做介绍。SortedMapLinkedHashMap包含在小节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记住元素的插入顺序,可以使用LinkedHashMapVectorMap或者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.ParMapcollection.parallel.immutable.ParHashMapcollection.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

  • 使用 += 方法

  • 使用 ++= 方法

删除元素:

  • 使用 -= 方法

  • 使用 --= 方法

可以将键重新进行赋值来更新元素。讨论中展示了可以使用的其他方法,包括putfilterInPlaceremoveclear方法。

假设有如下的可变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()

例子中的注释解释了像putremove方法会返回SomeNone值。

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小节介绍了valuesvaluesIterator方法。

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>

讨论

如这些例子所示,keysIteratorvaluesIterator方法都会从Map数据中返回一个迭代器。当操作一个大Map时,通常需要使用这些方法,因为它们不会创建一个新的集合;它们只提供一个迭代器来遍历现有元素。

还需注意,如果想要转换Map中值,可以使用view.mapValues或者transform方法,参阅14.9小节。

14.8 找到Map中最大(或最小)的键或者值

问题

你想在Map中查找最大或者最小的键或者值。

解决方案

使用Mapmaxmin方法,或者使用keysIterator或者valuesIterator的方式,取决于具体的需求。

两种查找最大最小的方式将会在解决方案中进行展示,查找最大最小的方式将会在讨论中进行展示。

为了展示使用Map键的方法,首先创建一个简单的Map

    val grades = Map(
        "Al" -> 80,
        "Kim" -> 95,
        "Teri" -> 85,
        "Julia" -> 90
    )

在这个Map中,键的类型是String,哪个键是“最大的”取决于如何定义。通过调用Mapmaxmin方法找到String自然排序的最大或最小的键:

    grades.max // (Teri,85)
    grades.min // (Al,80)

因为 Teri 中的 T 是名字中字母排序最大的,所以作为最大值被返回了。Al 作为最小值被返回也是同样的道理。

也可以调用keysIterator方法返回一个scala.collection.Iterator对象,然后调用maxmin方法:

    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小节“在集合上创建惰性视图”获取更多信息。

相反,可以使用valuesmap方法来解决这个问题:

    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的数据按预想的方式完成排序,将其保存到一个LinkedHashMapVectorMap或者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(或者VectorMapListMap)。如果曾经在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))

不幸的是,不能直接使用元组序列来构造VectorMapLinkedHashMap或者ListMap

    scala> VectorMap(seqOfTuples)
    1 |VectorMap(seqOfTuples)
      |          ^^^^^
      |          Found: (seqOfTuples : Seq[(String, Int)])
      |          Required: (Any, Any)

但是由于VectorMap伴生对象的apply方法接受一个Tuple2的变长参数,可以调整seqOfTuples使用 _* 将Tuple2元素序列转换为一系列单独的Tuple2值。为VectorMapapply方法提供了所需的输入:

    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”的定义,也可以像removeclear方法那样删除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小节使用其他方法的例子,包括takeWhiledropslice等。

最后更新于

这有帮助吗?