Scala第十六章节
章节目标
- 掌握泛型方法, 类, 特质的用法
- 了解泛型上下界相关内容
- 了解协变, 逆变, 非变的用法
- 掌握列表去重排序案例
1. 泛型
泛型的意思是泛指某种具体的数据类型
, 在Scala中, 泛型用[数据类型]
表示. 在实际开发中, 泛型一般是结合数组或者集合来使用的, 除此之外, 泛型的常见用法还有以下三种:
- 泛型方法
- 泛型类
- 泛型特质
1.1 泛型方法
泛型方法指的是把泛型定义到方法声明上, 即:该方法的参数类型是由泛型来决定的
. 在调用方法时, 明确具体的数据类型.
格式
def 方法名[泛型名称](..) = {
//...
}
需求
定义方法getMiddleElement(), 用来获取任意类型数组的中间元素.
- 思路一: 不考虑泛型直接实现(基于Array[Int]实现)
- 思路二: 加入泛型支持.
参考代码
//案例: 泛型方法演示.
//细节: 泛型方法在调用方法的时候 明确具体的数据类型.
object ClassDemo01 {
//需求: 用一个方法来获取任意类型数组的中间的元素
//思路一:不考虑泛型直接实现(基于Array[Int]实现)
//def getMiddleElement(arr: Array[Int]) = arr(arr.length / 2)
//思路二: 加入泛型支持
def getMiddleElement[T](arr: Array[T]) = arr(arr.length / 2)
def main(args: Array[String]): Unit = {
//调用方法
println(getMiddleElement(Array(1, 2, 3, 4, 5)))
println(getMiddleElement(Array("a", "b", "c")))
}
}
1.2 泛型类
泛型类指的是把泛型定义到类的声明上, 即:该类中的成员的参数类型是由泛型来决定的
. 在创建对象时, 明确具体的数据类型.
格式
class 类[T](val 变量名: T)
需求
- 定义一个Pair泛型类, 该类包含两个字段,且两个字段的类型不固定.
- 创建不同类型的Pair泛型类对象,并打印.
参考代码
//案例: 泛型-演示泛型类的使用.
//泛型类: 在创建对象的时候, 明确具体的数据类型.
object ClassDemo02 {
//1. 实现一个Pair泛型类
//2. Pair类包含两个字段,而且两个字段的类型不固定
class Pair[T](var a:T, var b:T)
def main(args: Array[String]): Unit = {
//3. 创建不同类型泛型类对象,并打印
var p1 = new Pair[Int](10, 20)
println(p1.a, p1.b)
var p2 = new Pair[String]("abc", "bcd")
println(p2.a, p2.b)
}
}
1.3 泛型特质
泛型特质指的是把泛型定义到特质的声明上, 即:该特质中的成员的参数类型是由泛型来决定的
. 在定义泛型特质的子类或者子单例对象时, 明确具体的数据类型.
格式
trait 特质A[T] {
//特质中的成员
}
class 类B extends 特质A[指定具体的数据类型] {
//类中的成员
}
需求
- 定义泛型特质Logger, 该类有一个变量a和show()方法, 它们都是用Logger特质的泛型.
- 定义单例对象ConsoleLogger, 继承Logger特质.
- 打印单例对象ConsoleLogger中的成员.
参考代码
//案例: 演示泛型特质.
object ClassDemo03 {
//1. 定义泛型特质Logger, 该类有一个a变量和show()方法, 都是用Logger特质的泛型.
trait Logger[T] {
//定义变量
val a:T
//定义方法.
def show(b:T) = println(b)
}
//2. 定义单例对象ConsoleLogger, 继承Logger特质.
object ConsoleLogger extends Logger[String]{
override val a: String = "张三"
}
//main方法, 作为程序的主入口.
def main(args: Array[String]): Unit = {
//3. 打印单例对象ConsoleLogger中的成员.
println(ConsoleLogger.a)
ConsoleLogger.show("10")
}
}
2. 上下界
我们在使用泛型(方法, 类, 特质)时,如果要限定该泛型必须从哪个类继承、或者必须是哪个类的父类。此时,就需要使用到泛型的上下界
。
2.1 上界
使用T <: 类型名
表示给类型添加一个上界,表示泛型参数必须要从该类(或本身)继承.
格式
[T <: 类型]
例如: [T <: Person]的意思是, 泛型T的数据类型必须是Person类型或者Person的子类型
需求
- 定义一个Person类
- 定义一个Student类,继承Person类
- 定义一个泛型方法demo(),该方法接收一个Array参数.
- 限定demo方法的Array元素类型只能是Person或者Person的子类
- 测试调用demo()方法,传入不同元素类型的Array
参考代码
//案例: 演示泛型的上下界之 上界.
object ClassDemo04 {
//1. 定义一个Person类
class Person
//2. 定义一个Student类,继承Person类
class Student extends Person
//3. 定义一个demo泛型方法,该方法接收一个Array参数,
//限定demo方法的Array元素类型只能是Person或者Person的子类
def demo[T <: Person](arr: Array[T]) = println(arr)
def main(args: Array[String]): Unit = {
//4. 测试调用demo,传入不同元素类型的Array
//demo(Array(1, 2, 3)) //这个会报错, 因为只能传入Person或者它的子类型.
demo(Array(new Person()))
demo(Array(new Student()))
}
}
2.2 下界
使用T >: 数据类型
表示给类型添加一个下界,表示泛型参数必须是从该类型本身或该类型的父类型.
格式
[T >: 类型]
注意:
- 例如: [T >: Person]的意思是, 泛型T的数据类型必须是Person类型或者Person的父类型
- 如果泛型既有上界、又有下界。下界写在前面,上界写在后面. 即: [T >: 类型1 <: 类型2]
需求
- 定义一个Person类
- 定义一个Policeman类,继承Person类
- 定义一个Superman类,继承Policeman类
- 定义一个demo泛型方法,该方法接收一个Array参数,
- 限定demo方法的Array元素类型只能是Person、Policeman
- 测试调用demo,传入不同元素类型的Array
参考代码
//案例: 演示泛型的上下界之 下界.
//如果你在设定泛型的时候, 涉及到既有上界, 又有下界, 一定是: 下界在前, 上界在后.
object ClassDemo05 {
//1. 定义一个Person类
class Person
//2. 定义一个Policeman类,继承Person类
class Policeman extends Person
//3. 定义一个Superman类,继承Policeman类
class Superman extends Policeman
//4. 定义一个demo泛型方法,该方法接收一个Array参数,
//限定demo方法的Array元素类型只能是Person、Policeman
// 下界 上界
def demo[T >: Policeman <: Policeman](arr: Array[T]) = println(arr)
def main(args: Array[String]): Unit = {
//5. 测试调用demo,传入不同元素类型的Array
//demo(Array(new Person))
demo(Array(new Policeman))
//demo(Array(new Superman)) //会报错, 因为只能传入: Policeman类获取它的父类型, 而Superman是Policeman的子类型, 所以不行.
}
}
3. 协变、逆变、非变
在Spark的源代码中大量使用到了协变、逆变、非变,学习该知识点对我们将来阅读spark源代码很有帮助。
- 非变: 类A和类B之间是父子类关系, 但是Pair[A]和Pair[B]之间没有
任何关系
. - 协变: 类A和类B之间是父子类关系, Pair[A]和Pair[B]之间也有
父子类
关系. - 逆变: 类A和类B之间是父子类关系, 但是Pair[A]和Pair[B]之间是
子父类
关系.
如下图:
3.1 非变
语法格式
class Pair[T]{}
- 默认泛型类是
非变的
- 即: 类型B是A的子类型,Pair[A]和Pair[B]没有任何从属关系
3.2 协变
语法格式
class Pair[+T]
- 类型B是A的子类型,Pair[B]可以认为是Pair[A]的子类型
- 参数化类型的方向和类型的方向是一致的。
3.3 逆变
语法格式
class Pair[-T]
- 类型B是A的子类型,Pair[A]反过来可以认为是Pair[B]的子类型
- 参数化类型的方向和类型的方向是相反的
3.4 示例
需求
- 定义一个Super类、以及一个Sub类继承自Super类
- 使用协变、逆变、非变分别定义三个泛型类
- 分别创建泛型类对象来演示协变、逆变、非变
参考代码
//案例: 演示非变, 协变, 逆变.
object ClassDemo06 {
//1. 定义一个Super类、以及一个Sub类继承自Super类
class Super //父类
class Sub extends Super //子类
//2. 使用协变、逆变、非变分别定义三个泛型类
class Temp1[T] //非变
class Temp2[+T] //协变
class Temp3[-T] //逆变.
def main(args: Array[String]): Unit = {
//3. 分别创建泛型类来演示协变、逆变、非变
//演示非变.
val t1:Temp1[Sub] = new Temp1[Sub]
//val t2:Temp1[Super] = t1 //编译报错, 因为非变是: Super和Sub有父子类关系, 但是Temp1[Super] 和 Temp1[Sub]之间没有关系.
//演示协变
val t3:Temp2[Sub] = new Temp2[Sub]
val t4:Temp2[Super] = t3 //不报错, 因为协变是: Super和Sub有父子类关系, 所以Temp2[Super] 和 Temp2[Sub]之间也有父子关系.
//Temp2[Super]是父类型, Temp2[Sub]是子类型.
//演示逆变
val t5:Temp3[Super] = new Temp3[Super]
val t6:Temp3[Sub] = t5 //不报错, 因为逆变是: Super和Sub有父子类关系, 所以Temp3[Super] 和 Temp3[Sub]之间也有子父关系.
//Temp3[Super]是子类型, Temp3[Sub]是父类型.
}
}
4. 案例: 列表去重排序
4.1 需求
-
已知当前项目下的data文件夹中有一个1.txt文本文件, 文件内容如下:
11 6 5 3 22 9 3 11 5 1 2
-
对上述数据去重排序后, 重新写入到data文件夹下的2.txt文本文件中, 即内容如下:
1 2 3 5 6 9 11 22
4.2 目的
考察泛型, 列表, 流
相关的内容.
4.3 参考代码
import java.io.{BufferedWriter, FileWriter}
import scala.io.Source
//案例: 列表去重排序, 并写入文件.
object ClassDemo07 {
def main(args: Array[String]): Unit = {
//1. 定义数据源对象.
val source = Source.fromFile("./data/1.txt")
//2. 从指定文件中读取所有的数据(字符串形式)
val list1:List[String] = source.mkString.split("\\s+").toList
//3. 把List[String]列表转换成List[Int]
val list2:List[Int] = list1.map(_.toInt)
//4. 把List[Int]转换成Set[Int], 对列表元素去重.
val set:Set[Int] = list2.toSet
//5. 把Set[Int]转成List[Int], 然后升序排列
val list3:List[Int] = set.toList.sorted
//println(list3)
//6. 把数据重新写入到data文件夹下的2.txt文件中.
val bw = new BufferedWriter(new FileWriter("./data/2.txt"))
for(i <- list3) {
bw.write(i.toString)
bw.newLine() //别忘记加换行
}
//7. 释放资源
bw.close()
}
}
Scala第十七章节
章节目标
- 了解集合的相关概念
- 掌握Traversable集合的用法
- 掌握随机学生序列案例
1. 集合
1.1 概述
但凡了解过编程的人都知道程序 = 算法 + 数据结构
这句话, 它是由著名的瑞士计算机科学家尼古拉斯·沃斯
提出来的, 而他也是1984年图灵奖的获得者. 算法指的是计算的一系列有效, 通用的步骤. 算法和数据结构是程序设计中相辅相成的两个方面, 因此数据结构也是编程中很重要的一个方面. 很多编程语言都提供了数据结构的对应编程库,
并称之为集合库(Collection Library). Scala中也有集合库, 它的优点如下:
-
易于使用
使用集合库提供的20到50个左右的方法, 灵活的组合运用, 就可以解决大部分的集合问题.
-
简洁
简单的一个单词(例如: foreach), 就可以实现一个或者多个循环操作.
-
安全
绝大部分错误都可以在编译期被发现.
-
快速
集合类型的方法实现时, 都进行了调优, 用户可以根据需求选择合适的集合.
-
统一
Scala的集合有非常严谨的继承体系, 相似类型的集合拥有同样的一组方法, 以及属于自己独有的方法.
1.2 分类
Scala同时支持不可变集合和可变集合
, 因为不可变集合可以安全的并发访问, 所以它也是默认使用的集合类库. 在Scala中, 对于几乎所有的集合类, 都提供了可变和不可变两个版本, 具体如下:
-
不可变集合: 指的是
集合内的元素一旦初始化完成就不可再进行更改, 任何对集合的改变都将生成一个新的集合.
都在
scala.collection.immutable
这个包下, 使用时无需手动导包. -
可变集合: 指的是
这个集合本身可以动态变化, 且可变集合提供了改变集合内元素的方法
都在
scala.collection.mutable
这个包下, 使用是需要手动导包.
如下图:
小技巧:
可变集合比不可变集合更加丰富.
例如: 在Seq集合中, 增加了Buffer集合, 我们常用的有: ArrayBuffer和ListBuffer.
当我们接触一个新的继承体系是, 建议采用
学顶层, 用底层
的方式.
顶层定义的是整个继承体系所共有的内容.
而底层才是具体的体现, 实现.
2. Traversable
2.1 概述
Traversable是一个特质(trait), 它是其他集合的父特质, 它的子特质immutable.Traversable和mutable.Traversable分别是不可变集合和可变集合的父特质, 集合中大部分通用的方法都是在这个特质中定义的. 因此了解它的功能对学习其他集合类至关重要.
2.2 格式
-
格式一: 创建空的Traversable对象.
//方式一: 通过empty方法实现. val t1 = Traversable.empty[Int] //方式二: 通过小括号方式实现 val t2 = Traversable[Int]() //方式三: 通过Nil实现. val t3 = Nil
-
格式二: 创建带参数的Traversable对象.
//方式一: 通过toTraversable()方法实现 val t1 = List(1, 2, 3).toTraversable //方式二: 通过Traversable的伴生对象的apply()方法实现. val t1 = Traversable(1, 2, 3)
2.3 示例一: 创建Traversable对象
- 创建空的, 用来存储Int类型数据的Traversable对象.
- 创建Traversable集合对象, 存储数字1, 2, 3, 并将结果打印到控制台上.
参考代码
//案例: 演示创建Traversable对象.
object ClassDemo01 {
def main(args: Array[String]): Unit = {
//1. 创建空的, 用来存储Int类型数据的Traversable对象.
//1.1 创建对象.
val t1: Traversable[Int] = Traversable.empty[Int]
val t2: Traversable[Int] = Traversable[Int]()
val t3: Traversable[Int] = Nil
//1.2 比较它们是否相等.
println(t1 == t2) //==比较的是集合中的数据.
println(t1 == t3)
println(t2 == t3)
println(t1 eq t2) //eq比较的是集合的地址值.
println(t1 eq t3)
println(t2 eq t3)
println("-" * 15)
//2. 创建Traversable集合对象, 存储数字1, 2, 3, 并将结果打印到控制台上.
//2.1 通过toTraversable()方法实现.
val t4: Traversable[Int] = List(1, 2, 3).toTraversable
val t5: Traversable[Int] = Set(1, 2, 3).toTraversable
//2. 通过Traversable的伴生对象的apply()方法实现.
val t6:Traversable[Int] = Traversable(11, 22, 33, 44, 55)
//3. 打印结果(因为Traversable是特质, 所以底层还是通过它的具体子类来实现的).
println(s"t4: ${t4}")
println(s"t5: ${t5}")
println(s"t6: ${t6}")
}
}
2.4 案例二: 转置Traversable集合
了解过线性代数的同学都知道, 矩阵有一个转置的操作, 在Scala中, 可以通过transpose()方法
来实现类似的操作.
如下图:
注意:
进行转置操作时, 程序会自动检测每个集合中的元素个数是否一致, 如果一致, 则转置成功. 如果不一致, 则报错.
需求
- 定义一个Traversable集合t1, 它有三个元素, 每个元素都是Traversable集合, 并分别存储如下数据:
- 第一个元素存储(1, 4, 7), 第二个元素存储(2, 5, 8), 第三个元素存储(3, 6, 9).
- 通过transpose方法, 对集合t1进行转置操作.
- 打印结果.
参考代码
//案例: 演示转置集合.
object ClassDemo02 {
def main(args: Array[String]): Unit = {
//1. 定义一个Traversable集合t1, 它有三个元素, 每个元素都是Traversable集合, 并分别存储如下数据:
//2. 第一个元素存储(1, 4, 7), 第二个元素存储(2, 5, 8), 第三个元素存储(3, 6, 9).
val t1 = Traversable(Traversable(1, 4, 7), Traversable(2, 5, 8), Traversable(3, 6, 9))
//3. 通过transpose方法, 对集合t1进行转置操作.
val t2 = t1.transpose
//4. 打印结果.
println(t2)
}
}
2.5 案例三: 拼接集合
在实际开发中, 数据是从多渠道获取到的, 所以我们经常需要拼接一些数据, 在Scala中, 我们可以通过++
来拼接数据, 但是这种方式会创建大量的临时集合(即: 每++一次, 就会创建一个新的临时集合), 针对这种情况, 我们可以通过concat()方法
来实现. 该方法会预先计算出所需的集合的大小, 然后生成一个集合, 减少了中间无用的临时集合, 所以它更加有效.
需求
- 已知有三个Traversable集合, 分别存储(11, 22, 33), (44, 55), (66, 77, 88, 99)元素.
- 通过concat()方法拼接上述的三个集合.
- 将拼接后的结果打印到控制台上.
参考代码
//案例: 演示concat()方法, 拼接集合.
object ClassDemo03 {
def main(args: Array[String]): Unit = {
//1. 已知有三个Traversable集合, 分别存储(11, 22, 33), (44, 55), (66, 77, 88, 99)元素.
val t1 = Traversable(11, 22, 33)
val t2 = Traversable(44, 55)
val t3 = Traversable(66, 77, 88, 99)
//2. 通过concat()方法拼接上述的三个集合.
val t4 = Traversable.concat(t1, t2, t3)
//3. 将拼接后的结果打印到控制台上.
println(s"拼接后结果为: ${t4}")
}
}
2.6 案例四: 利用偏函数筛选元素
在Scala中, 我们还可以通过collect()方法
实现偏函数结合集合
来使用, 从而来从集合中筛选指定的数据.
格式
def collect[B](pf: PartialFunction[A, B]): Traversable[B]
解释:
[B]
表示通过偏函数处理后, 返回值的数据类型.pf: PartialFunction[A, B]
表示collect()方法需要传入一个偏函数对象.Traversable[B]
表示返回的具体数据的集合.
需求
- 已知有一个Traversable集合, 存储元素为: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.
- 通过collect方法筛选出集合中所有的偶数.
参考代码
//案例: 通过偏函数筛选出集合中所有的偶数.
object ClassDemo04 {
def main(args: Array[String]): Unit = {
//1. 已知有一个Traversable集合, 存储元素为: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.
val t1 = (1 to 10).toTraversable
//2. 通过collect方法筛选出集合中所有的偶数.
val t2 = t1.collect( {
case x if x % 2 == 0 => x
})
//3. 打印结果.
println(t2)
}
}
2.7 案例五: 计算集合元素的阶乘
假设一个Traversable[Int]集合中包含(1, 2, 3, 4, 5)五个数字, 如果让我们计算每个元素的阶乘, 并放到一个新的Traversable[Int]集合中, 我们可以通过递归来实现, 但是这种实现方式有弊端, 每次计算都是从头开始计算的, 例如: 获取5的阶乘, 是通过5 * 4 * 3 * 2 * 1
计算出来的, 并没有运用之前计算出来的4的阶乘的结果. 此时, 我们就可以通过scan()方法
来优化此需求了, 它不仅将中间的计算结果放入新的集合中, 并且还会把中间结果传递给下一次的函数调用.
格式
def scan[B](z: B)(op: (B, B) => B)
解释:
[B]
表示返回值的数据类型.(z: B)
表示初始化值.(op: (B, B) => B)
表示一个具体的运算函数.- scan()方法等价于scanLeft()方法, 还有一个跟它相反的方法scanRight().
需求
- 定义Traversable集合t1, 存储1, 2, 3, 4, 5这五个数字.
- 假设初始值为1, 通过scan()方法, 分别获取t1集合中各个元素的阶乘值.
- 打印结果.
参考代码
//案例: 通过scan()方法, 获取集合中元素的阶乘值.
object ClassDemo05 {
def main(args: Array[String]): Unit = {
//1. 定义Traversable集合t1, 存储1, 2, 3, 4, 5这五个数字.
val t1 = Traversable(1, 2, 3, 4, 5)
//2. 假设初始值为1, 通过scan()方法, 分别获取t1集合中各个元素的阶乘值.
val t2 = t1.scan(1)(_ * _)
//3. 打印结果.
println(t2)
}
}
2.8 案例六: 获取集合的指定元素
集合是用来存储数据的, 既然能存储, 那肯定也可以从集合中获取我们想要的数据, 可以通过如下的方法实现:
-
head: 获取集合的第一个元素, 如果元素不存在, 则抛出
NoSuchElementException异常
. -
last: 获取集合的最后一个元素, 如果元素不存在, 则抛出
NoSuchElementException异常
. -
headOption: 获取集合的第一个元素, 返回值类型是Option.
-
lastOption: 获取集合的最后一个元素, 返回值类型是Option.
-
find: 查找集合中第一个满足指定条件的元素.
-
slice: 截取集合中的一部分元素.
def slice(from:Int, until: Int): Traversable[A]
注意:
截取从from(起始索引)开始, 至until索引(结束索引)结束的元素, 包含from索引, 但是不包含until索引.
需求
- 定义一个Traversable集合, 包含1, 2, 3, 4, 5, 6这六个元素.
- 分别通过head, headOption, last, lastOption获取集合中的首尾第一个元素, 并打印.
- 通过find方法获取集合中第一个偶数, 并打印.
- 通过slice()方法获取3, 4, 5这三个元素, 并将它们放到一个新的Traversable集合中, 然后打印结果.
参考代码
//案例: 获取Traversable对象的特定的元素
object ClassDemo06 {
def main(args: Array[String]): Unit = {
//1. 定义一个Traversable集合, 包含1, 2, 3, 4, 5, 6这六个元素.
val t1 = (1 to 6).toTraversable
//2. 分别通过head, headOption, last, lastOption获取集合中的首尾第一个元素, 并打印.
println(t1.head)
println(t1.last)
println(t1.headOption)
println(t1.lastOption)
//3. 通过find方法获取集合中第一个偶数, 并打印.
println(t1.find(_ % 2 == 0))
//4. 通过slice()方法获取3, 4, 5这三个元素, 然后打印结果.
val t2 = t1.slice(2, 5)
println(t2)
}
}
2.9 案例七: 判断元素是否合法
如果我们遇到判断集合中所有的元素是否都满足指定的条件, 或者任意元素满足指定的条件
这种需求时, 就可以考虑使用forall()方法和exists()方法
来实现了.
-
forall(): 如果集合中所有元素都满足指定的条件则返回true, 否则返回false.
def forall(p: (A) => Boolean): Boolean
-
exist(): 只要集合中任意一个元素满足指定的条件就返回true, 否则返回false.
def exists(p: (A) => Boolean): Boolean
需求
- 定义Traversable集合t1, 包含1到6这六个数字.
- 通过forall()方法实现, 判断t1中的元素是否都是偶数.
- 通过exists()方法实现, 判断t1中是否有偶数.
参考代码
//案例: 判断元素是否合法
object ClassDemo07 {
def main(args: Array[String]): Unit = {
//1. 定义Traversable集合t1, 包含1到6这六个数字.
val t1 = (1 to 6).toTraversable
//2. 通过forall()方法实现, 判断t1中的元素是否都是偶数.
println(t1.forall(_ % 2 == 0)) //所有的元素都要满足条件.
//3. 通过exists()方法实现, 判断t1中是否有偶数.
println(t1.exists(_ % 2 == 0)) //只要有一个元素满足条件即可.
}
}
2.10 案例八: 聚合函数
如果我们想统计集合中满足条件的元素个数, 或者计算集合元素和, 乘积, 求最大值, 最小值等操作
, 就可以用到如下的这些方法了:
-
count: 统计集合中满足条件的元素个数.
def count(p: (A) => Boolean): Int
-
sum: 获取集合中所有的元素和.
-
product: 获取集合中所有的元素乘积.
-
max: 获取集合中所有元素的最大值.
-
min: 获取集合中所有元素的最小值.
需求
- 定义Traversable集合t1, 包含1到6这六个数字.
- 通过count()方法统计t1集合中所有奇数的个数, 并打印结果.
- 通过sum()方法获取t1集合中所有的元素和, 并打印结果.
- 通过product()方法获取t1集合中所有的元素乘积, 并打印结果.
- 通过max()方法获取t1集合中所有元素的最大值, 并打印结果.
- 通过min()方法获取t1集合中所有元素的最小值, 并打印结果.
参考代码
//案例: 演示聚合操作.
object ClassDemo08 {
def main(args: Array[String]): Unit = {
//1. 定义Traversable集合t1, 包含1到6这六个数字.
val t1 = (1 to 6).toTraversable
//2. 通过count()方法统计t1集合中所有奇数的个数, 并打印结果.
println(t1.count(_ % 2 == 0))
println(t1.filter(_ % 2 == 0).size) //不推荐使用, 因为会产生一个新的Traversable对象.
//3. 通过sum()方法获取t1集合中所有的元素和, 并打印结果.
println(t1.sum)
//4. 通过product()方法获取t1集合中所有的元素乘积, 并打印结果.
println(t1.product)
//5. 通过max()方法获取t1集合中所有元素的最大值, 并打印结果.
println(t1.max)
//6. 通过min()方法获取t1集合中所有元素的最小值, 并打印结果.
println(t1.min)
}
}
2.11 案例九: 集合类型转换
有时候, 我们需要将Traversable集合转换成其他的集合来进行操作, 这时候就要用toXxx()方法
了.
注意: 上述的Xxx表示目标集合的名称, 例如: toList, toSet, toArray, toSeq等等…
需求
- 定义Traversable集合t1, 包含1到5这五个数字.
- 将t1集合分别转成数组(Array), 列表(List), 集(Set)这三种形式, 并打印结果.
参考代码
//案例: 集合类型转换.
object ClassDemo09 {
def main(args: Array[String]): Unit = {
//1. 定义Traversable集合t1, 包含1到5这五个数字.
val t1 = (1 to 5).toTraversable
//2. 将t1集合分别转成数组(Array), 列表(List), 集(Set)这三种形式, 并打印结果.
val arr = t1.toArray
val list = t1.toList
val set = t1.toSet
//3. 打印结果.
println(arr)
println(list)
println(set)
}
}
2.12 案例十: 填充元素
如果我们需要往集合中快速添加相同元素, 例如: 生成5个都是"abc"的Traversable对象, 就需要用到fill()和iterate()方法了
, 那如果是想生成指定间隔的队列元素, 就可以通过range()方法
来实现了, 具体如下:
- fill()方法: 快速生成指定数量的元素.
- iterate()方法: 根据指定的条件, 生成指定个数的元素.
- range()方法: 生成某个区间内的指定间隔的所有数据.
需求
-
通过fill()方法, 生成一个Traversable集合, 该集合包含5个元素, 值都是"传智播客".
-
通过fill()方法, 生成一个Traversable集合, 该集合包含3个随机数.
-
通过fill()方法, 生成一个Traversable集合, 格式如下:
List(List(传智播客, 传智播客), List(传智播客, 传智播客), List(传智播客, 传智播客), List(传智播客, 传智播客), List(传智播客, 传智播客))
-
通过iterate()方法, 生成一个Traversable集合, 该集合包含5个元素, 分别为:1, 10, 100, 1000, 10000.
-
通过range()方法, 获取从数字1开始, 截止数字21之间, 间隔为5的所有数据.
参考代码
import scala.util.Random
//案例: 演示填充元素.
object ClassDemo10 {
def main(args: Array[String]): Unit = {
//1. 通过fill()方法, 生成一个Traversable集合, 该集合包含5个元素, 值都是"传智播客".
println(Traversable.fill(5)("传智播客"))
//2. 通过fill()方法, 生成一个Traversable集合, 该集合包含3个随机数.
println(Traversable.fill(5)(Random.nextInt(100)))
//3. 通过fill()方法, 生成一个Traversable集合, 该集合有5个Traversable集合, 每个Traversable集合有两个元素.
//5表示有5个Traversable, 2表示每个Traversable都有2个元素.
println(Traversable.fill(5, 2)("传智播客"))
//4. 通过iterate()方法, 生成一个Traversable集合, 该集合包含5个元素, 分别为:1, 10, 100, 1000, 10000.
//1表示初始化值, 5表示最终要获取的元素个数
println(Traversable.iterate(1, 5)(_ * 10))
//5. 通过range()方法, 获取从数字1开始, 截止数字21之间, 间隔为5的所有数据.
println(Traversable.range(1, 21, 5)) //从1开始, 截止到20, 间隔5的所有数据
println(Traversable.range(1, 5)) //从1开始, 截止到5, 间隔1的所有数据(如果不指定, 默认间隔为1)
}
}
3. 案例: 随机学生序列
3.1 需求
- 定义一个Traversable集合, 包含5个学生(属性为: 姓名, 年龄)的信息, 且学生的姓名和年龄信息是随机生成的.
- 假设姓名信息为(“张三”, “李四”, “王五”, “赵六”, “田七”), 年龄的取值范围是: [20, 30), 前闭后开写法.
- 按照学生的年龄信息降序排列后, 将结果打印到控制台上.
3.2 目的
考察样例类, 随机数, 集合
相关的内容.
3.3 步骤
- 创建Student样例类, 属性为: 姓名, 年龄, 用来记录学生的信息.
- 定义列表, 记录学生的姓名信息, 值为: “张三”, “李四”, “王五”, “赵六”, “田七”.
- 创建随机数对象r, 用来实现获取一些随机值的操作.
- 创建Traversable集合, 包含5个随机的学生信息.
- 将Traversable集合转换成List列表.
- 通过列表的sortWith()方法, 按照学生的年龄降序排列.
- 打印结果.
3.4 参考代码
import scala.util.Random
//案例: 随机学生序列
object ClassDemo11 {
//1. 创建Student样例类, 属性为: 姓名, 年龄, 用来记录学生的信息.
case class Student(name:String, age:Int)
def main(args: Array[String]): Unit = {
//2. 定义列表, 记录学生的姓名信息, 值为: "张三", "李四", "王五", "赵六", "田七".
val names: List[String] = List("张三", "李四", "王五", "赵六", "田七")
//3. 创建随机数对象r, 用来实现获取一些随机值的操作.
val r: Random = new Random()
//4. 创建Traversable集合, 包含5个随机的学生信息.
val t1: Traversable[Student] = Traversable.fill(5)(Student(names(r.nextInt(names.size)), 20 + r.nextInt(10)))
//5. 将Traversable集合转换成List列表.
val t2: List[Student] = t1.toList
//6. 通过列表的sortWith()方法, 按照学生的年龄降序排列.
//下边两种排序方式都可以.
//val t3 = t2.sortBy(_.age).reverse
val t3 = t2.sortWith(_.age > _.age)
//7. 打印结果.
println(t3)
}
}
Scala第十八章节
章节目标
- 掌握Iterable集合相关内容.
- 掌握Seq集合相关内容.
- 掌握Set集合相关内容.
- 掌握Map集合相关内容.
- 掌握统计字符个数案例.
1. Iterable
1.1 概述
Iterable代表一个可以迭代的集合, 它继承了Traversable特质, 同时也是其他集合的父特质. 最重要的是, 它定义了获取迭代器(iterator)的方法: def iterator: Iterator[A]
, 这是一个抽象方法, 它的具体实现类需要实现这个方法, 从而实现迭代的返回集合中的元素
.
1.2 分类
Traversable提供了两种遍历数据的方式:
-
通过iterator()方法实现, 迭代访问元素的功能.
这种方式属于
主动迭代
, 我们可以通过hasNext()检查是否还有元素, 并且可以主动的调用next()方法获取元素, 即: 我们可以自主的控制迭代过程. -
通过foreach()方法实现, 遍历元素的功能.
这种方式属于
被动迭代
, 我们只提供一个函数, 并不能控制遍历的过程, 即: 迭代过程是由集合本身控制的.
1.3 案例一: 遍历集合
需求
- 定义一个列表, 存储1, 2, 3, 4, 5这五个数字.
- 通过iterator()方法遍历上述的列表.
- 通过foreach()方法遍历上述的列表.
参考代码
//案例: 遍历集合元素.
object ClassDemo01 {
def main(args: Array[String]): Unit = {
//1. 定义一个列表, 存储1, 2, 3, 4, 5这五个数字.
val list1 = (1 to 5).toList
//2. 通过iterator()方法遍历上述的列表.
val it: Iterator[Int] = list1.iterator
while(it.hasNext) {
val ele = it.next()
println(ele)
if(ele == 3) println("额外的操作: " + ele * 10)
}
println("-" * 15)
//3. 通过foreach()方法遍历上述的列表.
list1.foreach(println(_))
}
}
1.4 案例二: 分组遍历
如果遇到将Iterable对象中的元素分成固定大小的组, 然后遍历
这种需求, 就可以通过grouped()方法
来实现了.
格式
def grouped(size: Int): Iterator[Iterable[A]]
需求
- 定义一个Iterable集合, 存储1~13之间的所有整数.
- 通过grouped()方法, 对Iterable集合按照5个元素为一组的形式进行分组, 遍历并打印结果.
参考代码
//案例: 演示grouped分组功能.
object ClassDemo02 {
def main(args: Array[String]): Unit = {
//1. 定义Iterable集合, 存储1~13之间的数字.
val i1 = (1 to 13).toIterable
//2. 通过grouped方法按照5个元素为一组的方式, 对Iterable集合分组.
val result1 = i1.grouped(5)
//3. 遍历元素, 打印结果.
while (result1.hasNext) {
println(result1.next())
}
}
}
1.5 案例三: 按照索引生成元组
Iterable集合中存储的每个元素都是有索引的, 如果我们想按照元素 -> 索引
这种格式, 生成一个新的集合, 此时, 就需要用到zipWithIndex()方法
了.
需求
- 定义一个Iterable集合, 存储"A", “B”, “C”, “D”, "E"这五个字符串.
- 通过zipWithIndex()方法, 按照 字符串->索引这种格式, 生成新的集合.
- 重新按照 索引->字符串这种格式, 生成新的集合.
- 打印结果.
参考代码
//案例: 按照索引生成元组
object ClassDemo03 {
def main(args: Array[String]): Unit = {
//1. 定义一个Iterable集合, 存储"A", "B", "C", "D", "E"这五个字符串.
val i1 = Iterable("A", "B", "C", "D", "E")
//2. 通过zipWithIndex()方法, 按照 字符串->索引这种格式, 生成新的集合.
val i2 = i1.zipWithIndex //List((A,0), (B,1), (C,2))
//3. 重新按照 索引->字符串这种格式, 生成新的集合.
val i3 = i1.zipWithIndex.map(x => x._2 -> x._1)
//4. 打印结果.
println(i2)
println(i3)
}
}
1.6 案例四: 判断集合是否相同
有时候, 我们不仅想判断两个集合中的元素是否全部相同, 而且还要求这两个集合元素的迭代顺序保持一致
, 此时, 就可以通过sameElements()方法
来实现该需求了.
即sameElements()方法的功能是: 判断集合所包含的元素及元素的迭代顺序是否一致.
需求
- 定义Iterable集合list1, 包含"A", “B”, "C"这三个元素.
- 通过sameElements()方法, 判断list1和Iterable(“A”, “B”, “C”)集合是否相同.
- 通过sameElements()方法, 判断list1和Iterable(“A”, “C”, “B”)集合是否相同.
- 定义Iterable集合list2, 包含"A", “B”, “C”, "D"这四个元素.
- 通过sameElements()方法, 判断list1和list2是否相同.
参考代码
import scala.collection.mutable
//案例: 检查两个Iterable是否包含相同的元素.
object ClassDemo04 {
def main(args: Array[String]): Unit = {
//1. 定义Iterable集合i1, 包含"A", "B", "C"这三个元素.
val i1 = Iterable("A", "B", "C")
//2. 判断i1和Iterable("A", "B", "C")集合是否相同.
println(i1.sameElements(Iterable("A", "B", "C")))
//3. 判断i1和Iterable("A", "C", "B")集合是否相同.
println(i1.sameElements(Iterable("A", "C", "B")))
//4. 定义Iterable集合i2, 包含"A", "B", "C", "D"这四个元素.
val i2 = Iterable("A", "B", "C", "D")
//5. 判断i1和i2是否相同.
println(i1.sameElements(i2))
//6. 扩展: 创建HashSet集合存储1, 2, 创建TreeSet集合存储2, 1, 然后判断它们是否相同.
val hs = mutable.HashSet(1, 2)
val ts = mutable.TreeSet(2, 1)
println(hs.sameElements(ts)) //结果是true, 因为TreeSet会对元素排序.
}
}
2. Seq
2.1 概述
Seq特质代表按照一定顺序排列的元素序列
, 序列是一种特别的可迭代集合, 它的元素特点是有序(元素存取顺序一致), 可重复, 有索引
.
2.2 分类
继承关系如下图:
IndexedSeq和LinearSeq是Seq的子特质, 代表着序列的两大子门派.
-
IndexedSeq特质
代表索引序列, 相对于Seq来说, 它并没有增加额外的方法, 对于随机访问元素的方法来讲, 它更加有效, 常用的子集合有: NumericRange, Range, Vector, String等.-
Range集合
Range代表一个有序的整数队列, 每个元素之间的间隔相同, 例如奇数队列:1, 3, 5, 7, 9, 但是斐波那契数列1, 1, 2, 3, 5, 8就不属于Range类. 简单来说, Range类似于数学中的等差数列.
-
NumericRange集合
NumericRange集合是一个更通用的等差队列, 可以生成Int, BigInteger, Short, Byte等类型的序列.
-
Vector集合
Vector是一个通用的不可变的数据结构, 相对来讲, 它获取数据的时间会稍长一些, 但是随机更新数据要比数组和链表快很多.
-
-
LinearSeq特质
代表线性序列, 它通过链表的方式实现, 因此它的head, tail, isEmpty执行起来相对更高效. 常用的子集合有: List, Stack, Queue等.-
Stack: 表示
栈
数据结构, 元素特点是先进后出
.由于历史的原因, Scala当前的库中还包含一个immutable.Stack, 但当前已经被标记为弃用, 因为它的设计不怎么优雅, 而且性能也不太好, 因为栈会涉及到大量元素的进出, 所以不可变栈的应用场景还是比较小的, 最常用的还是可变栈, 例如: mutable.Stack, mutable.ArrayStack.
- mutable.Stack: 是通过List, 也就是链表的方式实现的, 增删快, 查询慢.
- mutable.ArrayStack: 是通过Array, 也就是数组的方式实现的, 查询快, 增删慢.
-
Queue: 表示
队列
数据结构, 元素特点是先进先出
.
-
2.3 案例一: 创建Seq集合
需求
创建Seq集合, 存储元素1, 2, 3, 4, 5, 并打印结果.
参考代码
//案例: 创建Seq对象.
object ClassDemo01 {
def main(args: Array[String]): Unit = {
//1. 创建Seq集合, 存储元素1, 2, 3, 4, 5.
val s1 = (1 to 5).toSeq
//2. 打印结果.
println(s1)
}
}
2.4 案例二: 获取长度及元素
因为Seq集合的每个元素都是有索引的, 所以我们可以通过索引直接获取其对应的元素, 而且可以通过length()或者size()方法获取集合的长度
.
需求
- 创建Seq集合, 存储元素1, 2, 3, 4, 5.
- 打印集合的长度.
- 获取索引为2的元素.
参考代码
//案例: 获取长度及元素
object ClassDemo02 {
def main(args: Array[String]): Unit = {
//1. 创建Seq集合, 存储元素1, 2, 3, 4, 5.
val s1 = (1 to 5).toSeq
//2. 打印集合的长度.
println(s1.length, s1.size)
println("-" * 15)
//3. 获取索引为2的元素.
println(s1(2))
println(s1.apply(2))
}
}
2.5 案例三: 获取指定元素的索引值
格式
获取指定元素的索引值, 我们可以通过indexOf(), lastIndexOf(), indexWhere(), lastIndexWhere(),indexOfSlice()
方法来实现, 具体功能如下:
- indexOf: 获取指定元素在列表中第一次出现的位置.
- lastIndexOf: 获取指定元素在列表中最后一次出现的位置.
- indexWhere: 获取满足条件的元素, 在集合中第一次出现的索引.
- lastIndexWhere: 获取满足条件的元素, 在集合中最后一次出现的索引.
- indexOfSlice: 获取指定的子序列在集合中第一次出现的位置.
注意: 上述方式都是查找到指定数据后则返回对应的索引, 如果找不到此数据, 则返回-1.
需求
- 定义Seq集合, 存储数据: 1, 2, 4, 6, 4, 3, 2.
- 获取元素2在集合中第一次出现的索引, 并打印.
- 获取元素2在集合中最后一次出现的索引, 并打印.
- 获取集合中, 小于5的第一个偶数的索引, 并打印.
- 从索引2位置开始查找集合中, 小于5的第一个偶数的索引, 并打印.
- 获取集合中, 小于5的最后一个偶数的索引, 并打印.
- 获取子序列Seq(1, 2)在t1集合中, 第一次出现的索引, 并打印.
- 从索引3开始查找子序列Seq(1, 2)在t1集合中, 第一次出现的索引, 并打印.
参考代码
//案例: 获取指定元素的索引值
object ClassDemo03 {
def main(args: Array[String]): Unit = {
//1. 定义Seq集合, 存储数据: 1, 2, 4, 6, 4, 3, 2.
val s1 = Seq(1, 2, 4, 6, 4, 3, 2)
//2. 获取元素2在集合中第一次出现的索引, 并打印.
println(s1.indexOf(2))
//3. 获取元素2在集合中最后一次出现的索引, 并打印.
println(s1.lastIndexOf(2))
println("-" * 15)
//4. 获取集合中, 小于5的第一个偶数的索引, 并打印.
println(s1.indexWhere(x => (x % 2 == 0) && x < 5))
//5. 从索引2位置开始查找集合中, 小于5的第一个偶数的索引, 并打印.
println(s1.indexWhere(x => (x % 2 == 0) && x < 5, 2))
//6. 获取集合中, 小于5的最后一个偶数的索引, 并打印.
println(s1.lastIndexWhere(x => (x % 2 == 0) && x < 5))
//7. 获取子序列Seq(1, 2)在t1集合中, 第一次出现的索引, 并打印.
println(s1.indexOfSlice(Seq(1, 2)))
//8. 从索引3开始查找子序列Seq(1, 2)在t1集合中, 第一次出现的索引, 并打印.
println(s1.indexOfSlice(Seq(1,2), 3))
}
}
2.6 案例四: 判断是否包含指定数据
如果我们想判断序列是否以指定的数据开头或者结尾, 以及判断序列是否包含指定的数据
, 就可以通过startsWith(), endsWith(), contains(), containsSlice()
来实现了, 具体如下:
- startsWith: 判断集合是否以指定的子序列开头.
- endsWith: 判断集合是否以指定的子序列结尾.
- contains: 判断集合是否包含某个指定的数据.
- containsSlice: 判断集合是否包含某个指定的子序列.
需求
- 定义Seq集合s1, 存储1到10这十个数据.
- 判断s1集合是否以子序列(1, 2)开头, 并打印结果.
- 判断s1集合是否以子序列(1, 3)开头, 并打印结果.
- 判断s1集合是否以子序列(9, 10)结尾, 并打印结果.
- 判断s1集合是否以子序列(8, 9)结尾, 并打印结果.
- 判断s1集合是否包含元素3, 并打印结果.
- 判断s1集合是否包含子序列Seq(1, 2), 并打印结果.
- 判断s1集合是否包含子序列Seq(1, 3), 并打印结果.
参考代码
//案例: 判断集合是否包含指定数据
object ClassDemo04 {
def main(args: Array[String]): Unit = {
//1. 定义Seq集合s1, 存储1到10这十个数据.
val s1 = (1 to 10).toSeq
//2. 判断s1集合是否以子序列(1, 2)开头, 并打印结果.
println(s1.startsWith(Seq(1,2)))
//3. 判断s1集合是否以子序列(1, 3)开头, 并打印结果.
println(s1.startsWith(Seq(1,3)))
//4. 判断s1集合是否以子序列(9, 10)结尾, 并打印结果.
println(s1.endsWith(Seq(9,10)))
//5. 判断s1集合是否以子序列(8, 9)结尾, 并打印结果.
println(s1.endsWith(Seq(8,9)))
println("-" * 15)
//6. 判断s1集合是否包含元素3, 并打印结果.
println(s1.contains(3))
//7. 判断s1集合是否包含子序列Seq(1, 2), 并打印结果.
println(s1.containsSlice(Seq(1, 2)))
//8. 判断s1集合是否包含子序列Seq(1, 3), 并打印结果.
println(s1.containsSlice(Seq(1, 3)))
}
}
2.7 案例五: 修改指定的元素
如果我们想修改某个元素值, 或者用指定的子序列来替代集合中的某一段元素
, 就可以通过updated(), patch()
方法来实现了, 具体如下:
-
updated: 修改指定索引位置的元素为指定的值.
-
patch: 修改指定区间的元素为指定的值.
需求
- 定义Seq集合s1, 存储1到5这五个数据.
- 修改索引2位置的元素值为: 10, 并打印结果.
- 从索引1开始, 用子序列Seq(10, 20)替换3个元素, 并打印结果.
参考代码
//案例: 修改指定的元素
object ClassDemo05 {
def main(args: Array[String]): Unit = {
//1. 定义Seq集合s1, 存储1到5这五个数据.
val s1 = (1 to 5).toSeq
//2. 修改索引2位置的元素值为: 10, 并打印结果.
val s2 = s1.updated(2, 10)
println(s2)
//3. 从索引1开始, 用子序列Seq(10, 20)替换3个元素, 并打印结果.
val s3 = s1.patch(1, Seq(10, 20), 3)
println(s3)
}
}
3. Stack
3.1 概述
表示栈
数据结构, 元素特点是先进后出
. 由于历史的原因, Scala当前的库中还包含一个immutable.Stack, 但当前已经被标记为弃用, 因为它的设计不怎么优雅, 而且性能也不太好, 因为栈会涉及到大量元素的进出, 所以不可变栈的应用场景还是比较小的, 最常用的还是可变栈, 例如: mutable.Stack, mutable.ArrayStack.
3.2 图解
把元素1, 2, 3添加到栈中, 图解如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ANXudj9X-1648463020578)(assets/1584792168888.png)]
3.3 常用方法
- top: 获取栈顶元素, 但是不会把这个元素从栈顶移除.
- push: 表示入栈操作, 相当于把元素压入栈顶
- pop: 移除栈顶元素, 并返回此元素.
- clear: 移除集合中所有的元素.
注意:
- immutable.Stack集合中有一个独有方法
pushAll()
, 把多个元素压入栈中.- mutable.ArrayStack集合独有的方法为:
- dup(duplicate缩写): 可以用来复制栈顶元素.
- preserving: 该方法会执行一个表达式, 在表达式执行完毕后恢复栈, 即: 栈的内容和调用前一致.
3.4 示例一: 演示Stack可变栈
需求
- 定义可变栈Stack, 存储1到5这5个数字.
- 通过top()方法获取栈顶元素, 并打印.
- 通过push()方法把元素6添加到栈顶位置, 并打印.
- 通过pushAll()往栈顶位置添加Seq(11, 22, 33)序列, 并打印.
- 通过pop()方法移除栈顶元素, 并打印.
- 通过clear()方法移除集合内所有的元素.
参考代码
import scala.collection.mutable
//案例: 演示Stack可变栈
object ClassDemo06 {
def main(args: Array[String]): Unit = {
//1. 定义可变栈Stack, 存储1到5这5个数字.
val s1 = mutable.Stack(1, 2, 3, 4, 5)
//2. 通过top()方法获取栈顶元素, 并打印.
println(s1.top)
println(s1.size)
println("-" * 15)
//3. 通过push()方法把元素6添加到栈顶位置, 并打印.
s1.push(6)
println(s1)
println("-" * 15)
//4. 通过pushAll()往栈顶位置添加Seq(11, 22, 33)序列, 并打印.
s1.pushAll(Seq(11, 22, 33))
println(s1)
println("-" * 15)
//5. 通过top()方法移除栈顶元素, 并打印.
println(s1.pop())
println(s1.size)
//6. 通过clear()方法移除集合内所有的元素.
s1.clear()
println(s1)
}
}
3.5 案例二: 演示ArrayStack可变栈
需求
- 定义可变栈ArrayStack, 存储1到5这5个数字.
- 通过dup()方法复制栈顶元素, 并打印结果.
- 通过preserving()方法实现
先清空集合元素, 然后恢复集合中清除的数据
, 并打印.
参考代码
import scala.collection.mutable
//案例: 演示ArrayStack可变栈
object ClassDemo07 {
def main(args: Array[String]): Unit = {
//1. 定义可变栈ArrayStack, 存储1到5这5个数字.
val s1 = mutable.ArrayStack(1, 2, 3, 4, 5)
//2. 通过dup()方法复制栈顶元素, 并打印结果.
s1.dup()
println(s1)
println("-" * 15)
//3. 通过preserving()方法实现`先清空集合元素, 然后恢复集合中清除的数据`, 并打印.
s1.preserving({s1.clear(); println("看看我执行了吗!")})
println(s1)
}
}
4. Queue
4.1 概述
表示队列, 元素特点是先进先出
, 我们常用的队列是可变队列: mutable.Queue
, 它内部是以MutableList
数据结构实现的.
4.2 图解
把元素1, 2, 3添加到队列中, 图解如下:
4.3 常用方法
- enqueue: 入队列方法, 可以传入零到多个元素.
- dequeue: 出队列, 移除一个元素.
- dequeueAll: 移除所有满足条件的元素.
- dequeueFirst: 移除第一个满足条件的元素
4.4 案例演示
需求
- 定义可变队列Queue, 存储1到5这五个数据.
- 往队列中添加元素6, 并打印.
- 往队列中添加元素7, 8, 9, 并打印.
- 移除队列的第一个元素, 并打印该元素.
- 移除队列的第一个奇数, 并打印该元素.
- 移除队列中所有的偶数, 并打印所有被移除的数据.
- 打印可变队列Queue, 查看最终结果.
参考代码
import scala.collection.mutable
//案例: 演示Queue序列.
object ClassDemo08 {
def main(args: Array[String]): Unit = {
//1. 定义可变队列Queue, 存储1到5这五个数据.
val q1 = mutable.Queue(1, 2, 3, 4, 5)
//2. 往队列中添加元素6, 并打印.
q1.enqueue(6)
//3. 往队列中添加元素7, 8, 9, 并打印.
q1.enqueue(7, 8, 9)
println(q1)
println("-" * 15)
//4. 移除队列的第一个元素, 并打印该元素.
println(q1.dequeue())
//5. 移除队列的第一个奇数, 并打印该元素.
println(q1.dequeueFirst(_ % 2 != 0))
//6. 移除队列中所有的偶数, 并打印所有被移除的数据.
println( q1.dequeueAll(_ % 2 == 0))
//7. 打印可变队列Queue, 查看最终结果.
println(q1)
}
}
5. Set
5.1 概述
Set集合中的元素不包含重复的元素, 常用子类有: SortedSet(子类是TreeSet), HashSet, , ListSet.
5.2 分类
继承关系如下图:
- HashSet: 是以前缀树(也叫: 字典树)的形式实现的, 元素特点是
唯一, 无序
, 经常用作统计. - LinkedHashSet: 元素特点是
唯一, 有序
. - TreeSet: 元素特点是
唯一, 排序
.
5.3 示例
需求
- 创建SortedSet集合, 存储元素1, 4, 3, 2, 5, 然后打印该集合.
- 创建HashSet集合, 存储元素1, 4, 3, 2, 5, 然后打印该集合.
- 创建LinkedHashSet集合, , 存储元素1, 4, 3, 2, 5, 然后打印该集合.
参考代码
import scala.collection.{SortedSet, mutable}
//案例: 演示Set集合.
object ClassDemo09 {
def main(args: Array[String]): Unit = {
//1. 创建SortedSet集合, 存储元素1, 4, 3, 2, 5, 然后打印该集合.
val s1 = SortedSet(1, 4, 3, 2, 5)
println(s1)
println("-" * 15)
//2. 创建HashSet集合, 存储元素1, 4, 3, 2, 5, 然后打印该集合.
val s2 = mutable.HashSet(1, 4, 3, 2, 5)
println(s2)
println("-" * 15)
//3. 创建LinkedHashSet集合, , 存储元素1, 4, 3, 2, 5, 然后打印该集合.
val s3 = mutable.LinkedHashSet(1, 4, 3, 2, 5)
println(s3)
}
}
6. Map
6.1 概述
Map表示映射, 它是包含键值对(key-value)的集合, Map类型的基本操作类似于Set集合的操作, 由于它包含的元素Entry是键值对, 所以Map提供了一些单独针对键或者值操作的方法.
6.2 分类
继承关系如下图:
6.3 示例
需求
-
定义Map集合, 存储数据为: “A” -> 1, “B” -> 2, “C” -> 3.
-
遍历Map集合.
-
通过filterKeys()方法, 获取出键为"B"的这组键值对对象, 并打印结果.
filterKeys: 针对键进行筛选, 根据键获取满足条件的键值对元素.
参考代码
//案例: 演示Map集合.
object ClassDemo10 {
def main(args: Array[String]): Unit = {
//1. 定义Map集合, 存储数据为: "A" -> 1, "B" -> 2, "C" -> 3.
val m1 = Map("A" -> 1, "B" -> 2, "C" -> 3)
//2. 遍历Map集合.
m1.foreach(println(_))
println("-" * 15)
//3. 通过filterKeys()方法, 获取出键为"B"的这组键值对对象, 并打印结果.
println(m1.filterKeys(_ == "B"))
}
}
7. 案例: 统计字符个数
7.1 需求
- 提示用户录入字符串, 并接收.
- 统计上述字符串中每个字符出现的次数, 并将结果打印到控制台上.
7.2 目的
综合考察集合, 键盘录入
相关知识点.
7.3 步骤
- 提示用户录入字符串, 并接收.
- 定义Map集合, 用来存储字符及其出现的次数. 键:字符, 值: 字符出现的次数.
- 将字符串转成字符数组.
- 遍历字符数组, 获取到每一个字符.
- 如果字符是第一次出现, 就将其次数记录为1, 如果字符是重复出现, 就将其次数+1, 然后重新存储.
- 遍历集合, 查看结果.
7.4 参考代码
import scala.collection.mutable
import scala.io.StdIn
//案例: 统计字符个数.
object ClassDemo11 {
def main(args: Array[String]): Unit = {
//1. 提示用户录入字符串, 并接收.
println("请录入一个字符串: ")
val s = StdIn.readLine()
//2. 定义Map集合, 用来存储字符及其出现的次数. 键:字符, 值: 字符出现的次数.
val m1 = mutable.Map[Char,Int]()
//3. 将字符串转成字符数组.
val chs = s.toCharArray
//4. 遍历字符数组, 获取到每一个字符.
for(k <- chs) {
//5. 如果字符是第一次出现, 就将其次数记录为1, 如果字符是重复出现, 就将其次数+1, 然后重新存储.
m1 += (k -> (if(m1.contains(k)) m1.getOrElse(k, 1) + 1 else 1))
}
//6. 遍历集合, 查看结果.
for((k, v) <- m1) {
println(k, v)
}
}
}
Scala第十九章节
章节目标
- 了解Actor的相关概述
- 掌握Actor发送和接收消息
- 掌握WordCount案例
1. Actor介绍
Scala中的Actor并发编程模型可以用来开发比Java线程效率更高的并发程序。我们学习Scala Actor的目的主要是为后续学习Akka做准备。
1.1 Java并发编程的问题
在Java并发编程中,每个对象都有一个逻辑监视器(monitor),可以用来控制对象的多线程访问。我们添加sychronized关键字来标记,需要进行同步加锁访问。这样,通过加锁的机制来确保同一时间只有一个线程访问共享数据。但这种方式存在资源争夺、以及死锁问题,程序越大问题越麻烦。
线程死锁
1.2 Actor并发编程模型
Actor并发编程模型,是Scala提供给程序员的一种与Java并发编程完全不一样的并发编程模型,是一种基于事件模型的并发机制。Actor并发编程模型是一种不共享数据,依赖消息传递的一种并发编程模式,有效避免资源争夺、死锁等情况。
1.3 Java并发编程对比Actor并发编程
Java内置线程模型 | Scala Actor模型 |
---|---|
"共享数据-锁"模型 (share data and lock) | share nothing |
每个object有一个monitor,监视线程对共享数据的访问 | 不共享数据,Actor之间通过Message通讯 |
加锁代码使用synchronized标识 | |
死锁问题 | |
每个线程内部是顺序执行的 | 每个Actor内部是顺序执行的 |
注意:
scala在2.11.x版本中加入了Akka并发编程框架,老版本已经废弃。
Actor的编程模型和Akka很像,我们这里学习Actor的目的是为学习Akka做准备。
2. 创建Actor
我们可以通过类(class)或者单例对象(object), 继承Actor特质的方式, 来创建Actor对象.
2.1 步骤
- 定义class或object继承Actor特质
- 重写act方法
- 调用Actor的start方法执行Actor
注意: 每个Actor是并行执行的, 互不干扰.
2.2 案例一: 通过class实现
需求
- 创建两个Actor,一个Actor打印1-10,另一个Actor打印11-20
- 使用class继承Actor实现.(如果需要在程序中创建多个相同的Actor)
参考代码
import scala.actors.Actor
//案例:Actor并发编程入门, 通过class创建Actor
object ClassDemo01 {
//需求: 创建两个Actor,一个Actor打印1-10,另一个Actor打印11-20
//1. 创建Actor1, 用来打印1~10的数字.
class Actor1 extends Actor {
override def act(): Unit = for (i <- 1 to 10) println("actor1: " + i)
}
//2. 创建Actor2, 用来打印11~20的数字.
class Actor2 extends Actor {
override def act(): Unit = for (i <- 11 to 20) println("actor2: " + i)
}
def main(args: Array[String]): Unit = {
//3. 启动两个Actor.
new Actor1().start()
new Actor2().start()
}
}
2.3 案例二: 通过object实现
需求
- 创建两个Actor,一个Actor打印1-10,另一个Actor打印11-20
- 使用object继承Actor实现.(如果在程序中只创建一个Actor)
参考代码
import scala.actors.Actor
//案例:Actor并发编程入门, 通过object创建Actor
object ClassDemo02 {
//需求: 创建两个Actor,一个Actor打印1-10,另一个Actor打印11-20
//1. 创建Actor1, 用来打印1~10的数字.
object Actor1 extends Actor {
override def act(): Unit = for (i <- 1 to 10) println("actor1: " + i)
}
//2. 创建Actor2, 用来打印11~20的数字.
object Actor2 extends Actor {
override def act(): Unit = for (i <- 11 to 20) println("actor2: " + i)
}
def main(args: Array[String]): Unit = {
//3. 启动两个Actor.
Actor1.start()
Actor2.start()
}
}
2.4 Actor程序运行流程
- 调用start()方法启动Actor
- 自动执行act()方法
- 向Actor发送消息
- act方法执行完成后,程序会调用**exit()**方法结束程序执行.
3. 发送消息/接收消息
我们之前介绍Actor的时候,说过Actor是基于事件(消息)的并发编程模型,那么Actor是如何发送消息和接收消息的呢?
3.1 使用方式
3.1.1 发送消息
我们可以使用三种方式来发送消息:
! | 发送异步消息,没有返回值 |
---|---|
!? | 发送同步消息,等待返回值 |
!! | 发送异步消息,返回值是Future[Any] |
例如:要给actor1发送一个异步字符串消息,使用以下代码:
actor1 ! "你好!"
3.1.2 接收消息
Actor中使用receive方法
来接收消息,需要给receive方法传入一个偏函数
{
case 变量名1:消息类型1 => 业务处理1
case 变量名2:消息类型2 => 业务处理2
...
}
注意: receive方法只接收一次消息,接收完后继续执行act方法
3.2 案例一: 发送及接收一句话
需求
- 创建两个Actor(ActorSender、ActorReceiver)
- ActorSender发送一个异步字符串消息给ActorReceiver
- ActorReceiver接收到该消息后,打印出来
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1bauLY8Q-1648463336117)(D:/%E6%95%99%E6%A1%88/%E5%A4%A7%E6%95%B0%E6%8D%AE%E5%B0%B1%E4%B8%9A%E7%8F%AD/%E5%8C%97%E4%BA%AC31~40%E6%9C%9F/%E5%8C%97%E4%BA%AC34%E6%9C%9F/Day03_Scala%E7%AC%AC%E4%B8%89%E5%A4%A9/%E8%AE%B2%E4%B9%89/%E5%90%88%E5%B9%B6%E7%89%88/assets/1552791021244.png)]
参考代码
//案例: 采用 异步无返回的形式, 发送消息.
object ClassDemo03 {
//1. 创建发送消息的Actor, ActorSender, 发送一句话给ActorReceiver
object ActorSender extends Actor {
override def act(): Unit = {
//发送一句话给ActorReceiver
ActorReceiver ! "你好啊, 我是ActorSender!"
//发送第二句话
ActorReceiver ! "你叫什么名字呀? "
}
}
//2. 创建接收消息的Actor, ActorReceiver
object ActorReceiver extends Actor {
override def act(): Unit = {
//接收发送过来的消息.
receive {
case x: String => println(x)
}
}
}
def main(args: Array[String]): Unit = {
//3. 启动两个Actor
ActorSender.start()
ActorReceiver.start()
}
}
3.3 案例二: 持续发送和接收消息
如果我们想实现ActorSender一直发送消息, ActorReceiver能够一直接收消息
,该怎么实现呢?
答: 我们只需要使用一个while(true)循环,不停地调用receive来接收消息就可以啦。
需求
- 创建两个Actor(ActorSender、ActorReceiver)
- ActorSender持续发送一个异步字符串消息给ActorReceiver
- ActorReceiver持续接收消息,并打印出来
参考代码
//案例:Actor 持续发送和接收消息.
object ClassDemo04 {
//1. 创建发送消息的Actor, ActorSender, 发送一句话给ActorReceiver
object ActorSender extends Actor {
override def act(): Unit = {
while(true) {
//发送一句话给ActorReceiver
ActorReceiver ! "你好啊, 我是ActorSender!"
//休眠3秒.
TimeUnit.SECONDS.sleep(3) //单位是: 秒
}
}
}
//2. 创建接收消息的Actor, ActorReceiver
object ActorReceiver extends Actor {
override def act(): Unit = {
//接收发送过来的消息, 持续接收.
while(true) {
receive {
case x: String => println(x)
}
}
}
}
def main(args: Array[String]): Unit = {
//3. 启动两个Actor
ActorSender.start()
ActorReceiver.start()
}
}
3.4 案例三: 优化持续接收消息
上述代码,是用while循环来不断接收消息的, 这样做可能会遇到如下问题:
- 如果当前Actor没有接收到消息,线程就会处于阻塞状态
- 如果有很多的Actor,就有可能会导致很多线程都是处于阻塞状态
- 每次有新的消息来时,重新创建线程来处理
- 频繁的线程创建、销毁和切换,会影响运行效率
针对上述情况, 我们可以使用loop(), 结合react()
来复用线程, 这种方式比while循环 + receive()
更高效.
需求
- 创建两个Actor(ActorSender、ActorReceiver)
- ActorSender持续发送一个异步字符串消息给ActorReceiver
- ActorReceiver持续接收消息,并打印出来
注意: 使用loop + react重写上述案例.
参考代码
//案例: 使用loop + react循环接收消息.
object ClassDemo05 {
//1. 创建发送消息的Actor, ActorSender, 发送一句话给ActorReceiver
object ActorSender extends Actor {
override def act(): Unit = {
while(true) {
//发送一句话给ActorReceiver
ActorReceiver ! "你好啊, 我是ActorSender!"
//休眠3秒.
TimeUnit.SECONDS.sleep(3) //单位是: 秒
}
}
}
//2. 创建接收消息的Actor, ActorReceiver
object ActorReceiver extends Actor {
override def act(): Unit = {
//接收发送过来的消息, 持续接收.
loop{
react {
case x: String => println(x)
}
}
}
}
def main(args: Array[String]): Unit = {
//3. 启动两个Actor
ActorSender.start()
ActorReceiver.start()
}
}
3.5 案例四: 发送和接收自定义消息
我们前面发送的消息都是字符串类型,Actor中也支持发送自定义消息,例如:使用样例类封装消息,然后进行发送处理。
3.5.1 示例一: 发送同步有返回消息
需求
- 创建一个MsgActor,并向它发送一个同步消息,该消息包含两个字段(id、message)
- MsgActor回复一个消息,该消息包含两个字段(message、name)
- 打印回复消息
注意:
- 使用
!?
来发送同步消息- 在Actor的act方法中,可以使用sender获取发送者的Actor引用
参考代码
//案例: Actor发送和接收自定义消息, 采用 同步有返回的形式
object ClassDemo06 {
//1. 定义两个样例类Message(表示发送数据), ReplyMessage(表示返回数据.)
case class Message(id: Int, message: String) //自定义的发送消息 样例类
case class ReplyMessage(message: String, name: String) //自定义的接收消息 样例类
//2. 创建一个MsgActor,用来接收MainActor发送过来的消息, 并向它回复一条消息.
object MsgActor extends Actor {
override def act(): Unit = {
//2.1 接收 主Actor(MainActor) 发送过来的消息.
loop {
react {
//结合偏函数使用
case Message(id, message) => println(s"我是MsgActor, 我收到的消息是: ${id}, ${message}")
//2.2 给MainActor回复一条消息.
//sender: 获取消息发送方的Actor对象
sender ! ReplyMessage("我很不好, 熏死了!...", "车磊")
}
}
}
}
def main(args: Array[String]): Unit = {
//3. 开启MsgActor
MsgActor.start()
//4. 通过MainActor, 给MsgActor发送一个 Message对象.
//采用 !? 同步有返回.
val reply:Any = MsgActor !? Message(1, "你好啊, 我是MainActor, 我在给你发消息!")
//resutl表示最终接收到的 返回消息.
val result = reply.asInstanceOf[ReplyMessage]
//5. 输出结果.
println(result)
}
}
3.5.2 示例二: 发送异步无返回消息
需求
创建一个MsgActor,并向它发送一个异步无返回消息,该消息包含两个字段(id, message)
注意: 使用
!
发送异步无返回消息
参考代码
//案例: Actor发送和接收自定义消息, 采用 异步 无返回的形式
object ClassDemo07 {
//1. 定义一个样例类Message(表示发送数据)
case class Message(id: Int, message: String) //自定义的发送消息 样例类
//2. 创建一个MsgActor,用来接收MainActor发送过来的消息, 并打印.
object MsgActor extends Actor {
override def act(): Unit = {
//2.1 接收 主Actor(MainActor) 发送过来的消息.
loop {
react {
//结合偏函数使用
case Message(id, message) => println(s"我是MsgActor, 我收到的消息是: ${id}, ${message}")
}
}
}
}
def main(args: Array[String]): Unit = {
//3. 开启MsgActor
MsgActor.start()
//4. 通过MainActor, 给MsgActor发送一个 Message对象.
//采用 ! 异步无返回
MsgActor ! Message(1, "我是采用 异步无返回 的形式发送消息!")
}
}
3.5.3 示例三: 发送异步有返回消息
需求
- 创建一个MsgActor,并向它发送一个异步有返回消息,该消息包含两个字段(id、message)
- MsgActor回复一个消息,该消息包含两个字段(message、name)
- 打印回复消息
注意:
- 使用
!!
发送异步有返回消息- 发送后,返回类型为Future[Any]的对象
- Future表示异步返回数据的封装,虽获取到Future的返回值,但不一定有值,可能在将来某一时刻才会返回消息
- Future的isSet()可检查是否已经收到返回消息,apply()方法可获取返回数据
图解
参考代码
//案例: Actor发送和接收自定义消息, 采用 异步有返回的形式
object ClassDemo08 {
//1. 定义两个样例类Message(表示发送数据), ReplyMessage(表示返回数据.)
case class Message(id: Int, message: String) //自定义的发送消息 样例类
case class ReplyMessage(message: String, name: String) //自定义的接收消息 样例类
//2. 创建一个MsgActor,用来接收MainActor发送过来的消息, 并向它回复一条消息.
object MsgActor extends Actor {
override def act(): Unit = {
//2.1 接收 主Actor(MainActor) 发送过来的消息.
loop {
react {
//结合偏函数使用
case Message(id, message) => println(s"我是MsgActor, 我收到的消息是: ${id}, ${message}")
//2.2 给MainActor回复一条消息.
//sender: 获取消息发送方的Actor对象
sender ! ReplyMessage("我很不好, 熏死了!...", "糖糖")
}
}
}
}
def main(args: Array[String]): Unit = {
//3. 开启MsgActor
MsgActor.start()
//4. 通过MainActor, 给MsgActor发送一个 Message对象.
//采用 !! 异步有返回.
val future: Future[Any] = MsgActor !! Message(1, "你好啊, 我是MainActor, 我在给你发消息!")
//5. 因为future中不一定会立马有数据, 所以我们要校验.
//Future的isSet()可检查是否已经收到返回消息,apply()方法可获取返回数据
//!future.isSet表示: 没有接收到具体的返回消息, 就一直死循环.
while(!future.isSet){}
//通过Future的apply()方法来获取返回的数据.
val result = future.apply().asInstanceOf[ReplyMessage]
//5. 输出结果.
println(result)
}
}
4. 案例: WordCount
4.1 需求
接下来,我们要使用Actor并发编程模型实现多文件的单词统计
。
案例介绍
给定几个文本文件(文本文件都是以空格分隔的),使用Actor并发编程来统计单词的数量.
思路分析
实现思路
- MainActor获取要进行单词统计的文件
- 根据文件数量创建对应的WordCountActor
- 将文件名封装为消息发送给WordCountActor
- WordCountActor接收消息,并统计单个文件的单词计数
- 将单词计数结果发送给MainActor
- MainActor等待所有的WordCountActor都已经成功返回消息,然后进行结果合并
4.2 步骤一: 获取文件列表
实现思路
-
在当前项目下的data文件夹下有: 1.txt, 2.txt两个文本文件, 具体存储内容如下:
1.txt文本文件存储内容如下:
hadoop sqoop hadoop hadoop hadoop flume hadoop hadoop hadoop spark
2.txt文本文件存储内容如下:
flink hadoop hive hadoop sqoop hadoop hadoop hadoop hadoop spark
-
获取上述两个文本文件的路径, 并将结果打印到控制台上.
参考代码
object MainActor {
def main(args: Array[String]): Unit = {
//1. 获取所有要统计的文件的路径.
//1.1 定义变量dir, 记录保存所有文件的: 文件夹路径. ./data/
var dir = "./data/"
//1.2 获取该文件夹下, 所有的文件名.
var fileNameList = new File(dir).list().toList //List("1.txt", "2.txt")
//1.3 对获取到的文件名进行封装, 获取其全路径. ./data/1.txt ./data/2.txt
var fileDirList = fileNameList.map(dir + _)
//println(fileDirList)
}
}
4.3 步骤二: 创建WordCountActor
实现思路
- 根据文件数量创建对应个数的WordCountActor对象.
- 为了方便后续发送消息给Actor,将每个Actor与文件名关联在一起
实现步骤
- 创建WordCountActor
- 将文件列表转换为WordCountActor
- 为了后续方便发送消息给Actor,将Actor列表和文件列表拉链到一起
- 打印测试
参考代码
-
WordCountActor.scala文件中的代码
//2.1 先创建WordCountActor类, 用来获取WordCountActor对象. //创建WordCountActor类, 每一个WordCountActor对象, 统计一个文件. class WordCountActor extends Actor { override def act(): Unit = { } }
-
MainActor.scala文件中的代码
object MainActor { def main(args: Array[String]): Unit = { //1. 获取所有要统计的文件的路径. //1.1 定义变量dir, 记录保存所有文件的: 文件夹路径. ./data/ var dir = "./data/" //1.2 获取该文件夹下, 所有的文件名. var fileNameList = new File(dir).list().toList //List("1.txt", "2.txt") //1.3 对获取到的文件名进行封装, 获取其全路径. ./data/1.txt ./data/2.txt var fileDirList = fileNameList.map(dir + _) //println(fileDirList) //2. 根据文件数量, 创建对应的WordCountActor对象. //2.1 先创建WordCountActor类, 用来获取WordCountActor对象. //2.2 根据文件数量, 创建对应的WordCountActor对象. val wordCountList = fileNameList.map(_ => new WordCountActor) //根据两个txt文件, 创建了两个wordCount对象. //println(wordCountList) //2.3 将WordCountActor和文件全路径关联起来 val actorWithFile = wordCountList.zip(fileDirList) //WordCountActor -> ./data/1.txt , WordCountActor -> ./data/2.txt println(actorWithFile) } }
4.4 步骤三: 启动Actor/发送/接收任务消息
实现思路
启动所有WordCountActor对象,并发送单词统计任务消息给每个WordCountActor对象.
注意: 此处应
发送异步有返回消息
实现步骤
- 创建一个WordCountTask样例类消息,封装要进行单词计数的文件名
- 启动所有WordCountActor,并发送异步有返回消息
- 获取到所有的WordCountActor中返回的消息(封装到一个Future列表中)
- 在WordCountActor中接收并打印消息
参考代码
-
MessagePackage.scala文件中的代码
/** * 表示: MainActor 给每一个WordCountActor发送任务的 格式. * @param fileName 具体的要统计的 文件路径. */ case class WordCountTask(fileName:String)
-
MainActor.scala文件中的代码
object MainActor { def main(args: Array[String]): Unit = { //1. 获取所有要统计的文件的路径. //1.1 定义变量dir, 记录保存所有文件的: 文件夹路径. ./data/ var dir = "./data/" //1.2 获取该文件夹下, 所有的文件名. var fileNameList = new File(dir).list().toList //List("1.txt", "2.txt") //1.3 对获取到的文件名进行封装, 获取其全路径. ./data/1.txt ./data/2.txt var fileDirList = fileNameList.map(dir + _) //println(fileDirList) //2. 根据文件数量, 创建对应的WordCountActor对象. //2.1 先创建WordCountActor类, 用来获取WordCountActor对象. //2.2 根据文件数量, 创建对应的WordCountActor对象. val wordCountList = fileNameList.map(_ => new WordCountActor) //根据两个txt文件, 创建了两个wordCount对象. //println(wordCountList) //2.3 将WordCountActor和文件全路径关联起来 val actorWithFile = wordCountList.zip(fileDirList) //WordCountActor -> ./data/1.txt , WordCountActor -> ./data/2.txt println(actorWithFile) //3. 启动WordCountActor, 并给每一个WordCountActor发送任务. /* Map(spark -> 1, hadoop -> 7, sqoop -> 1, flume -> 1) Map(sqoop -> 1, flink -> 1, hadoop -> 6, spark -> 1, hive -> 1) */ val futureList: List[Future[Any]] = actorWithFile.map { //futureList: 记录的是所有WordCountActor统计的结果. keyVal => //keyVal的格式: WordCountActor -> ./data/1.txt //3.1 获取具体的要启动的WordCountActor对象. val actor = keyVal._1 //actor: WordCountActor //3.2 启动具体的WordCountActor. actor.start() //3.3 给每个WordCountActor发送具体的任务(文件路径) 异步有返回. val future: Future[Any] = actor !! WordCountTask(keyVal._2) future //记录的是某一个WordCountActor返回的统计结果. } } }
-
WordCountActor.scala文件中的代码
//2.1 先创建WordCountActor类, 用来获取WordCountActor对象. //创建WordCountActor类, 每一个WordCountActor对象, 统计一个文件. class WordCountActor extends Actor { override def act(): Unit = { loop { react { //3.4 接收具体的任务 case WordCountTask(fileName) => //3.5 打印具体的任务 println(s"接收到的具体任务是: ${fileName}") } } } }
4.5 步骤四: 统计文件单词计数
实现思路
读取文件文本,并统计出来单词的数量。例如:
(hadoop, 3), (spark, 1)...
实现步骤
- 读取文件内容,并转换为列表
- 按照空格切割文本,并转换为一个一个的单词
- 为了方便进行计数,将单词转换为元组
- 按照单词进行分组,然后再进行聚合统计
- 打印聚合统计结果
参考代码
-
WordCountActor.scala文件中的代码
class WordCountActor extends Actor { override def act(): Unit = { //采用loop + react 方式接收数据. loop { react { //3.4 接收具体的任务 case WordCountTask(fileName) => //3.5 打印具体的任务 println(s"接收到的具体任务是: ${fileName}") //4. 统计接收到的文件中的每个单词的数量. //4.1 获取指定文件中的所有的文件. List("hadoop sqoop hadoop","hadoop hadoop flume") val lineList = Source.fromFile(fileName).getLines().toList //4.2 将上述获取到的数据, 转换成一个一个的字符串. //List("hadoop", "sqoop", "hadoop","hadoop", "hadoop", "flume") val strList = lineList.flatMap(_.split(" ")) //4.3 给每一个字符串后边都加上次数, 默认为1. //List("hadoop"->1, "sqoop"->1, "hadoop"->1, "hadoop"->1, "flume"->1) val wordAndCount = strList.map(_ -> 1) //4.4 按照 字符串内容分组. //"hadoop" -> List("hadoop"->1, "hadoop"->1), "sqoop" -> List("sqoop"->1) val groupMap = wordAndCount.groupBy(_._1) //4.5 对分组后的内容进行统计, 统计每个单词的总次数. "hadoop" -> 2, "sqoop" -> 1 val wordCountMap = groupMap.map(keyVal => keyVal._1 -> keyVal._2.map(_._2).sum) //4.6 打印统计后的结果. println(wordCountMap) } } } }
4.6 步骤五: 返回结果给MainActor
实现思路
- 将单词计数的结果封装为一个样例类消息,并发送给MainActor
- MainActor等待所有WordCountActor均已返回后,获取到每个WordCountActor单词计算后的结果
实现步骤
- 定义一个样例类封装单词计数结果
- 将单词计数结果发送给MainActor
- MainActor中检测所有WordCountActor是否均已返回,如果均已返回,则获取并转换结果
- 打印结果
参考代码
-
MessagePackage.scala文件中的代码
/** * 表示: MainActor 给每一个WordCountActor发送任务的 格式. * @param fileName 具体的要统计的 文件路径. */ case class WordCountTask(fileName:String) /** * 每个WordCountActor统计完的返回结果的: 格式 * @param wordCountMap 具体的返回结果, 例如: Map("hadoop"->6, "sqoop"->1) */ case class WordCountResult(wordCountMap:Map[String, Int])
-
WordCountActor.scala文件中的代码
class WordCountActor extends Actor { override def act(): Unit = { //采用loop + react 方式接收数据. loop { react { //3.4 接收具体的任务 case WordCountTask(fileName) => //3.5 打印具体的任务 println(s"接收到的具体任务是: ${fileName}") //4. 统计接收到的文件中的每个单词的数量. //4.1 获取指定文件中的所有的文件. List("hadoop sqoop hadoop","hadoop hadoop flume") val lineList = Source.fromFile(fileName).getLines().toList //4.2 将上述获取到的数据, 转换成一个一个的字符串. List("hadoop", "sqoop", "hadoop","hadoop", "hadoop", "flume") val strList = lineList.flatMap(_.split(" ")) //4.3 给每一个字符串后边都加上次数, 默认为1. List("hadoop"->1, "sqoop"->1, "hadoop"->1,"hadoop"->1, "hadoop"->1, "flume"->1) val wordAndCount = strList.map(_ -> 1) //4.4 按照 字符串内容分组. "hadoop" -> List("hadoop"->1, "hadoop"->1), "sqoop" -> List("sqoop"->1) val groupMap = wordAndCount.groupBy(_._1) //4.5 对分组后的内容进行统计, 统计每个单词的总次数. "hadoop" -> 2, "sqoop" -> 1 val wordCountMap = groupMap.map(keyVal => keyVal._1 -> keyVal._2.map(_._2).sum) //4.6 把统计后的结果返回给: MainActor. sender ! WordCountResult(wordCountMap) } } } }
4.7 步骤六: 结果合并
实现思路
对接收到的所有单词计数进行合并。
参考代码
-
MainActor.scala文件中的代码
object MainActor { def main(args: Array[String]): Unit = { //1. 获取所有要统计的文件的路径. //1.1 定义变量dir, 记录保存所有文件的: 文件夹路径. ./data/ var dir = "./data/" //1.2 获取该文件夹下, 所有的文件名. var fileNameList = new File(dir).list().toList //List("1.txt", "2.txt") //1.3 对获取到的文件名进行封装, 获取其全路径. ./data/1.txt ./data/2.txt var fileDirList = fileNameList.map(dir + _) //println(fileDirList) //2. 根据文件数量, 创建对应的WordCountActor对象. //2.1 先创建WordCountActor类, 用来获取WordCountActor对象. //2.2 根据文件数量, 创建对应的WordCountActor对象. val wordCountList = fileNameList.map(_ => new WordCountActor) //根据两个txt文件, 创建了两个wordCount对象. //println(wordCountList) //2.3 将WordCountActor和文件全路径关联起来 val actorWithFile = wordCountList.zip(fileDirList) //WordCountActor -> ./data/1.txt , WordCountActor -> ./data/2.txt println(actorWithFile) //3. 启动WordCountActor, 并给每一个WordCountActor发送任务. /* Map(spark -> 1, hadoop -> 7, sqoop -> 1, flume -> 1) Map(sqoop -> 1, flink -> 1, hadoop -> 6, spark -> 1, hive -> 1) */ val futureList: List[Future[Any]] = actorWithFile.map { //futureList: 记录的是所有WordCountActor统计的结果. keyVal => //keyVal的格式: WordCountActor -> ./data/1.txt //3.1 获取具体的要启动的WordCountActor对象. val actor = keyVal._1 //actor: WordCountActor //3.2 启动具体的WordCountActor. actor.start() //3.3 给每个WordCountActor发送具体的任务(文件路径) 异步有返回. val future: Future[Any] = actor !! WordCountTask(keyVal._2) future //记录的是某一个WordCountActor返回的统计结果. } //5. MainActor对接收到的数据进行合并. //5.1 判断所有的future都有返回值后, 再往下执行. // 过滤没有返回值的future 不为0说明还有future没有收到值 while(futureList.filter(!_.isSet).size != 0) {} //futureList: future1, future2 //5.2 从每一个future中获取数据. //wordCountMap: List(Map(spark -> 1, hadoop -> 7, sqoop -> 1, flume -> 1), Map(sqoop -> 1, flink -> 1, hadoop -> 6, spark -> 1, hive -> 1)) val wordCountMap = futureList.map(_.apply().asInstanceOf[WordCountResult].wordCountMap) //5.3 对获取的数据进行flatten, groupBy, map, 然后统计. val result = wordCountMap.flatten.groupBy(_._1).map(keyVal => keyVal._1 -> keyVal._2.map(_._2).sum) //5.4 打印结果 println(result) } }
Scala第二十章节
章节目标
- 理解Akka并发编程框架简介
- 掌握Akka入门案例
- 掌握Akka定时任务代码实现
- 掌握两个进程间通信的案例
- 掌握简易版spark通信框架案例
1. Akka并发编程框架简介
1.1 Akka概述
Akka是一个用于构建高并发、分布式和可扩展的基于事件驱动的应用工具包。Akka是使用scala开发的库,同时可以使用scala和Java语言来开发基于Akka的应用程序。
1.2 Akka特性
- 提供基于异步非阻塞、高性能的事件驱动编程模型
- 内置容错机制,允许Actor在出错时进行恢复或者重置操作
- 超级轻量级的事件处理(每GB堆内存几百万Actor)
- 使用Akka可以在单机上构建高并发程序,也可以在网络中构建分布式程序。
1.3 Akka通信过程
以下图片说明了Akka Actor的并发编程模型的基本流程:
- 学生创建一个ActorSystem
- 通过ActorSystem来创建一个ActorRef(老师的引用),并将消息发送给ActorRef
- ActorRef将消息发送给Message Dispatcher(消息分发器)
- Message Dispatcher将消息按照顺序保存到目标Actor的MailBox中
- Message Dispatcher将MailBox放到一个线程中
- MailBox按照顺序取出消息,最终将它递给TeacherActor接受的方法中
2. 创建Actor
Akka中,也是基于Actor来进行编程的。类似于之前学习过的Actor。但是Akka的Actor的编写、创建方法和之前有一些不一样。
2.1 API介绍
-
ActorSystem: 它负责创建和监督Actor
- 在Akka中,ActorSystem是一个重量级的结构,它需要分配多个线程.
- 在实际应用中, ActorSystem通常是一个单例对象, 可以使用它创建很多Actor.
- 直接使用
context.system
就可以获取到管理该Actor的ActorSystem的引用.
-
实现Actor类
- 定义类或者单例对象继承Actor(注意:要导入akka.actor包下的Actor)
- 实现receive方法,receive方法中直接处理消息即可,不需要添加loop和react方法调用. Akka会自动调用receive来接收消息.
- 【可选】还可以实现preStart()方法, 该方法在Actor对象构建后执行,在Actor生命周期中仅执行一次.
-
加载Actor
- 要创建Akka的Actor,必须要先获取创建一个ActorSystem。需要给ActorSystem指定一个名称,并可以去加载一些配置项(后面会使用到)
- 调用ActorSystem.actorOf(Props(Actor对象), “Actor名字”)来加载Actor.
2.2 Actor Path
每一个Actor都有一个Path,这个路径可以被外部引用。路径的格式如下:
Actor类型 | 路径 | 示例 |
---|---|---|
本地Actor | akka://actorSystem名称/user/Actor名称 | akka://SimpleAkkaDemo/user/senderActor |
远程Actor | akka.tcp://my-sys@ip地址:port/user/Actor名称 | akka.tcp://192.168.10.17:5678/user/service-b |
2.3 入门案例
2.3.1 需求
基于Akka创建两个Actor,Actor之间可以互相发送消息。
2.3.2 实现步骤
- 创建Maven模块
- 创建并加载Actor
- 发送/接收消息
2.3.3 创建Maven模块
使用Akka需要导入Akka库,这里我们使用Maven来管理项目, 具体步骤如下:
-
创建Maven模块.
选中项目, 右键 -> new -> Module -> Maven -> Next -> GroupId: com.itheima ArtifactId: akka-demo next -> 设置"module name"值为"akka-demo" -> finish
-
打开pom.xml文件,导入akka Maven依赖和插件.
//1. 直接把资料的pom.xml文件中的内容贴过来就行了. //2. 源码目录在: src/main/scala下 //3. 测试代码目录在: src/test/scala下. //4. 上述的这两个文件夹默认是不存在的, 需要我们手动创建. //5. 创建出来后, 记得要修改两个文件夹的类型. 选中文件夹, 右键 -> Mark Directory as -> Source Roots //存放源代码. Test Source Roots //存放测试代码.
2.3.4 创建并加载Actor
到这, 我们已经把Maven项目创建起来了, 后续我们都会采用Maven来管理我们的项目. 接下来, 我们来实现:
创建并加载Actor, 这里, 我们要创建两个Actor:
- SenderActor:用来发送消息
- ReceiverActor:用来接收,回复消息
具体步骤
-
在src/main/scala文件夹下创建包: com.itheima.akka.demo
-
在该包下创建两个Actor(注意: 用object修饰的单例对象).
-
SenderActor: 表示发送消息的Actor对象.
-
ReceiverActor: 表示接收消息的Actor对象.
-
-
在该包下创建
单例对象Entrance, 并封装main方法
, 表示整个程序的入口. -
把程序启动起来, 如果不报错, 说明代码是没有问题的.
参考代码
object SenderActor extends Actor {
/*
细节:
在Actor并发编程模型中, 需要实现act方法, 想要持续接收消息, 可通过loop + react实现.
在Akka编程模型中, 需要实现receive方法, 直接在receive方法中编写偏函数处理消息即可.
*/
//重写receive()方法
override def receive: Receive = {
case x => println(x)
}
}
object ReceiverActor extends Actor{
//重写receive()方法
override def receive: Receive = {
case x => println(x)
}
}
object Entrance {
def main(args:Array[String]) = {
//1. 实现一个Actor Trait, 其实就是创建两个Actor对象(上述步骤已经实现).
//2. 创建ActorSystem
//两个参数的意思分别是:ActorSystem的名字, 加载配置文件(此处先不设置)
val actorSystem = ActorSystem("actorSystem",ConfigFactory.load())
//3. 加载Actor
//actorOf方法的两个参数意思是: 1. 具体的Actor对象. 2.该Actor对象的名字
val senderActor = actorSystem.actorOf(Props(SenderActor), "senderActor")
val receiverActor = actorSystem.actorOf(Props(ReceiverActor), "receiverActor")
}
}
2.3.5 发送/接收消息
思路分析
- 使用样例类封装消息
- SubmitTaskMessage——提交任务消息
- SuccessSubmitTaskMessage——任务提交成功消息
- 使用
!
发送异步无返回消息.
参考代码
-
MessagePackage.scala文件中的代码
/** * 记录发送消息的 样例类. * @param msg 具体的要发送的信息. */ case class SubmitTaskMessage(msg:String) /** * 记录 回执信息的 样例类. * @param msg 具体的回执信息. */ case class SuccessSubmitTaskMessage(msg:String)
-
Entrance.scala文件中的代码
//程序主入口. object Entrance { def main(args: Array[String]): Unit = { //1. 创建ActorSystem, 用来管理所有用户自定义的Actor. val actorSystem = ActorSystem("actorSystem", ConfigFactory.load()) //2. 通过ActorSystem, 来管理我们自定义的Actor(SenderActor, ReceiverActor) val senderActor = actorSystem.actorOf(Props(SenderActor), "senderActor") val receiverActor = actorSystem.actorOf(Props(ReceiverActor), "receiverActor") //3. 由ActorSystem给 SenderActor发送一句话"start". senderActor ! "start" } }
-
SenderActor.scala文件中的代码
object SenderActor extends Actor{ override def receive: Receive = { //1. 接收Entrance发送过来的: start case "start" => { //2. 打印接收到的数据. println("SenderActor接收到: Entrance发送过来的 start 信息.") //3. 获取ReceiverActor的具体路径. //参数: 要获取的Actor的具体路径. //格式: akka://actorSystem的名字/user/要获取的Actor的名字. val receiverActor = context.actorSelection("akka://actorSystem/user/receiverActor") //4. 给ReceiverActor发送消息: 采用样例类SubmitTaskMessage receiverActor ! SubmitTaskMessage("我是SenderActor, 我在给你发消息!...") } //5. 接收ReceiverActor发送过来的回执信息. case SuccessSubmitTaskMessage(msg) => println(s"SenderActor接收到回执信息: ${msg} ") } }
-
ReceiverActor.scala文件中的代码
object ReceiverActor extends Actor { override def receive: Receive = { //1. 接收SenderActor发送过来的消息. case SubmitTaskMessage(msg) => { //2. 打印接收到的消息. println(s"ReceiverActor接收到: ${msg}") //3. 给出回执信息. sender ! SuccessSubmitTaskMessage("接收任务成功!. 我是ReceiverActor") } } }
输出结果
SenderActor接收到: Entrance发送过来的 start 信息.
ReceiverActor接收到: 我是SenderActor, 我在给你发消息!...
SenderActor接收到回执信息: 接收任务成功!. 我是ReceiverActor
3. Akka定时任务
需求: 如果我们想要使用Akka框架定时的执行一些任务,该如何处理呢?
答: 在Akka中,提供了一个scheduler对象来实现定时调度功能。使用ActorSystem.scheduler.schedule()方法
,就可以启动一个定时任务。
3.1 schedule()方法的格式
-
方式一: 采用
发送消息
的形式实现.def schedule( initialDelay: FiniteDuration, // 延迟多久后启动定时任务 interval: FiniteDuration, // 每隔多久执行一次 receiver: ActorRef, // 给哪个Actor发送消息 message: Any) // 要发送的消息 (implicit executor: ExecutionContext) // 隐式参数:需要手动导入
-
方式二: 采用
自定义方式
实现.def schedule( initialDelay: FiniteDuration, // 延迟多久后启动定时任务 interval: FiniteDuration // 每隔多久执行一次 )(f: ⇒ Unit) // 定期要执行的函数,可以将逻辑写在这里 (implicit executor: ExecutionContext) // 隐式参数:需要手动导入
注意: 不管使用上述的哪种方式实现定时器, 都需要
导入隐式转换和隐式参数
, 具体如下://导入隐式转换, 用来支持 定时器. import actorSystem.dispatcher //导入隐式参数, 用来给定时器设置默认参数. import scala.concurrent.duration._
3.2 案例
需求
- 定义一个ReceiverActor, 用来循环接收消息, 并打印接收到的内容.
- 创建一个ActorSystem, 用来管理所有用户自定义的Actor.
- 关联ActorSystem和ReceiverActor.
- 导入隐式转换和隐式参数.
- 通过定时器, 定时(间隔1秒)给ReceiverActor发送一句话.
- 方式一: 采用发送消息的形式实现.
- 方式二: 采用自定义方式实现.
参考代码
//案例: 演示Akka中的定时器.
object MainActor {
//1. 定义一个Actor, 用来循环接收消息, 并打印.
object ReceiverActor extends Actor {
override def receive: Receive = {
case x => println(x) //不管接收到的是什么, 都打印.
}
}
def main(args: Array[String]): Unit = {
//2. 创建一个ActorSystem, 用来管理所有用户自定义的Actor.
val actorSystem = ActorSystem("actorSystem", ConfigFactory.load())
//3. 关联ActorSystem和ReceiverActor.
val receiverActor = actorSystem.actorOf(Props(ReceiverActor), "receiverActor")
//4. 导入隐式转换和隐式参数.
//导入隐式转换, 用来支持 定时器.
import actorSystem.dispatcher
//导入隐式参数, 用来给定时器设置默认参数.
import scala.concurrent.duration._
//5. 通过定时器, 定时(间隔1秒)给ReceiverActor发送一句话.
//方式一: 通过定时器的第一种方式实现, 传入四个参数.
//actorSystem.scheduler.schedule(3.seconds, 2.seconds, receiverActor, "你好, 我是种哥, 我有种子你买吗?...")
//方式二: 通过定时器的第二种方式实现, 传入两个时间, 和一个函数.
//actorSystem.scheduler.schedule(0 seconds, 2 seconds)(receiverActor ! "新上的种子哟, 你没见过! 嘿嘿嘿...")
//实际开发写法
actorSystem.scheduler.schedule(0 seconds, 2 seconds){
receiverActor ! "新上的种子哟, 你没见过! 嘿嘿嘿..."
}
}
}
4. 实现两个进程之间的通信
4.1 案例介绍
基于Akka实现在两个进程间发送、接收消息。
- WorkerActor启动后去连接MasterActor,并发送消息给MasterActor.
- MasterActor接收到消息后,再回复消息给WorkerActor。
4.2 Worker实现
步骤
-
创建一个Maven模块,导入依赖和配置文件.
-
创建Maven模块.
GroupId: com.itheima
ArtifactID: akka-worker
-
把资料下的pom.xml文件中的内容复制到Maven项目akka-worker的pom.xml文件中
-
把资料下的application.conf复制到 src/main/resources文件夹下.
-
打开 application.conf配置文件, 修改端口号为: 9999
-
-
创建启动WorkerActor.
- 在src/main/scala文件夹下创建包: com.itheima.akka
- 在该包下创建 WorkerActor(单例对象的形式创建).
- 在该包下创建Entrance单例对象, 里边定义main方法
-
发送"setup"消息给WorkerActor,WorkerActor接收打印消息.
-
启动测试.
参考代码
-
WorkerActor.scala文件中的代码
//1. 创建WorkActor, 用来接收和发送消息. object WorkerActor extends Actor{ override def receive: Receive = { //2. 接收消息. case x => println(x) } }
-
Entrance.scala文件中的代码
//程序入口. //当前ActorSystem对象的路径 akka.tcp://actorSystem@127.0.0.1:9999 object Entrance { def main(args: Array[String]): Unit = { //1. 创建ActorSystem. val actorSystem = ActorSystem("actorSystem", ConfigFactory.load()) //2. 通过ActorSystem, 加载自定义的WorkActor. val workerActor = actorSystem.actorOf(Props(WorkerActor), "workerActor") //3. 给WorkActor发送一句话. workerActor ! "setup" } } //启动测试: 右键, 执行, 如果打印结果出现"setup", 说明程序执行没有问题.
4.3 Master实现
步骤
-
创建一个Maven模块,导入依赖和配置文件.
-
创建Maven模块.
GroupId: com.itheima
ArtifactID: akka-master
-
把资料下的pom.xml文件中的内容复制到Maven项目akka-master的pom.xml文件中
-
把资料下的application.conf复制到 src/main/resources文件夹下.
-
打开 application.conf配置文件, 修改端口号为: 8888
-
-
创建启动MasterActor.
- 在src/main/scala文件夹下创建包: com.itheima.akka
- 在该包下创建 MasterActor(单例对象的形式创建).
- 在该包下创建Entrance单例对象, 里边定义main方法
-
WorkerActor发送"connect"消息给MasterActor
-
MasterActor回复"success"消息给WorkerActor
-
WorkerActor接收并打印接收到的消息
-
启动Master、Worker测试
参考代码
-
MasterActor.scala文件中的代码
//MasterActor: 用来接收WorkerActor发送的数据, 并给其返回 回执信息. //负责管理MasterActor的ActorSystem的地址: akka.tcp://actorSystem@127.0.0.1:8888 object MasterActor extends Actor{ override def receive: Receive = { //1. 接收WorkerActor发送的数据 case "connect" => { println("MasterActor接收到: connect!...") //2. 给WorkerActor回执一句话. sender ! "success" } } }
-
Entrance.scala文件中的代码
//Master模块的主入口 object Entrance { def main(args: Array[String]): Unit = { //1. 创建ActorSystem, 用来管理用户所有的自定义Actor. val actorSystem = ActorSystem("actorSystem", ConfigFactory.load()) //2. 关联ActorSystem和MasterActor. val masterActor = actorSystem.actorOf(Props(MasterActor), "masterActor") //3. 给masterActor发送一句话: 测试数据, 用来测试. //masterActor ! "测试数据" } }
-
WorkerActor.scala文件中的代码(就修改了第3步)
//WorkerActor: 用来接收ActorSystem发送的消息, 并发送消息给MasterActor, 然后接收MasterActor的回执信息. //负责管理WorkerActor的ActorSystem的地址: akka.tcp://actorSystem@127.0.0.1:9999 object WorkerActor extends Actor{ override def receive: Receive = { //1. 接收Entrance发送过来的: setup. case "setup" => { println("WorkerActor接收到: Entrance发送过来的指令 setup!.") //2. 获取MasterActor的引用. val masterActor = context.system.actorSelection("akka.tcp://actorSystem@127.0.0.1:8888/user/masterActor") //3. 给MasterActor发送一句话. masterActor ! "connect" } //4. 接收MasterActor的回执信息. case "success" => println("WorkerActor接收到: success!") } }
5. 案例: 简易版spark通信框架
5.1 案例介绍
模拟Spark的Master与Worker通信.
- 一个Master
- 管理多个Worker
- 若干个Worker(Worker可以按需添加)
- 向Master发送注册信息
- 向Master定时发送心跳信息
5.2 实现思路
- 构建Master、Worker阶段
- 构建Master ActorSystem、Actor
- 构建Worker ActorSystem、Actor
- Worker注册阶段
- Worker进程向Master注册(将自己的ID、CPU核数、内存大小(M)发送给Master)
- Worker定时发送心跳阶段
- Worker定期向Master发送心跳消息
- Master定时心跳检测阶段
- Master定期检查Worker心跳,将一些超时的Worker移除,并对Worker按照内存进行倒序排序
- 多个Worker测试阶段
- 启动多个Worker,查看是否能够注册成功,并停止某个Worker查看是否能够正确移除
5.3 工程搭建
需求
本项目使用Maven搭建工程.
步骤
- 分别搭建以下几个项目, Group ID统一都为: com.itheima, 具体工程名如下:
工程名 | 说明 |
---|---|
spark-demo-common | 存放公共的消息、实体类 |
spark-demo-master | Akka Master节点 |
spark-demo-worker | Akka Worker节点 |
-
导入依赖(资料包中的pom.xml).
注意: master, worker要添加common依赖, 具体如下:
<!--导入spark-demo-common模块--> <dependency> <groupId>com.itheima</groupId> <artifactId>spark-demo-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
分别在三个项目下的src/main, src/test下, 创建scala目录.
-
导入配置文件(资料包中的application.conf)
- 修改Master的端口为7000
- 修改Worker的端口为8000
5.4 构建Master和Worker
需求
分别构建Master和Worker,并启动测试
步骤
- 创建并加载Master Actor
- 创建并加载Worker Actor
- 测试是否能够启动成功
参考代码
-
完成master模块中的代码, 即: 在src/main/scala下创建包: com.itheima.spark.master, 包中代码如下:
-
MasterActor.scala文件中的代码
//Master: 用来管理多个Worker的. //MasterActor的路径: akka.tcp://actorSystem@127.0.0.1:7000 object MasterActor extends Actor{ override def receive: Receive = { case x => println(x) } }
-
Master.scala文件中的代码
//程序入口: 相当于我们以前写的MainActor object Master { def main(args: Array[String]): Unit = { //1. 创建ActorSystem. val actorSystem = ActorSystem("actorSystem", ConfigFactory.load()) //2. 通过ActorSystem, 关联MasterActor. val masterActor = actorSystem.actorOf(Props(MasterActor), "masterActor") //3. 启动程序, 如果不报错, 说明代码没有问题. } }
-
-
完成worker模块中的代码, 即: 在src/main/scala下创建包: com.itheima.spark.worker, 包中代码如下:
-
WorkerActor.scala文件中的代码
//WorkerActor的地址: akka.tcp://actorSystem@127.0.0.1:7100 object WorkerActor extends Actor{ override def receive: Receive = { case x => println(x) } }
-
Worker.scala文件中的代码
//程序入口 object Worker { def main(args: Array[String]): Unit = { //1. 创建ActorSystem. val actorSystem = ActorSystem("actorSystem", ConfigFactory.load()) //2. 通过ActorSystem, 关联MasterActor. val workerActor = actorSystem.actorOf(Props(WorkerActor), "workerActor") //3. 启动程序, 如果不报错, 说明代码没有问题. workerActor ! "hello" } }
-
5.5 Worker注册阶段实现
需求
在Worker启动时,发送注册消息给Master.
思路分析
- Worker向Master发送注册消息(workerid、cpu核数、内存大小)
- 随机生成CPU核(1、2、3、4、6、8)
- 随机生成内存大小(512、1024、2048、4096)(单位M)
- Master保存Worker信息,并给Worker回复注册成功消息
- 启动测试
具体步骤
-
在spark-demo-common项目的src/main/scala文件夹下创建包: com.itheima.spark.commons
把资料下的MessagePackage.scala和Entities.scala这两个文件拷贝到commons包下.
-
在WorkerActor单例对象中定义一些成员变量, 分别表示:
- masterActorRef: 表示MasterActor的引用.
- workerid: 表示当前WorkerActor对象的id.
- cpu: 表示当前WorkerActor对象的CPU核数.
- mem: 表示当前WorkerActor对象的内存大小.
- cup_list: 表示当前WorkerActor对象的CPU核心数的取值范围.
- mem_list: 表示当前WorkerActor对象的内存大小的取值范围.
-
在WorkerActor的preStart()方法中, 封装注册信息, 并发送给MasterActor.
-
在MasterActor中接收WorkerActor提交的注册信息, 并保存到双列集合中…
-
MasterActor给WorkerActor发送回执信息(注册成功信息.).
-
在WorkerActor中接收MasterActor回复的 注册成功信息.
参考代码
-
WorkerActor.scala文件中的代码
//WorkerActor的地址: akka.tcp://actorSystem@127.0.0.1:7100 object WorkerActor extends Actor { //1 定义成员变量, 记录MasterActor的引用, 以及WorkerActor提交的注册参数信息. private var masterActorRef: ActorSelection = _ //表示MasterActor的引用. private var workerid:String = _ //表示WorkerActor的id private var cpu:Int = _ //表示WorkerActor的CPU核数 private var mem:Int = _ //表示WorkerActor的内存大小. private val cpu_list = List(1, 2, 3, 4, 6, 8) //CPU核心数的取值范围 private val mem_list = List(512, 1024, 2048, 4096) //内存大小取值范围 //2. 重写preStart()方法, 里边的内容: 在Actor启动之前就会执行. override def preStart(): Unit = { //3. 获取Master的引用. masterActorRef = context.actorSelection("akka.tcp://actorSystem@127.0.0.1:7000/usre/masterActor") //4. 构建注册消息. workerid = UUID.randomUUID().toString //设置workerActor的id val r = new Random() cpu = cpu_list(r.nextInt(cpu_list.length)) mem = mem_list(r.nextInt(mem_list.length)) //5. 将WorkerActor的提交信息封装成 WorkerRegisterMessage对象. var registerMessage = WorkerRegisterMessage(workerid, cpu, mem) //6. 发送消息给MasterActor. masterActorRef ! registerMessage } override def receive: Receive = { case x => println(x) } }
-
MasterActor.scala文件中的代码
//Master: 用来管理多个Worker的. //MasterActor的路径: akka.tcp://actorSystem@127.0.0.1:7000 object MasterActor extends Actor{ //1. 定义一个可变的Map集合, 用来保存注册成功好的Worker信息. private val regWorkerMap = collection.mutable.Map[String, WorkerInfo]() override def receive: Receive = { case WorkerRegisterMessage(workId, cpu, mem) => { //2. 打印接收到的注册信息 println(s"MasterActor: 接收到worker注册信息, ${workId}, ${cpu}, ${mem}") //3. 把注册成功后的保存信息保存到: workInfo中. regWorkerMap += workId -> WorkerInfo(workId, cpu, mem) //4. 回复一个注册成功的消息. sender ! RegisterSuccessMessage } } }
-
修改WorkerActor.scala文件中receive()方法的代码
override def receive: Receive = { case RegisterSuccessMessage => println("WorkerActor: 注册成功!") }
5.6 Worker定时发送心跳阶段
需求
Worker接收到Master返回的注册成功信息后,定时给Master发送心跳消息。而Master收到Worker发送的心跳消息后,需要更新对应Worker的最后心跳时间。
思路分析
- 编写工具类读取心跳发送时间间隔
- 创建心跳消息
- Worker接收到注册成功后,定时发送心跳消息
- Master收到心跳消息,更新Worker最后心跳时间
- 启动测试
具体步骤
-
在worker的src/main/resources文件夹下的 application.conf文件中添加一个配置.
worker.heartbeat.interval = 5 //配置worker发送心跳的周期(单位是 s)
-
在worker项目的com.itheima.spark.work包下创建一个新的单例对象: ConfigUtils, 用来读取配置文件信息.
-
在WorkerActor的receive()方法中, 定时给MasterActor发送心跳信息.
-
Master接收到心跳消息, 更新Worker最后心跳时间. .
参考代码
-
worker项目的ConfigUtils.scala文件中的代码
object ConfigUtils { //1. 获取配置信息对象. private val config = ConfigFactory.load() //2. 获取worker心跳的具体周期 val `worker.heartbeat.interval` = config.getInt("worker.heartbeat.interval") }
-
修改WorkerActor.scala文件的receive()方法中的代码
override def receive: Receive = { case RegisterSuccessMessage => { //1. 打印接收到的 注册成功消息 println("WorkerActor: 接收到注册成功消息!") //2. 导入时间单位隐式转换 和 隐式参数 import scala.concurrent.duration._ import context.dispatcher //3. 定时给Master发送心跳消息. context.system.scheduler.schedule(0 seconds, ConfigUtil.`worker.heartbeat.interval` seconds){ //3.1 采用自定义的消息的形式发送 心跳信息. masterActorRef ! WorkerHeartBeatMessage(workerId, cpu, mem) } } }
-
MasterActor.scala文件中的代码
object MasterActor extends Actor { //1. 定义一个可变的Map集合, 用来保存注册成功好的Worker信息. private val regWorkerMap = collection.mutable.Map[String, WorkerInfo]() override def receive: Receive = { //接收注册信息. case WorkerRegisterMessage(workId, cpu, mem) => { //2. 打印接收到的注册信息 println(s"MasterActor: 接收到worker注册信息, ${workId}, ${cpu}, ${mem}") //3. 把注册成功后的保存信息保存到: workInfo中. regWorkerMap += workId -> WorkerInfo(workId, cpu, mem, new Date().getTime) //4. 回复一个注册成功的消息. sender ! RegisterSuccessMessage } //接收心跳消息 case WorkerHeartBeatMessage(workId, cpu, mem) => { //1. 打印接收到的心跳消息. println(s"MasterActor: 接收到${workId}的心跳信息") //2. 更新指定Worker的最后一次心跳时间. regWorkerMap += workId -> WorkerInfo(workId, cpu, mem, new Date().getTime) //3. 为了测试代码逻辑是否OK, 我们可以打印下 regWorkerMap的信息 println(regWorkerMap) } } }
5.7 Master定时心跳检测阶段
需求
如果某个worker超过一段时间没有发送心跳,Master需要将该worker从当前的Worker集合中移除。可以通过Akka的定时任务,来实现心跳超时检查。
思路分析
- 编写工具类,读取检查心跳间隔时间间隔、超时时间
- 定时检查心跳,过滤出来大于超时时间的Worker
- 移除超时的Worker
- 对现有Worker按照内存进行降序排序,打印可用Worker
具体步骤
-
修改Master的application.conf配置文件, 添加两个配置
#配置检查Worker心跳的时间周期(单位: 秒)
master.check.heartbeat.interval = 6
#配置worker心跳超时的时间(秒)
master.check.heartbeat.timeout = 15 -
在Master项目的com.itheima.spark.master包下创建: ConfigUtils工具类(单例对象), 用来读取配置文件信息.
-
在MasterActor中开始检查心跳(即: 修改MasterActor#preStart中的代码.).
-
开启Master, 然后开启Worker, 进行测试.
参考代码
-
Master项目的ConfigUtils.scala文件中的代码
//针对Master的工具类. object ConfigUtil { //1. 获取到配置文件对象. private val config: Config = ConfigFactory.load() //2. 获取检查Worker心跳的时间周期(单位: 秒) val `master.check.heartbeat.interval` = config.getInt("master.check.heartbeat.interval") //3. 获取worker心跳超时的时间(秒) val `master.check.heartbeat.timeout` = config.getInt("master.check.heartbeat.timeout") }
-
MasterActor.scala文件的preStart()方法中的代码
//5. 定时检查worker的心跳信息 override def preStart(): Unit = { //5.1 导入时间转换隐式类型 和 定时任务隐式变量 import scala.concurrent.duration._ import context.dispatcher //5.2 启动定时任务. context.system.scheduler.schedule(0 seconds, ConfigUtil.`master.check.heartbeat.interval` seconds) { //5.3 过滤大于超时时间的Worker. val timeOutWorkerMap = regWorkerMap.filter { keyval => //5.3.1 获取最后一次心跳更新时间. val lastHeatBeatTime = keyval._2.lastHeartBeatTime //5.3.2 超时公式: 当前系统时间 - 最后一次心跳时间 > 超时时间(配置文件信息 * 1000) if (new Date().getTime - lastHeatBeatTime > ConfigUtil.`master.check.heartbeat.timeout` * 1000) true else false } //5.4 移除超时的Worker if(!timeOutWorkerMap.isEmpty) { //如果要被移除的Worker集合不为空, 则移除此 timeOutWorkerMap //注意: 双列集合是根据键移除元素的, 所以最后的 _._1是在获取键. regWorkerMap --= timeOutWorkerMap.map(_._1) } //5.5 对worker按照内存大小进行降序排序, 打印Worker //_._2 获取所有的WorkInfo对象. val workerList = regWorkerMap.map(_._2).toList //5.6 按照内存进行降序排序. val sortedWorkerList = workerList.sortBy(_.mem).reverse //5.7 打印结果 println("按照内存的大小降序排列的Worker列表: ") println(sortedWorkerList) } }
5.8 多个Worker测试阶段
需求
修改配置文件,启动多个worker进行测试。
大白话: 启动一个Worker, 就修改一次Worker项目下的application.conf文件中记录的端口号, 然后重新开启Worker即可.
步骤
- 测试启动新的Worker是否能够注册成功
- 停止Worker,测试是否能够从现有列表删除