Table of Contents
Scala 简介 Scala 集成了面向对象编程和函数式编程语言的各种特性。Scala 的静态类型帮助减少复杂系统中的 bug,并且支持 JVM 和 JavaScript 运行环境,同时还提供丰富的类库可供使用。
Scala 的官网地址为 https://www.scala-lang.org
安装与配置 Scala 运行环境依赖于 JVM,所以要先安装 Java。之后再安装 Scala,最后安装 sbt。
安装 Java 在 oracle 官网下载 JavaSE 1.5 以上的版本。下载地址如下:
http://www.oracle.com/technetwork/java/javase/downloads/index.html
这里我选择 1.8 版本,根据自己的操作系统选择合适的下载包。
设置环境变量的教程在这里:https://www.runoob.com/java/java-environment-setup.html
设置完成后,我们可以在终端查看 java
和 javac
命令:
1 2 3 4 5 6 $ java -version java version "1.8.0_181" Java(TM) SE Runtime Environment (build 1.8.0_181-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode) $ javac -version javac 1.8.0_181
安装 Scala Scala 的下载地址为 https://www.scala-lang.org/download/ 。可以根据自己的操作系统选择合适的下载包。
如果操作系统为 Windows,可以查找 msi 的安装文件。
如果操作系统为 Mac,可以使用 Homebrew 来安装:
1 2 brew update brew install scala
如果操作系统为 Ubuntu,直接用 apt-get install scala
来进行安装。
安装完成后,可以运行如下命令来查看 scala
命令:
1 2 $ scala -version Scala code runner version 2.13.0 -- Copyright 2002-2019, LAMP/EPFL and Lightbend, Inc.
安装 sbt sbt 的全称为 Simple Build Tool,用于管理 Scala 项目的依赖并进行构建,其地址就像 Maven 之于 Java。其下载地址为 https://www.scala-sbt.org/download.html 。
Hello World 我们演示三种输出 Hello World
的方式。
交互模式 在命令行输入 scala
命令,进入交互模式。
1 2 3 4 5 6 $ scala Welcome to Scala 2.13.0 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_181). Type in expressions for evaluation. Or try :help. scala> println("Hello World" ); Hello World
编译模式 创建一个文件 HelloWorldDemo.scala,文件内容如下:
1 2 3 4 5 object HelloWorldDemo { def main (args: Array [String ]):Unit = { println("Hello World" ); } }
运行编译及运行命令:
1 2 3 $ scalac HelloWorldDemo.scala $ scala HelloWorldDemo Hello World
构建模式 可以使用 sbt 来构建。如编译模式的文件为例,我们将其放到 helloworld 目录里。确保该目录只有一个文件。输入 sbt 命令,然后用 run 命令运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ sbt sbt:helloworld> run [info] Updating ... [info] Done updating. [info] Compiling 1 Scala source to /Users/didi/code/scala/helloworld/target/scala-2.12/classes ... [info] Non-compiled module 'compiler-bridge_2.12' for Scala 2.12.7. Compiling... [info] Compilation completed in 8.672s. [info] Done compiling. [info] Packaging /Users/didi/code/scala/helloworld/target/scala-2.12/helloworld_2.12-0.1.0-SNAPSHOT.jar ... [info] Done packaging. [info] Running HelloWorldDemo Hello World [success] Total time: 18 s, completed 2019-6-25 15:19:19 sbt:helloworld> run [info] Running HelloWorldDemo Hello World [success] Total time: 0 s, completed 2019-6-25 15:19:23
可以看到,第一次 run 时,需要进行编译,耗时 18 s。第二次 run 时,不需再编译即可运行,耗时可以忽略不计。
基本语法 基本概念
类:外部事物的抽象,具有方法和属性。
对象:对象是类的具体实例;类是对象的抽象。
方法:方法是类的某个行为动作。一个类可以包含多个方法。
属性:类的若干属性,又称为字段。
语句:语句是执行代码的单元。如果一行只有一个语句,那么结尾不需要加;
分号。如果一行之内有多个语句的话,需要加分号。
注释:支持多行注释 /* */
和单行注释 //
。
包:使用 package
定义包,类似于命名空间。
引用:使用 import
引用包。注意 Scala 的包引用可以出现在任何地方,而不只是文件的顶部。
标识符:标识符可以由字母和下划线开头,后面可以连接数字、字母和下划线。区分大小写。
类名:类名采用驼峰写法,每个单词首字母大写。
方法名:方法名称的第一个字母用小写,其余每个单词的第一个字母大写。
程序文件名:程序文件名称和对象名称要完全匹配。虽然新版本不再要求,但为了保证代码能通过旧版本编译器,还是保持一致较好。
程序入口:程序入口为 main 方法。
数据类型 Scala 的数据类型分为以下几类:
整数
类型
说明
Byte
8位有符号补码整数。数值区间为 -128 到 127
Short
16位有符号补码整数。数值区间为 -32768 到 32767
Int
32位有符号补码整数。数值区间为 -2147483648 到 2147483647
Long
64位有符号补码整数。数值区间为 -9223372036854775808 到 9223372036854775807
浮点数
类型
说明
Float
32 位, IEEE 754 标准的单精度浮点数
Double
64 位 IEEE 754 标准的双精度浮点数
字符
类型
说明
Char
16位无符号Unicode字符, 区间值为 U+0000 到 U+FFFF
String
字符序列
布尔
其它类型
类型
说明
Unit
表示无值,和其他语言中void等同。用作不返回任何结果的方法的结果类型。Unit只有一个实例值,写成()。
Null
null 或空引用
Nothing
Nothing类型在Scala的类层级的最低端;它是任何其他类型的子类型。
Any
Any是所有其他类的超类
AnyRef
AnyRef类是Scala里所有引用类(reference class)的基类
变量与常量 变量在程序运行过程中,其值可能发生改变,采用 var
声明变量;
常量在程序运行过程中,其值不会发生改变,采用 val
声明常量。
声明变量和常量的语法如下:
1 2 3 4 5 var VariableName : DataType [= Initial Value] 或 val VariableName : DataType [= Initial Value]
另外 DataType 是可选的。如果不指定数据类型,编译器会自动根据初始值推断出数据类型。
流程控制逻辑 Scala 的流程控制主要包括条件控制和循环控制。
条件控制 其使用方法和其它语言类似,示例如下:
1 2 3 4 5 6 7 8 9 if (布尔表达式 1 ){ }else if (布尔表达式 2 ){ }else if (布尔表达式 3 ){ }else { }
循环控制 循环支持三种循环类型:while 循环、do while 循环和 for 循环。
while 循环
1 2 3 4 while(condition) { //语句块 }
do while循环
1 2 3 do { //语句块 } while( condition );
for 循环
1 2 3 for( var x <- Range ){ //语句块 }
循环控制的 break 和 continue Scala 的 break 的语法和其它语言不同,它完全是基于类的一种实现。
文件:BreakDemo.scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import scala.util.control.Breaks ;object BreakDemo { def main (args: Array [String ]) { var i = 0 ; val numList = List (1 ,2 ,3 ,4 ); val loop = new Breaks ; loop.breakable { for ( i <- numList){ println( "第" + i + "次" ); if ( i == 3 ){ loop.break ; } } } println( "事不过三" ); } }
输出结果:
遗憾的是,Scala 里并没有 continue 的语法,即跳过本次循环。但想一下 continue 的实质是什么?跳过满足特定条件的循环中的元素,即过滤掉某些元素,让其不参与循环。Scala 提供了循环过滤的机制。
我们看一下示例。
文件:ForFilterDemo.scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.util.Random ;object ForFilterDemo { def main (args: Array [String ]) { var r = new Random () var rand = r.nextInt(100 ) var i = 0 for (i <- 0 to 10 if rand % 2 == 0 ){ println(rand) rand = r.nextInt(100 ) } } }
输出结果(每次都不相同):
我们循环 10 次,每次取一个 100 以内的随机数,要求只保留偶数,所有的奇数都过滤掉。实现上可以用 for + if 的循环过滤机制来实现。
类和对象 Scala 类的特征有如下几个:
一个例子 首先看一个类的示例。Point 类有两个属性 x 和 y,并有一个方法 move。
文件:PointDemo.scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.io._class Point (val xc: Int , val yc: Int ) { var x: Int = xc var y: Int = yc def move (dx: Int , dy: Int ) { x = x + dx y = y + dy println ("Point x location : " + x); println ("Point y location : " + y); } } object PointDemo { def main (args: Array [String ]) { val pt = new Point (10 , 20 ); pt.move(10 , 10 ); } }
运行命令如下:
1 2 3 4 > scalac PointDemo.scala > scala PointDemo Point x location : 20 Point y location : 30
在这个例子里,需要注意的是,如果文件中定义了不止一个类,则要使用 scalac
进行编译后再运行。
这个例子还可以学到构造函数和 singleton objects 的知识,我们分述如下。
构造函数 Scala 的构造函数分为主构造函数和辅助构造函数。
主构造函数 在上例中,定义 Point 类的语句如下:
1 class Point(val xc: Int, val yc: Int)
其中 xc
、yc
为构造函数的参数。那么其函数的定义在哪里呢?
在类的主体里,除了 def
定义的函数之外,其余部分都是主构造函数。
辅助构造函数 辅助构造函数用于创造不同类型、不同数目参数的构造函数。定义方法如下:
我们改造一下PointDemo.scala
文件,来演示主构造函数和辅助构造函数。
文件:PointDemo2.scala
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 import java.io._class Point (val xc: Int , val yc: Int ) { var x: Int = xc var y: Int = yc def this (){ this (0 ,0 ) println("Auxiliary constructor" ) } def move (dx: Int , dy: Int ) { x = x + dx y = y + dy println ("Point x location : " + x); println ("Point y location : " + y); } println("Main Constructor" ); } object PointDemo2 { def main (args: Array [String ]) { val pt1 = new Point (10 , 20 ); pt1.move(10 , 10 ); val pt2 = new Point (); pt2.move(10 , 10 ); } }
输出结果:
1 2 3 4 5 6 7 Main Constructor Point x location : 20 Point y location : 30 Main Constructor Auxiliary constructor Point x location : 10 Point y location : 10
我们看以下问题:
主构造函数和辅助构造函数有什么区别?
答案是没有区别。当我们实例化一个类时,编译器会自动匹配参数类型和参数个数相符的构造的函数。
能否将主构造函数和辅助构造函数的参数完全一致?
答案是不能。我们定义辅助构造函数如下:
1 2 3 4 5 def this (xc: Int , yc: Int ){ this (xc,yc) println("Auxiliary constructor" ) }
结果编译通不过。
singleton objects singleton object
有如下特征:
singleton object
的主构造函数不接收参数
singleton object
可以继承 class
和 traits
singleton object
始终存在 main
方法
不需要创建实例即可调用 singleton object
的方法
我们查看一下实例。
文件:SingletonDemo.scala
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 object Exampleofsingleton { var str1 = "Welcome ! MIS" ; var str2 = "This is Scala language tutorial" ; def display () { println("Called By Display Method" ) println(str1); println(str2); } } object SingletonDemo { def main (args: Array [String ]) { Exampleofsingleton .display(); println("Called By Property" ) println(Exampleofsingleton .str1); println(Exampleofsingleton .str2); } }
输出结果如下:
1 2 3 4 5 6 Called By Display Method Welcome ! MIS This is Scala language tutorial Called By Property Welcome ! MIS This is Scala language tutorial
伴生对象 Companion Object 我们经常会遇到这样的场景:定义一个类,实现常见的功能;再定义一个类来调用以完成特定的任务。这样带来命名问题,这有时也是让开发者头疼的一件事情。
Scala 的伴生对象完美解决了这个问题。我们先看下例子:
文件:SingletonDemo.scala
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 class CompanionDemo { var str1 = "MIS" ; var str2 = "Tutorial of Companion object" ; private var str3 = "Hello MIS" def show () { println(str1); println(str2); } private def mis () { println("In mis method" ) println(str3) } } object CompanionDemo { def main (args: Array [String ]) { var obj = new CompanionDemo (); obj.show(); println(obj.str3); obj.mis(); } }
输出结果:
1 2 3 4 5 MIS Tutorial of Companion object Hello MIS In mis method Hello MIS
可以看到同时存在 class CompanionDemo
和 object CompanionDemo
。
伴生对象的特点如下:
companion object
和 companion class
的名称必须完全一致
companion object
必须和 companion class
定义在同一个文件中
companion object
可以访问 companion class
的私有属性和私有方法
伴生对象除了解决命名问题外,还可以对外提供接口,方便调用。
继承 Scala 是单继承的语言,子类只有一个父类。
###抽象类和抽象方法
定义一个抽象类,需要使用 abstract
关键字。例如:
1 2 3 4 5 abstract class Animal (iname:String ){ def eat }
定义抽象方法,执行保持方法的函数体为空即可。
子类继承抽象父类,必须实现父类的所有的抽象方法。
override override
关键字有以下特点:
一个例子 我们看一个演示继承的例子。
文件:InheritDemo.scala
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 abstract class Animal (iname:String ){ val name:String = iname; def eat def move (){ println("All Animals Can Move" ); } def display (){ println("Hello " +name) } } class Dog (iname:String ) extends Animal (iname:String ) { def eat (){ println("Dog Eats Bones" ); } override def move (){ println("Dog is An Animal, So Dog can move" ); } def display (msg:String ){ println("Hello " +name+" " +msg) } } object InheritDemo { def main (args: Array [String ]) { val dog = new Dog ("Teddy" ); dog.eat(); dog.move(); dog.display("Welcome!" ) } }
输出结果:
1 2 3 Dog Eats Bones Dog is An Animal, So Dog can move Hello Teddy Welcome!
Dog
类的 display
方法为什么没有加 override
?
答案是 Dog
类的 display
方法的参数和 Animal
类的 display
方法不同,不属于重写,所以不需要加 override
。
模式匹配 Scala 使用 case
关键字实现多条件逻辑匹配的功能。每个条件是一个备选项,称为模式。每个备选项包含一个模式和一到多个表达式。语法如下:
我们看几个例子,学习模式匹配。
简单例子(整型数值匹配、温度判断) 文件:Temperature.scala
1 2 3 4 5 6 7 8 9 10 11 12 13 object Temperature { def main (args: Array [String ]) { println(display(37 )) println(display(100 )) println(display(50 )) } def display (x: Int ): String = x match { case 0 => "freezing point" case 100 => "boiling point" case x if (x >= 36.5 && x <= 37.5 ) => "body point" case _ => "other point" } }
输出结果:
1 2 3 body point boiling point other point
match 表达式通过以代码编写的先后次序尝试每个模式来完成计算,只要发现有一个匹配的case,剩下的case不会继续匹配。
case class 文件:CaseClassDemo.scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 object CaseClassDemo { def main (args: Array [String ]) { val alice = new Person ("Alice" , 25 ) val bob = new Person ("Bob" , 32 ) val charlie = new Person ("Charlie" , 32 ) for (person <- List (alice, bob, charlie)) { person match { case Person ("Alice" , 25 ) => println("Hi Alice!" ) case Person ("Bob" , 32 ) => println("Hi Bob!" ) case Person (name, age) => println("Age: " + age + " year, name: " + name + "?" ) } } } case class Person (name: String , age: Int ) }
输出结果:
1 2 3 Hi Alice! Hi Bob! Age: 32 year, name: Charlie?
在声明样例类时,下面的过程自动发生了:
构造器的每个参数都成为val,除非显式被声明为var,但是并不推荐这么做;
在伴生对象中提供了apply方法,所以可以不使用new关键字就可构建对象;
提供unapply方法使模式匹配可以工作;
生成toString、equals、hashCode和copy方法,除非显示给出这些方法的定义。
apply 将对象以函数的方式进行调用时,scala会隐式地将调用改为在该对象上调用apply方法。例如XXX(“hello”)实际调用的是XXX.apply(“hello”), 因此apply方法又被称为注入方法。apply方法常用于创建类实例的工厂方法。示例如下:
1 2 3 4 5 6 7 object Greeting { def apply (name: String ) = "Hello " + name } Greeting .apply(“Lucy ”) Greeting (“Lucy ”)
unapply 与 apply 相对的是 unapply 方法,它的用法与 apply 类似,但其作用是用来抽取部分参数,它也称为抽取方法,主要用于模式匹配时抽取某些参数 case XXX(str) => println(str)
并发编程 Akka 从 Scala 的 2.11.0 版本开始,Scala 的 Actors 库已经过时了。早在 Scala 2.10.0 的时候,默认的 actor 库即是 Akka。所以我们重点讲述 Akka。
Akka 是构建高并发、分布式、弹性消息驱动的 Java 和 Scala 应用的工具集合。官网地址是https://akka.io/ 。
Akka Hello World 示例代码我已经放到 github 上了,地址为 https://github.com/spetacular/scala-simple-lesson。
下载代码后,用命令行 cd akkademo
进入示例代码目录。
依次运行 stb
、compile
、run
,可以看到类似于如下输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ sbt [info] Loading project definition from /Users/didi/Documents/GitHub/scala-simple-lesson/akkademo/project [info] Loading settings for project akkademo from build.sbt ... [info] Set current project to akka-demo (in build file:/Users/didi/Documents/GitHub/scala-simple-lesson/akkademo/) [info] sbt server started at local:///Users/didi/.sbt/1.0/server/bc7cb24c19aa2ba1ff53/sock sbt:akka-demo> compile [info] Updating ... [info] Done updating. [info] Compiling 1 Scala source to /Users/didi/Documents/GitHub/scala-simple-lesson/akkademo/target/scala-2.12/classes ... [info] Done compiling. [success] Total time: 6 s, completed 2019-6-25 17:57:00 sbt:akka-demo> run [info] Packaging /Users/didi/Documents/GitHub/scala-simple-lesson/akkademo/target/scala-2.12/akka-demo_2.12-1.0.jar ... [info] Done packaging. [info] Running com.example.AkkaDemo fine thank you huh?
我们看下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1 package com.example;2 import akka.actor.{Actor ,ActorSystem ,Props }3 4 class HelloActor extends Actor {5 def receive = {6 case "how are you" => println("fine thank you" )7 case _ => println("huh?" )8 }9 }10 11 object AkkaDemo extends App {12 val system = ActorSystem ("HelloSystem" )13 14 val helloActor = system.actorOf(Props [HelloActor ], name = "helloactor" )15 helloActor ! "how are you" 16 helloActor ! "Bonjour" 17 }
我们先看下调用示例图:
第 4 至 9 行,定义了 HelloActor
的类。HelloActor
继承在 Actor
类,实现了 receive
方法。该方法根据传递过来的字符串,做出不同的动作。
第 11 行,AkkaDemo
继承了 App
类,作为main class
,是程序执行的入口。
第 12 行,定义了一个 ActorSystem
,这是所有 Actor
的容器。
第 14 行,定义了一个 Actor
。Actor
是一种类似于线程、goroutine的一个事物,是执行调度的一个单位。在创建 helloActor
的时候,并没有用 new
关键字来实例化,而是用 actorOf
来创建一个 Actor
的引用。actorOf
接收两个参数:配置类 Props
和名称。这种方式的优点是在分布式系统上,调用方不用关心实质的 Actor
在哪台机器上。
第 15、16行,将"how are you"
和"Bonjour"
的消息传递给 HelloActor
。
这个例子演示了 akka 的 AkkaSystem、Actor、消息传递是如何交互的。但这个例子,仍然是串行处理的。我们将在下面的例子中,演示并行编程。
Akka Quickstart with Scala 这个例子来自 Akka 官方示例,网址为 Akka Quickstart with Scala 。
下载 下载方法有两种:
在官网页面 Lightbend Tech Hub 点击 CREATE A PROJECT FOR ME
嫌速度慢可以下载 Scala Simple Lesson ,解压后 akka-quickstart-scala 目录即是示例代码。
配置及运行 解压后的代码,构建脚本可能没有运行权限,需要用 chmod
命令添加可运行权限。
1 2 3 $ cd akka-quickstart-scala$ chmod u+x ./sbt$ chmod u+x ./sbt-dist/bin/sbt
然后输入 ./sbt
。这时会下载依赖的包,可能会持续一段时间。
然后在 stb 输入提示符下,输入 reStart
来启动示例。类似的输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 sbt:akka-quickstart-scala> reStart [info] Updating ... [info] Done updating. [info] Compiling 1 Scala source to /Users/didi/Documents/GitHub/scala-simple-lesson/akka-quickstart-scala/target/scala-2.12/classes ... [info] Done compiling. [info] Application akka-quickstart-scala not yet started [info] Starting application akka-quickstart-scala in the background ... akka-quickstart-scala Starting com.example.AkkaQuickstart.main() [success] Total time: 5 s, completed 2019-6-25 20:16:03 sbt:akka-quickstart-scala> akka-quickstart-scala [INFO] [06/25/2019 20:16:04.502] [helloAkka-akka.actor.default-dispatcher-5] [akka://helloAkka/user/printerActor] Greeting received (from Actor[akka://helloAkka/user/helloGreeter#266661191]): Hello, Scala akka-quickstart-scala [INFO] [06/25/2019 20:16:04.502] [helloAkka-akka.actor.default-dispatcher-5] [akka://helloAkka/user/printerActor] Greeting received (from Actor[akka://helloAkka/user/howdyGreeter#1265860474]): Howdy, Akka akka-quickstart-scala [INFO] [06/25/2019 20:16:04.502] [helloAkka-akka.actor.default-dispatcher-5] [akka://helloAkka/user/printerActor] Greeting received (from Actor[akka://helloAkka/user/howdyGreeter#1265860474]): Howdy, Lightbend akka-quickstart-scala [INFO] [06/25/2019 20:16:04.502] [helloAkka-akka.actor.default-dispatcher-5] [akka://helloAkka/user/printerActor] Greeting received (from Actor[akka://helloAkka/user/goodDayGreeter#1928998630]): Good day, Play
代码解读 完整代码如下:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 1 2 package com.example3 4 import akka.actor.{ Actor , ActorLogging , ActorRef , ActorSystem , Props }5 6 7 8 object Greeter {9 10 def props (message: String , printerActor: ActorRef ): Props = Props (new Greeter (message, printerActor))11 12 final case class WhoToGreet (who: String )13 case object Greet 14 }15 16 17 18 19 class Greeter (message: String , printerActor: ActorRef ) extends Actor {20 import Greeter ._21 import Printer ._22 23 var greeting = "" 24 25 def receive = {26 case WhoToGreet (who) =>27 greeting = message + ", " + who28 case Greet =>29 30 printerActor ! Greeting (greeting)31 32 }33 }34 35 36 37 38 object Printer {39 40 def props : Props = Props [Printer ]41 42 final case class Greeting (greeting: String )43 }44 45 46 47 48 class Printer extends Actor with ActorLogging {49 import Printer ._50 51 def receive = {52 case Greeting (greeting) =>53 log.info("Greeting received (from " + sender() + "): " + greeting)54 }55 }56 57 58 59 object AkkaQuickstart extends App {60 import Greeter ._61 62 63 val system: ActorSystem = ActorSystem ("helloAkka" )64 65 66 67 val printer: ActorRef = system.actorOf(Printer .props, "printerActor" )68 69 70 val howdyGreeter: ActorRef =71 system.actorOf(Greeter .props("Howdy" , printer), "howdyGreeter" )72 val helloGreeter: ActorRef =73 system.actorOf(Greeter .props("Hello" , printer), "helloGreeter" )74 val goodDayGreeter: ActorRef =75 system.actorOf(Greeter .props("Good day" , printer), "goodDayGreeter" )76 77 78 79 howdyGreeter ! WhoToGreet ("Akka" )80 howdyGreeter ! Greet 81 82 howdyGreeter ! WhoToGreet ("Lightbend" )83 howdyGreeter ! Greet 84 85 helloGreeter ! WhoToGreet ("Scala" )86 helloGreeter ! Greet 87 88 goodDayGreeter ! WhoToGreet ("Play" )89 goodDayGreeter ! Greet 90 91 92 }93 94
我们先直观地看下这个文件的大体框架,如下图所示。
首先 main 类创建了ActorSystem 容器,然后创建了三个 Greeter Actor 实例和一个 Printer Actor 实例。
main 类首先将消息传递给三个 Greeter Actor,然后三个 Greeter Actor 分别再将消息传递给 Printer Actor,并由 Printer Actor 将消息打印出来。
下面是详细解读。
第 6 至 34 行,定义了 Greeter
的伴友类和伴友对象。注意 Greeter
是一个 Actor
,其 receive
方法定义在第 25 至 32 行。
第 12 行定义了 WhoToGreet
的样例类:final case class WhoToGreet(who: String)
。
第 13 行定义了 Greet
的样例类:case object Greet
。
Greeter
、WhoToGreet
、Greet
的关系是怎样的呢?
Greeter
意思为礼仪人员。它接收的 case class
是 WhoToGreet
和 Greet
。就是说礼仪人员有两个用途:一是组织根据人员组装合适的礼貌用语(WhoToGreet),另一个是将拼好的礼貌用语用嘴巴(Printer)说出去(Greet)。
第 36 至 56 行定义了 Printer
的伴友类和伴友对象。
第 41 行定义了 Greeting
样例类:case class Greeting(greeting: String)
。
第 51 至 54 行定义了 Printer
的 receive
方法。它接收 Greeting
,并将 greeting
内容打印出来。
第 58 至 94 行为入口方法。
第 63 行定义了名为 helloAkka
的 Actor System。
第 69 至 76 行创建了三个 Greeter 的实例:howdyGreeter
、helloGreeter
、goodDayGreeter
。他们会说不同的礼貌用语。
第 78 至 91 行调用 Actor 进行并发处理。我们以第一个调用为例,看一下处理流程。
1 2 howdyGreeter ! WhoToGreet ("Akka" ) howdyGreeter ! Greet
首先将 WhoToGreet("Akka")
的消息传递给 howdyGreeter
,这时会激活如下语句:
1 2 26 case WhoToGreet (who) =>27 greeting = message + ", " + who
此时
1 2 3 message = "Howdy" who = "Akka" greeting = message + ", " + who = "Howdy, Akka"
然后将 Greet 的消息传递给 howdyGreeter
,这时会激活如下语句:
1 2 3 4 28 case Greet =>29 30 printerActor ! Greeting (greeting)31
此时将 Greet
的消息通过 Greeting
传递给 PrinterActor
。
最后 PrintActor 收到消息后,将消息打印在日志上。
1 2 3 4 51 def receive = {52 case Greeting (greeting) =>53 log.info("Greeting received (from " + sender() + "): " + greeting)54 }
思考 如果我们多次输入 reStrart
来运行该例子,会发现输出的礼貌用语的次序各不相同。这就是说程序是并发执行的。同时需要注意,无论怎么执行,howdyGreeter
的两次结果,Akka
始终在 Lightbend
之前。我们可以得出结论:
不同的 Actor
直接是并发执行的。
同一个 Actor
接收到的多个消息,是串行执行的。
参考文献 https://www.geeksforgeeks.org/scala-singleton-and-companion-objects/
https://www.tutorialspoint.com/scala/index.htm
https://www.runoob.com/scala/scala-tutorial.html
https://blog.csdn.net/shenlei19911210/article/details/78538255
https://docs.scala-lang.org/zh-cn/tour/pattern-matching.html
https://developer.lightbend.com/guides/akka-quickstart-scala/