Scala学习笔记-核心特性(3)-集合

集合总览

简介

在Scala中,集合主要分为三大类型:序列(Seq)、集合(Set)以及映射(Map),并且所有的集合都扩展自Iterable特征。

对于几乎所有集合类,Scala中都同时提供了可变不可变版本,区分主要是按照集合的位置来区分:

  • 不可变集合:scala.collection.immutable
  • 可变集合:scala.collection.mutable

可变与不可变指的是引用是否可变,类比Java中的String和StringBuilder。String是不可变的,在对String进行修改的时候,实际上是返回了一个新的对象;StringBuilder是可变的,对它进行修改的时候是在对原对象进行修改。

对于不可变集合,集合的长度数量不可修改,每次修改(比如增删元素)都会返回一个新的对象,而不会修改源对象。

可变集合可以对源对象任意修改,一般也提供不可变集合相同的返回新对象的方法,但也可以用其他方法来修改源对象。

一般情况下一个集合类的可变和不可变版本的名称不同,但是有时候也可能相同,这时候就需要按照集合所属的包来进行区分。后面我们会介绍一些典型集合的操作方式,一般建议是:在操作集合的时候,不可变集合使用符号操作,可变集合使用方法操作

不可变集合总览

  • Set和Map是Java中也有的集合,对应的特点也和Java中相同
  • 在Scala的Map体系中有一个SortedMap,支持排序
  • Seq是Java中没有的特征,其中分为随机访问序列(IndexedSeq)和线性序列(LinearSeq)
    • IndexedSeq通过索引来查找和定位,因此速度快
    • LinearSeq是线性的,有头尾的概念,这种数据结构一般通过遍历来查找(像这里队列,栈等被归类到了LinearSeq,因为我们一般只关注它们的首尾元素,操作的也是首尾元素)
  • 前面我们学习的for语法,底层对应的就是IndexedSeq下的Range
  • 图中的Array和String使用虚线连接,它们其实对应的就是Java中的数组和java.lang.String,和Scala中的集合并没有直接关系。但是通过可以通过隐式转换成为一个包装类型之后就可以当作集合了
  • Scala中更多地推荐使用不可变集合,能使用不可变就使用不可变

可变集合总览

可变集合相较于不可变集合中多出了Buffer特征,其余的整体结构相差不大

数组

不可变数组 Array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 两种创建方式
val arr1 = new Array[Int](5)
val arr2 = Array(5, 4, 3, 2, 1)

// 赋值和访问
// 采用符号=进行赋值
for (i <- 0 until arr1.length) arr1(i) = i
// 调用方法update进行赋值
arr2.update(0, 6)

// 遍历
// 下标遍历
for (i <- 0 until arr1.length) print(arr1(i) + " ")
println()
// 元素遍历
for (elem <- arr1) print(elem + " ")
println()
// 迭代器遍历
val iterator = arr2.iterator
while (iterator.hasNext) {
print(iterator.next() + " ")
}
println()
// foreach方法遍历
arr2.foreach((elem: Int) => {
print(s"${elem} ")
})
println()
// mkString方法
println(arr2.mkString(" "))

// 增加元素
val newArr1 = arr1 :+ 10 // 在末尾添加(调用方法或者省略成符号)
val newArr2 = arr1.+:(20) // 调用方法在首部添加
val newArr3 = 30 +: arr2 // 省略成符号在首部添加 需要注意顺序
val newArr4 = 20 +: 10 +: arr2 :+ 30 :+ 40 // 完全使用符号,:需要朝向数组对象

println(newArr1.mkString(" "))
println(newArr2.mkString(" "))
println(newArr3.mkString(" "))
println(newArr4.mkString(" "))
  • 不可变数组在创建的时候有两种方式
    • 方式1:指定大小,不指定初值
    • 方式2:直接指定初值
  • 不可变数组的值赋值和修改是不会返回新对象的,但是增删元素是会返回新对象的
  • 需要注意这里添加符号的使用:++::始终朝向对象
  • 遍历方式对于集合类型来说基本上都是通用的
  • 下标越界会抛出异常,在使用之前应该注意
  • 建议不可变集合使用符号来操作

可变数组 ArrayBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 两种创建方式
val arr1 = new ArrayBuffer[Int]()
val arr2 = ArrayBuffer(5, 4, 3, 1, 1)


// 增加元素
arr1 += 10 // 使用符号添加元素
arr2.append(30) // 调用append方法
arr2.insert(0, 8, 9) // insert方法向指定位置插入数据
// 使用类似于不可变数组的方式
// 但是这种方式不会修改源数组,而是需要新变量接收
val arr3 = arr1 :+ 22
val arr4 = 33 +: arr1

// 删除元素
arr1.remove(0) // 调用remove方法(从哪个下标开始删除,删除多少个)
arr2 -= 1 // 使用符号删除元素(遇到符合条件的即删除,执行一次)
  • 两种创建方式,不指定大小默认初始大小为16

  • 注意可变和不可变的理念,一个是在源数组上修改,另一个是返回一个新的修改后的数组

  • 建议可变集合使用方法调用来操作

可变数组和不可变数组之间可以相互转化,转化不会修改本身的类型,而是返回一个结果

  • Array -> ArrayBuffer:toBuffer()
  • ArrayBuffer -> Array:toArray()

多维数组 Array.ofDim

多维数组可以使用Array.ofDim[Type]来指定维度

1
2
3
4
5
6
7
8
9
10
11
// 创建一个二维数组,2行3列
val array = Array.ofDim[Int](2, 3)

// 访问和修改元素
array(0)(2) = 10
array(1)(0) = 25

// 遍历
array.foreach(line =>{
line.foreach(print)
})

Scala中ofDim方法最多支持到构造5维的数组,更多维度的数组不支持

列表

不可变列表 List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 创建一个List(不能直接new,需要使用伴生对象的apply方法)
val list1 = List(92, 39, 52, 23)

// 访问和遍历元素
println(list1(1))
list1.foreach(elem => print(s"${elem} "))
println()

// 添加元素(返回新列表)
val list2 = 10 +: list1 // 添加到首部
val list3 = list1 :+ 10 // 添加到尾部
println(list1)
println(list2)
println(list3)

// ::符号的使用
val list4 = list1.::(49) // 添加元素到列表头部
val list5 = 49 :: list1
println(list4)
println(list5)

// 利用::来构造新列表,Nil表示空列表
val list6 = 12 :: 123 :: 23 :: 98 :: Nil
println(list6)

// 合并列表
val list7 = list1 :: list6 // 这种方式会将list1当作一个整体添加到list6的首部
val list8 = list1 :::list6
val list9 = list1 ++ list6
println(list7)
println(list8)
println(list9)
  • List是抽象类,本身不能直接new,需要调用伴生对象的apply创建对象
  • 支持+::+向首尾添加元素
  • Nil表示空列表,::添加元素到表头
  • 合并列表使用:::或者++
  • List本身可以使用apply进行随机访问,但是不能进行update更改,即不能通过类似list1(0) = 1的方式进行修改

可变列表 ListBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 创建可变列表
val list1 = new ListBuffer[Int]()
val list2 = ListBuffer(12, 23, 44, 53)

// 添加元素
list1.append(32) // 添加到末尾
list2.prepend(46) // 添加到首部
list1.insert(1, 12, 32) // 在指定位置插入
// 利用符号+=:添加到首部
10 +=: 11+=: list1 += 12 += 13
println(list1)
println(list2)

// 修改元素
list1(1) = 11
list2.update(1, 11)
println(list1)
println(list2)

// 删除元素
list1.remove(0)
list2 -= 46
println(list1)
println(list2)

// 合并列表
val list4 = list1 ++ list2 // ++合并成新列表
println(list4)
println(list1)
println(list2)

list1 ++= list2 // 合并到前面的列表
println(list1)
println(list2)
  • 可变列表ListBuffer的操作与可变数组ArrayBuffer类似

  • 有两种方式进行对象的创建

  • 常用方法:append prepend insert remove

  • 添加元素到头部或者尾部:+=: +=

  • 合并列表:++ ++=

集合

在Scala中,集合的可变和不可变版本的名称都是Set。默认情况下,Scala使用的是不可变集合,如果想要使用可变集合,则需要引用scala.collection.mutable.Set,建议在使用的时候,不可变集合使用Set,可变集合使用mutable.Set

不可变集合 Set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建Set
val set1 = Set(12,23,21,12,123,43,53,4)

// 添加元素
val set2 = set1 + 111
println(set1)
println(set2)

// 删除元素
val set3 = set1 - 123
println(set1)
println(set3)

// 合并Set
val set4 = set1 ++ set2
println(set4)
  • 数据无序且不可重复
  • 添加元素:+
  • 删除元素:-
  • 合并:++
  • 操作返回一个新的集合,不会修改源集合

可变集合 mutable.Set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 创建Set
val set1 = mutable.Set(12, 23, 12, 23, 34, 54, 65, 23)

// 添加元素
set1 += 11

// 返回结果为一个布尔值,表示是否执行成功
val flag1 = set1.add(33)
val flag2 = set1.add(33)
println(flag1)
println(flag2)
println(set1)

// 删除元素
set1 -= 11

val flag3 = set1.remove(12)
val flag4 = set1.remove(12)
println(flag3)
println(flag4)
println(set1)

// 合并Set
val set2 = mutable.Set(12, 5, 23, 1, 4)
set1 ++= set2
println(set1)

  • 操作在源集合上
  • 添加元素:+ add
  • 删除元素:- remove
  • 方法调用会返回操作是否成功的布尔值
  • 合并操作:++

映射

Scala中的Map和Java中的类似,存储的内容是键值对。与Set相同,Map的可变与不可变类型的名称相同,只是存在不同的包中。默认的Map是不可变类型的,可变类型建议使用mutable.Map加以区别

不可变映射 Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建Map
val map1: Map[String, Int] = Map("a" -> 1, "b" -> 2, "c" -> 3)
println(map1)

// 遍历元素,打印键值对
map1.foreach(println)
map1.foreach((kv: (String, Int)) => println(kv))

// 取出Map中所有的Key或者Value
for(key <- map1.keys) println(s"${key} -> ${map1.get(key)}")
for(value <-map1.values) println(s"${value} ")

// 通过key来访问value
println(map1.get("a")) // 得到Some对象
println(map1.get("a").get) // 得到确切的值
println(map1.get("d").get) // 先get("d")得到None,再进行get会报错
println(map1.getOrElse("d", 0)) // 普通get没有会得到None,再进行get会报错,getOrElse不会,可以设置默认值

println(map1("c")) // 直接得到确切的值,没有则报错

// 合并Map
val map2 = map1 ++ Map("d"->4)
println(map2)
  • 通过get(key)得到的是一个Some对象,如果没有找到则返回None
  • Some对象通过get可以得到确切的值

可变映射 mutable.Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建map
val map1:mutable.Map[String, Int] = mutable.Map("a"->1, "b"->2, "c"->3)
println(map1)

// 添加元素
map1.put("d", 4)
map1 += (("e", 5)) // 注意是两个括号
println(map1)

// 删除元素
map1.remove("c")
map1 -= "d"
println(map1)

// 修改元素
map1.put("a",-1)
map1.update("b", -2)
println(map1)

// 合并Map,合并的时候,相同的key,value以后面的为准,进行覆盖
val map2:mutable.Map[String, Int] = mutable.Map("a"->11, "b"->22, "c"->33)
map1 ++= map2
println(map1)
  • 不可变中的访问等机制都支持

  • put方式会可能会进行覆盖,返回值是一个Option对象,可以得到返回之前的值

    1
    2
    val maybeInt: Option[Int] = map1.put("a", 11)
    println(maybeInt.getOrELse(0))

元组

元组可以理解为是一个容器,其中可以存放相同和不同类型的数据。在Scala中,元组最多只能有22个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 构建元组
val tuple:(String, Int, Boolean) = ("xxx", 11, true)
println(tuple)

// 访问元组的元素(注意下标是从1开始)
println(tuple._1)
println(tuple._2)
println(tuple._3)

// 此处的下标为0
println(tuple.productElement(0))

// 遍历元组数据
for(elem <- tuple.productIterator) println(elem)
  • Map中存放的就是二元元组,->可以用来构建二元组
  • 元组可以嵌套定义

队列

队列也分为可变和不可变,名称相同均为Queue,同样是存在的包不同。默认为不可变队列,可变队列为mutable.Queue

1
2
3
4
5
6
7
8
// 创建队列
val que = new mutable.Queue[String]()

// 入队
que.enqueue("a", "b", "c")

// 出队
println(que.dequeue())

并行集合

Scala为了充分使用多核CPU,提供了并行集合,用于多核环境的并行计算。使用并行集合之后,在执行的时候会调用多个线程来加速执行。只需要在使用的集合类后添加.par方法即可。并行集合依赖于scala.collection.parallel.immutable/mutable,2.13版本之后不再通过标准库提供,需要单独下载。

1
2
3
4
val result1 = (0 to 100).map(_ => Thread.currentThread.getName)
val result2 = (0 to 100).par.map(_ => Thread.currentThread.getName)
println(result1)
println(result2)

集合常用函数

基本属性和常用操作

  • length:获取集合长度(线性序列才有长度)
  • size:获取集合大小(所有集合类型都有大小)
  • 遍历方式:下标遍历、元素遍历、迭代器遍历、foreach
  • iterator:迭代器
  • mkString:生成字符串
  • contains:是否包含

衍生集合

衍生集合方法指的是调用操作之后得到的结果可能是一个集合

  • 获取集合的头元素head(元素)和剩下的尾tail(除去第一个元素构成的集合)。
  • 集合最后一个元素last(元素)和除去最后一个元素的初始数据init(集合)。
  • 反转reverse
  • 取前后n个元素take(n) takeRight(n)
  • 去掉前后n个元素drop(n) dropRight(n)
  • 交集intersect
  • 并集union,线性序列的话已废弃用concat连接。
  • 差集diff,得到属于自己、不属于传入参数的部分。
  • 拉链zip,得到两个集合对应位置元素组合起来构成二元组的集合,大小不匹配会丢掉其中一个集合不匹配的多余部分。(类似Python的zip)
  • 滑窗sliding(n, step = 1),框住特定个数元素,方便移动和操作。得到迭代器,可以用来遍历,每个迭代的元素都是一个n个元素集合。步长大于1的话最后一个窗口元素数量可能个数会少一些。

集合计算简单函数

  • 求和sum 求乘积product 最小值min 最大值max

  • maxBy(func)支持传入一个函数获取元素并返回比较依据的值

    举例来说,元组默认就只会判断第一个元素,如果需要根据第二个元素判断,则返回第二个元素就行xxx.maxBy(_._2)

  • 排序sorted,默认从小到大排序。从大到小排序sorted(Ordering[Int].reverse)

  • 按元素排序sortBy(func),指定要用来做排序的字段。也可以再传一个隐式参数逆序sortBy(func)(Ordering[Int].reverse)

  • 自定义比较器sortWith(cmp),比如按元素升序排列sortWith((a, b) => a < b)或者sortWith(_ < _),按元组元素第二个元素升序sortWith(_._2 > _._2)

集合计算高级函数

  • filter:遍历一个集合,并从中获取符合条件的元素组成一个新的集合
  • map:将集合中的每个元素通过某种映射关系得到另一个元素
  • flatten:将元素打散扁平化
  • flatMap:相当于先对每个集合进行map操作,然后再进行flatten操作
  • group:按照指定的规则对集合进行分组
  • reduce:依次将集合中的元素进行规约,最终得到一个输出
  • fold:与reduce类似,但是具有初始值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
val list = List(1, 2, 3, 4)

// filter
// 选取偶数
val evenList = list.filter((elem: Int) => {
elem % 2 == 0
})
println(evenList)
// 选取奇数
println(list.filter(_ % 2 == 1))

// map
// 把集合中每个数都乘2 / 都进行平方
println(list.map(_ * 2))
println(list.map(x => x * x))

// flatten
val nestedList: List[List[Int]] = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))
println(nestedList.flatten)

// faltMap
// 将一组字符串进行分词,并保存成单词的列表
val strings: List[String] = List("Hello World", "Hello Java", "Hello Scala")
println(strings.flatMap(_.split(" ")))

// groupBy
// 将list分成奇偶两组,得到的是一个Map结构
val groupMap: Map[Int, List[Int]] = list.groupBy(_ % 2)
println(groupMap)

// reduce
// 逐个相加,以下结果都是一样的
println(list.reduce(_ + _)) // 1 + 2 + 3 + 4 = 10
println(list.reduceLeft(_ + _)) // 1 + 2 + 3 + 4 = 10
println(list.reduceRight(_ + _)) // (1 + (2 + (3 + 4))) = 10
// 逐个相减,注意顺序
println(list.reduce(_ - _)) // 1 - 2 - 3 - 4 = -8
println(list.reduceLeft(_ - _)) // 1 - 2 - 3 - 4 = -8
println(list.reduceRight(_ - _)) // (1 - (2 - (3 - 4))) = -2

// fold,类似于reduce,但是可以指定初值,并且注意初值的位置
println(list.fold(10)(_ + _)) // 10 + 1 + 2 + 3 + 4 = 20
println(list.foldLeft(10)(_ - _)) // 10 - 1 - 2 - 3 - 4 = 0
println(list.foldRight(10)(_ - _)) // (1 - (2 - (3 - (4 - 10)))) = 8
  • 注意foldRight和reduceRight的顺序
  • fold提供的初始值必须是同一类型,foldLeft中可以传入不同类型的初始值

集合应用案例

定制Map合并

Map的默认合并操作是使用后面的相同Key中的value进行覆盖,现在需要定制一个合并方式,逻辑为相同的Key进行value的叠加

(注意这里foldLeft的选用逻辑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val map1 = Map("a" -> 1, "b" -> 3, "c" -> 4)
// 注意map2应该是可变Map
val map2 = mutable.Map("a" -> 6, "b" -> 1, "c" -> 9, "d" -> 10)
// 如果是res = xxx.fold(a)(op(x, y)),每次进行的操作是op(x, y)
// y是集合中的每个元素,x是之前得到结果,如果是第一次则是传入的a
val map3 = map1.foldLeft(map2)(
(mergedMap, kv) => {
val key = kv._1
val value = kv._2
mergedMap(key) = mergedMap.getOrElse(key, 0) + value
mergedMap
}
)

println(map3)

简单Word Count

对字符串进行切分,之后统计每个词出现的次数,并取其中排名前三的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val stringList = List("hello", "hello world", "hello scala", "hello spark from scala", "hello flink form scala")

// 对每个字符串进行切分,一个字符串得到一个word列表 map
// 将得到的列表进行扁平化 flatten
// 按照每个word进行分类 group
// 得到的Map可以计算出每个word的出现次数 map
// 得到一个Map(Word -> times),将其转换成list,排序取前3
val res = stringList
.flatMap(_.split(" "))
.groupBy(word => word)
.map((kv: (String, List[String])) => (kv._1, kv._2.length))
.toList
.sortWith(_._2 > _._2)
.take(3)
println(res)

进阶Word Count

原始输入中以及提供了部分出现次数的信息,利用这些信息,同样是完成上面Word Count的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
val tupleList: List[(String, Int)] = List(
("hello", 1),
("hello world", 2),
("hello scala", 3),
("hello spark from scala", 1),
("hello flink from scala", 2)
)
// 第一次map需要将词进行划分,并且带上自己的计数,同时需要进行扁平化
val res = tupleList
.flatMap((content: (String, Int)) => {
val strings = content._1.split(" ")
// 对分出来的每个词都需要生成一个描述计数(word, count)
strings.map(word => (word, content._2))
})
.groupBy(content => content._1)
.map(content => {
val word = content._1
val countList = content._2
// 计算countList中第二位的值的合计
val sum = countList.map(_._2).sum
(word, sum)
}).toList.sortWith(_._2 > _._2).take(3)

println(res)

Scala与Java中的容器转换

在Scala中我们经常会调用Java中的方法,但是这些方法可能返回的是Java中的容器对象。Scala中的容器与Java中的容器是可以相互转换的,这些转换可以通过一些方法很容易的完成。不过首先需要我们引入对应包:

1
import scala.collection.JavaConverters._

之后可以调用对应的asScalaasJava方法,Java中的容器对象可以使用asScala方法,而Scala中的容器对象可以使用asJava方法。之后调用对应的toxxx方法就可以完成。举例来说,我现在有一个Java中的List数组,我想把它转化为Scala中的Array,则需要通过如下方式进行:

1
2
3
4
5
6
import scala.collection.JavaConverters._

val listInJava: java.util.List[String] = new ArrayList();
// Java中的ArrayList

val arrayInScala = listInJava.asScala.toArray;

当然一些容器之间可以直接相互转换,而另一些容器之间则只支持单向的转换。转换关系如下,其中左边的是Scala中的容器对象,右边则是Java中的容器对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 双向转换
Iterator <=> java.util.Iterator
Iterator <=> java.util.Enumeration
Iterable <=> java.lang.Iterable
Iterable <=> java.util.Collection
mutable.Buffer <=> java.util.List
mutable.Set <=> java.util.Set
mutable.Map <=> java.util.Map
mutable.ConcurrentMap <=> java.util.concurrent.ConcurrentMap

// 单向转换
Seq => java.util.List
mutable.Seq => java.util.List
Set => java.util.Set
Map => java.util.Map

参考文章

  1. scala-lang-docs

  2. Java中的List转化为Scala中的Array


Scala学习笔记-核心特性(3)-集合
http://example.com/2022/05/02/Scala学习笔记-核心特性-3-集合/
作者
EverNorif
发布于
2022年5月2日
许可协议