Scala核心编程(十一)数据结构之集合操作
本章深入讲解Scala集合的高阶函数式操作,包括映射、过滤、化简、折叠、扫描等核心API,以及拉链、迭代器、流、视图、并行集合等扩展内容。
一、集合元素的映射操作
1.1 map映射操作
需求引入:将List(3,5,7)中所有元素乘以2,返回新的List(6,10,14)。
传统方法(命令式):
vallist1=List(3,5,7)varlist2=List[Int]()for(item<-list1){// 遍历list2=list2:+item*2}println(list2)// List(6, 10, 14)传统方法缺点:
- 使用了可变变量
var - 代码冗长,需要手动维护中间集合
- 不符合函数式编程风格
使用map函数式解决:
vallist1=List(3,5,7)deff1(n1:Int):Int={2*n1}vallist2=list1.map(f1)println(list2)// List(6, 10, 14)map函数签名解析
以HashSet为例:
defmap[B](f:(A)=>B):HashSet[B]| 要点 | 说明 |
|---|---|
[B] | 泛型参数,表示返回集合的元素类型 |
f: (A) => B | 接收一个函数,将A类型映射为B类型 |
| 高阶函数 | map可以接受函数作为参数,是函数式编程的核心特征 |
HashSet[B] | 返回新的集合,原集合不变(不可变性) |
高阶函数基础
objectTestHighOrderDef{defmain(args:Array[String]):Unit={valres=test(sum,6.0)println("res="+res)// res=12.0}// 高阶函数:接收函数作为参数deftest(f:Double=>Double,n1:Double)={f(n1)}defsum(d:Double):Double={d+d}}模拟实现map机制
classMyList{varlist1=List(3,5,7)varlist2=List[Int]()defmap(f:Int=>Int):List[Int]={for(item<-list1){list2=list2:+f(item)}list2}}objectMyList{defapply():MyList=newMyList()}// 使用valmyList=MyList()valmyList2=myList.map(f1)println("myList2="+myList2)// myList2=List(6, 10, 14)课堂练习:转大写
valnames=List("Alice","Bob","Nick")defupper(s:String):String={s.toUpperCase}valnames2=names.map(upper)println("names="+names2)// names=List(ALICE, BOB, NICK)1.2 flatMap扁平化映射
flat即压扁、压平、扁平化。效果是将集合中的每个元素的子元素映射到某个函数并返回新的集合。
valnames=List("Alice","Bob","Nick")defupper(s:String):String={s.toUpperCase}// 注意:每个字符串也是char集合println(names.flatMap(upper))// 输出: List(A, L, I, C, E, B, O, B, N, I, C, K)map vs flatMap对比:
| 操作 | 结果 | 说明 |
|---|---|---|
names.map(upper) | List("ALICE", "BOB", "NICK") | 每个元素映射为一个新元素 |
names.flatMap(upper) | List('A','L','I','C','E'...) | 映射后扁平化展开子元素 |
二、集合元素的过滤 - filter
作用:将符合要求的数据(筛选)放置到新的集合中。
应用案例:从List("Alice", "Bob", "Nick")中筛选首字母为’A’的元素。
valnames=List("Alice","Bob","Nick")defstartA(s:String):Boolean={s.startsWith("A")}valnames2=names.filter(startA)println("names="+names2)// names=List(Alice)三、化简(Reduce)
3.1 基本介绍
化简:将二元函数应用于集合中的元素,进行归约操作。
需求:求List(1, 20, 30, 4, 5)的和。
vallist=List(1,20,30,4,5)defsum(n1:Int,n2:Int):Int={n1+n2}valres=list.reduceLeft(sum)println("res="+res)// res=603.2 reduceLeft执行逻辑
reduceLeft(_ + _) 的执行过程: ((((1 + 20) + 30) + 4) + 5) = 60 步骤分解: 1. 1 + 20 = 21 2. 21 + 30 = 51 3. 51 + 4 = 55 4. 55 + 5 = 603.3 课堂练习
分析下面代码的输出结果:
vallist=List(1,2,3,4,5)defminus(num1:Int,num2:Int):Int={num1-num2}println(list.reduceLeft(minus))// 输出: -13 ((1-2)-3)-4)-5 = -13println(list.reduceRight(minus))// 输出: 3 1-(2-(3-(4-5))) = 3println(list.reduce(minus))// 等同于reduceLeft使用化简求最小值:
vallist=List(3,4,2,7,5)valminVal=list.reduceLeft((n1,n2)=>if(n1<n2)n1elsen2)println(minVal)// 2四、折叠(Fold)
4.1 基本介绍
fold函数将上一步返回的值作为函数的第一个参数继续传递参与运算,直到list中的所有元素被遍历。
reduceLeft与foldLeft的关系:
defreduceLeft[B>:A](@deprecatedName('f)op:(B,A)=>B):B=if(isEmpty)thrownewUnsupportedOperationException("empty.reduceLeft")elsetail.foldLeft[B](head)(op)结论:
reduceLeft就是调用的foldLeft(head),默认从集合的head元素开始操作。
相关函数:fold,foldLeft,foldRight
4.2 应用案例
vallist=List(1,2,3,4)defminus(num1:Int,num2:Int):Int={num1-num2}// foldLeft(初始值)(函数) - 柯里化写法println(list.foldLeft(5)(minus))// 5-1-2-3-4 = -5println(list.foldRight(5)(minus))// 1-(2-(3-(4-5))) = -34.3 缩写方法
| 完整写法 | 缩写 | 说明 |
|---|---|---|
list.foldLeft(1)(minus) | (1 /: list)(minus) | /: 左折叠 |
list.foldRight(10)(minus) | (list :\ 10)(minus) | :\ 右折叠 |
vallist4=List(1,9,2,8)// 左折叠: 1 - 1 - 9 - 2 - 8 = -19vari6=(1/:list4)(minus)println(i6)// -19// 左折叠: 100 - 1 - 9 - 2 - 8 = 80i6=(100/:list4)(minus)println(i6)// 80// 右折叠: 1 - (9 - (2 - (8 - 10))) = -4i6=(list4:\10)(minus)println(i6)// -4五、扫描(Scan)
5.1 基本介绍
扫描:对某个集合的所有元素做fold操作,但是会把产生的所有中间结果放置于一个集合中保存。
defminus(num1:Int,num2:Int):Int={num1-num2}// 5 (1,2,3,4,5) => (5, 4, 2, -1, -5, -10)vali8=(1to5).scanLeft(5)(minus)println(i8)// Vector(5, 4, 2, -1, -5, -10)defadd(num1:Int,num2:Int):Int={num1+num2}// 5 (1,2,3,4,5) => (5, 6, 8, 11, 15, 20)vali9=(1to5).scanLeft(5)(add)println(i9)// Vector(5, 6, 8, 11, 15, 20)5.2 课堂练习
deftest(num1:Int,num2:Int):Int={num1*num2}vali8=(1to3).scanLeft(3)(test)println(i8)// Vector(3, 3, 6, 18)// 计算过程:// 初始值: 3// 3 * 1 = 3// 3 * 2 = 6// 6 * 3 = 18六、集合综合应用案例
6.1 练习1:foldLeft存放到ArrayBuffer
valsentence="AAAAAAAAAABBBBBBBBBCCCCCCDDDDDDDD"defputArry(arr:ArrayBuffer[Char],c:Char):ArrayBuffer[Char]={arr.append(c)arr}valarr=ArrayBuffer[Char]()sentence.foldLeft(arr)(putArry)println(arr.mkString)// AAAAAAAAAABBBBBBBBBCCCCCCDDDDDDDD6.2 练习2:统计字符出现次数
Java实现:
Stringsentence="AAAAAAAAAABBBBBBBBBCCCCCCDDDDDDDD";Map<Character,Integer>charCountMap=newHashMap<>();char[]cs=sentence.toCharArray();for(charc:cs){if(charCountMap.containsKey(c)){Integercount=charCountMap.get(c);charCountMap.put(c,count+1);}else{charCountMap.put(c,1);}}System.out.println(charCountMap);Scala foldLeft实现:
valsentence="AAAAAAAAAABBBBBBBBBCCCCCCDD"defcharCount(map:Map[Char,Int],c:Char):Map[Char,Int]={map+(c->(map.getOrElse(c,0)+1))}valmap2=sentence.foldLeft(Map[Char,Int]())(charCount)println(map2)// Map(A -> 10, B -> 9, C -> 6, D -> 7)6.3 练习3:大数据经典WordCount
vallines=List("atguigu han hello ","atguigu han aaa aaa aaa ccc ddd uuu")// 1. flatMap拆分单词valres1=lines.flatMap(_.split(" "))println("res1="+res1)// res1=List(atguigu, han, hello, atguigu, han, aaa, aaa, aaa, ccc, ddd, uuu)// 2. map转换为 (单词, 1) 的元组valres2=res1.map((_,1))println("res2="+res2)// 3. groupBy按单词分组valres3=res2.groupBy(_._1)println("res3="+res3)// Map(han -> List((han,1), (han,1)), atguigu -> ...)// 4. map统计每个单词出现次数valres4=res3.map(x=>(x._1,x._2.size))println("res4="+res4)// Map(han -> 2, atguigu -> 2, hello -> 1, aaa -> 3, ccc -> 1, ddd -> 1, uuu -> 1)// 5. toList后按次数排序valres5=res4.toList.sortBy(_._2)println("res5="+res5)// List((hello,1), (ccc,1), (ddd,1), (uuu,1), (han,2), (atguigu,2), (aaa,3))// 6. 从大到小排序valres6=res5.reverse println("res6="+res6)// List((aaa,3), (han,2), (atguigu,2), (hello,1), (ccc,1), (ddd,1), (uuu,1))七、扩展内容
7.1 拉链(Zip)合并
基本介绍:将两个集合进行对偶元组合并。
vallist1=List(1,2,3)vallist2=List(4,5,6)vallist3=list1.zip(list2)println("list3="+list3)// list3=List((1,4), (2,5), (3,6))注意事项:
- 拉链的本质是两个集合的合并操作,合并后每个元素是一个对偶元组
- 如果两个集合个数不对应,会造成数据丢失
- 集合不限于List,也可以是Array等其他集合
- 取出数据时按元组方式取出:
for(item<-list3){println(item._1+" "+item._2)// 1 4, 2 5, 3 6}7.2 迭代器(Iterator)
通过iterator方法从集合获得一个迭代器,通过while循环和for表达式遍历。
valiterator=List(1,2,3,4,5).iterator// 遍历方式1:whileprintln("--------遍历方式1 ---------")while(iterator.hasNext){println(iterator.next())}// 遍历方式2:forprintln("--------遍历方式2 for ---------")for(enum<-iterator){println(enum)}底层实现:
// iterator的构建实际是AbstractIterator的一个匿名子类defiterator:Iterator[A]=newAbstractIterator[A]{varthese=selfdefhasNext:Boolean=!these.isEmptydefnext():A=...}7.3 流 Stream
基本说明:Stream是一个集合,可以用于存放无穷多个元素,但这无穷个元素并不会一次性生产出来,而是需要用到多大的区间,就会动态的生产,末尾元素遵循lazy规则(即:要使用结果才进行计算)。
创建Stream对象:
defnumsForm(n:BigInt):Stream[BigInt]=n #::numsForm(n+1)valstream1=numsForm(1)// 取出第一个元素println("head="+stream1.head)// head=1println(stream1.tail)// Stream(2, ?)println(stream1)// Stream(1, ?)使用map映射Stream:
defnumsForm(n:BigInt):Stream[BigInt]=n #::numsForm(n+1)defmulti(x:BigInt):BigInt={x*x}println(numsForm(5).map(multi))// Stream(25, ?)⚠️注意:如果使用流集合,就不能使用
last属性,否则集合会进行无限循环。
7.4 视图 View
Stream的懒加载特性,也可以对其他集合应用view方法来得到类似的效果。
特点:
view方法产出一个总是被懒执行的集合view不会缓存数据,每次都要重新计算(比如遍历View时)
应用案例:找到1-100中,数字倒序排列和它本身相同的所有数。
defmultiple(num:Int):Int={num}defeq(i:Int):Boolean={i.toString.equals(i.toString.reverse)}// 没有使用view - 立即计算所有结果valviewSquares1=(1to100).map(multiple).filter(eq)println(viewSquares1)// 使用view - 懒执行valviewSquares2=(1to100).view.map(multiple).filter(eq)println(viewSquares2)// for (x <- viewSquares2) {} // 遍历时才真正计算7.5 线程安全的集合
所有线程安全的集合都是以Synchronized开头的集合:
SynchronizedBufferSynchronizedMapSynchronizedPriorityQueueSynchronizedQueueSynchronizedSetSynchronizedStack
7.6 并行集合
Scala为了充分使用多核CPU,提供了并行集合(有别于前面的串行集合),用于多核环境的并行计算。
主要算法:
Divide and conquer(分治算法):Scala通过splitters(分解器)、combiners(组合器)等抽象层来实现,将计算工作分解为很多任务,分发给处理器完成,并合并结果返回。
Work stealin算法:主要用于任务调度负载均衡,通俗地说就是完成自己的所有任务后,发现其他人还有活没干完,主动帮他人一起干。
应用案例:
// 串行打印(1to5).foreach(println(_))// 并行打印(顺序可能不一致)(1to5).par.foreach(println(_))// 查看并行集合中元素访问的线程valresult1=(0to100).map{case_=>Thread.currentThread.getName}valresult2=(0to100).par.map{case_=>Thread.currentThread.getName}println(result1)// 所有元素由main线程处理println(result2)// 元素由多个线程并行处理7.7 操作符扩展
反引号
如果想在变量名、类名等定义中使用语法关键字(保留字),可以配合反引号:
val`val`=42// 使用保留字作为变量名中置操作符
A 操作符 B等同于A.操作符(B):
valn1=1valn2=2valr1=n1+n2// 中置写法valr2=n1.+(n2)// 方法调用写法(等价)后置操作符
A操作符等同于A.操作符,如果操作符定义时不带()则调用时不能加括号:
classOperate{def++="123"// 定义时省略()}valoper=newOperate println(oper++)// 正确// println(oper.++) // 正确// println(oper++()) // 错误!定义时没带(),调用不能加()前置操作符
+、-、!、~等操作符A等同于A.unary_操作符:
classOperate{defunary_!=println("!!!!!!!")}valoper=newOperate!oper// 前置运算符,输出: !!!!!!!赋值操作符
A 操作符= B等同于A = A 操作符 B,比如A += B等价于A = A + B。
八、本章小结
| 操作 | 方法 | 核心特点 |
|---|---|---|
| 映射 | map/flatMap | 一对一映射 / 映射后扁平化 |
| 过滤 | filter | 按条件筛选元素 |
| 化简 | reduceLeft/reduceRight | 二元归约,无初始值 |
| 折叠 | foldLeft/foldRight | 二元归约,有初始值 |
| 扫描 | scanLeft/scanRight | 保留所有中间结果 |
| 拉链 | zip | 两个集合对偶合并 |
| 迭代器 | iterator | 惰性遍历集合 |
| 流 | Stream | 无穷集合,懒加载 |
| 视图 | view | 懒执行,不缓存 |
| 并行 | .par | 多核并行计算 |
掌握这些高阶函数式操作,是写出优雅、简洁、高效Scala代码的关键!
