Scala是一门现代的多范式编程语言,平滑地集成了面向对象和函数式语言的特性,旨在以简练、优雅的方式来表达常用编程模式。Scala的设计吸收借鉴了许多种编程语言的思想,只有很少量特点是Scala自己独有的。Scala语言从写个小脚本到建立个大系统的编程任务均可胜任。Scala运行于Java平台(JVM,Java 虚拟机)上,并兼容现有的Java程序,Scala代码可以调用Java方法,访问Java字段,继承Java类和实现Java接口。在面向对象方面,Scala是一门非常纯粹的面向对象编程语言,也就是说,在Scala中,每个值都是对象,每个操作都是方法调用。
Spark的设计目的之一就是使程序编写更快更容易,这也是Spark选择Scala的原因所在。总体而言,Scala具有以下突出的优点:
Scala是Spark的主要编程语言,但Spark还支持Java、Python、R作为编程语言,因此,若仅仅是编写Spark程序,并非一定要用Scala。Scala的优势是提供了REPL(Read-Eval-Print Loop,交互式解释器),因此,在Spark Shell中可进行交互式编程(即表达式计算完成就会输出结果,而不必等到整个程序运行完毕,因此可即时查看中间结果,并对程序进行修改),这样可以在很大程度上提升开发效率。现在的计算机都是多核CPU,想充分利用其多核处理,需要写可并行计算的代码。而函数式编程在并行操作性有着天生的优势,函数式编程没有可变变量,就不会有内存共享的问题。
Scala程序需要运行在JVM(Java虚拟机)上,因此,在安装Scala之前,需要在Windows系统中安装Java,然后,再安装Scala。具体步骤如下:
安装Java
Scala程序需要运行在JVM(Java虚拟机)上,因此,在安装Scala之前,需要在Windows系统中安装Java环境。可以到Java官网(官网)下载JDK,比如,jdk-8u111-windows-x64.exe
文件,然后,运行该程序就可以完成JDK的安装。
安装Scala
访问Scala官网(官网),下载Scala。登录后,官网会自动识别你的操作系统类型,如果是Windows操作系统,官网会自动提供.msi格式的安装包,比如,scala-2.11.8.msi
,在Windows操作系统中,可以运行这个安装包,安装Scala。
Scala程序需要运行在JVM(Java虚拟机)上,因此,在安装Scala之前,需要在Linux系统中安装Java,然后,再安装Scala。
这里假设你已经安装好了Linux系统。请启动并进入Linux操作系统,并打开命令行终端(可以使用快捷键组合:ctrl+alt+t,打开终端窗口),进入shell环境。下面开始安装Java和Scala。具体步骤如下:
安装Java
请确保你的Linux系统中已经安装了Java JDK1.5或更高版本,并设置了JAVA_HOME环境变量,而且已经把JDK的bin目录添加到PATH变量。
Java环境可选择 Oracle 的 JDK,或是 OpenJDK。为图方便,这边直接通过命令安装 OpenJDK 7。
sudo apt-get install openjdk-7-jre openjdk-7-jdk
安装Scala
Scala有两种类型的变量,
scala> val myStr = "Hello World!"
val myStr: String = Hello World!
scala>
我们可以看到,myStr变量的类型是String类型,变量的值是Hello World! 这里需要注意的是,尽管我们在第1行代码的声明中,没有给出myStr是String类型,但是,Scala具有“类型推断”能力,可以自动推断出变量的类型。
当然,我们也可以显式声明变量的类型:
scala> val myStr2:String = "Hello World!"
val myStr2: String = Hello World!
scala>
需要说明的是,上面的String类型全称是java.lang.String,也就是说,Scala的字符串是由Java的String类来实现的,因此,我们也可以使用java.lang.String来声明,具体如下:
scala> val myStr3:java.lang.String = "Hello World!"
val myStr3: String = Hello World!
scala>
为什么可以不用java.lang.String,而只需要使用String就可以声明变量呢?这是因为,在每个应用程序中,Scala都会自动添加一些引用,这样,就相当于在每个程序源文件的顶端都增加了一行下面的代码:
import java.lang_
- 1
上面已经声明了一个String类型的不可变的变量,下面我们可以使用该变量,比如要打印出来:
scala> println(myStr)
Hello World!
scala>
因为myStr是val变量,因此,一旦初始化以后,就不能再次赋值,所以,下面我们执行的再次赋值操作会报错:
scala> myStr = "Hello World!"
^
error: reassignment to val
scala>
如果一些变量,需要在初始化以后还要不断修改它的值(比如商品价格),则需要声明为var变量。
下面我们把myPrice声明为var变量,并且在声明的时候需要进行初始化:
scala> var myPrice:Double = 9.9
var myPrice: Double = 9.9
scala>
我们再次对myPrice进行赋值,并打印输出:
scala> myPrice = 18.8
// mutated myPrice
scala> println(myPrice)
18.8
scala>
小技巧:如何在Scala解释器中输入多行代码
在Scala解释器中,当在命令提示符后面输入一个表达式并且按回车以后,代码就会被执行并显示出结果,比如下面我们输入一行表达式并回车:
scala> 3+8*2-7 val res2: Int = 12 scala>
- 1
- 2
- 3
- 4
这是输入单行代码的情形,但是,有时候,我们需要在命令提示符后面输入多行代码。
在Java中,每个语句都是以英文的分号结束,但是,在Scala中,可以不用分号。当然,如果你想把多条语句全部写在一行上面,这时还是需要使用分号来隔开各个语句的。
通常而言,只要Scala解释器推断出你的代码还没有结束,应该延续到下一行,解释器就会在下一行显示一个竖线“|”,你可以继续输入剩余的代码,比如,我们要输入表达式val myStr4 = “Hello World!”,我们只在命令提示符后面输入“val myStr4 = ”然后就回车,显然,这个表达式还没有结束,所以,解释器会在下一行显示一个竖线“|”,你可以在第2行继续输入”Hello World!”然后回车,解释器就会得到执行结果,具体如下:scala> val myStr4= | "Hello World!" val myStr4: String = Hello World! scala>
- 1
- 2
- 3
- 4
- 5
如果我们在命令提示符后面输入“val myStr5 = ”然后就回车,解释器会在下一行显示一个竖线“|”,这时如果我们发现变量名称错误,想放弃本次输入,就可以在“|”后面连续敲入两个回车,结束本次输入,具体如下:
scala> val myStr5 = | | You typed two blank lines. Starting a new command. scala>
- 1
- 2
- 3
- 4
- 5
Scala的数据类型包括:Byte、Char、Short、Int、Long、Float、Double和Boolean。和Java不同的是,在Scala中,这些类型都是“类”,并且都是包scala的成员,比如,Int的全名是scala.Int。对于字符串,Scala用java.lang.String类来表示字符串。
这里要明确什么是“字面量”?字面量包括整数字面量、浮点数字面量、布尔型字面量、字符字面量、字符串字面量、符号字面量、函数字面量和元组字面量。举例如下:
val i = 123 //123就是整数字面量
val i = 3.14 //3.14就是浮点数字面量
val i = true //true就是布尔型字面量
val i = 'A' //'A'就是字符字面量
val i = "Hello" //"Hello"就是字符串字面量
Scala允许对“字面量”直接执行方法,比如:
scala> 5.toString()
val res3: String = 5
scala> "abc".intersect("bcd")
val res4: String = bc
scala>
上面的intersect()方法用来输出两个字符串中都存在的字符。
在Scala中,可以使用加(+
)、减(-
) 、乘(*
) 、除(/
) 、余数(%
)等操作符,而且,这些操作符就是方法。例如,5 + 3和(5).+(3)是等价的,也就是说:
a 方法 b
a.方法(b)
上面这二者是等价的。前者是后者的简写形式,这里的+是方法名,是Int类中的一个方法。具体代码如下:
scala> val sum1 = 5 + 2 //实际上调用了 (5).+(2)
val sum1: Int = 7
scala> val sum2 = (5). + (2) //可以发现,写成方法调用的形式,和上面得到相同的结果
val sum2: Int = 7
scala>
需要注意的是,和Java不同,在Scala中并没有提供++和–操作符,当需要递增和递减时,类似Python形式,可以采用如下方式表达:
scala> var i = 5
var i: Int = 5
scala> i += 1
scala> println(i)
6
scala>
此外,也可以使用关系和逻辑操作,比如,大于(>)、小于(<)、大于等于(>=)和小于等于(<=),会产生Boolean类型的结果。
在执行for循环时,我们经常会用到数值序列,比如,i的值从1循环到5,这时就可以采用Range来实现。Range可以支持创建不同数据类型的数值序列,包括Int、Long、Float、Double、Char、BigInt和BigDecimal等。
在创建Range时,需要给出区间的起点和终点以及步长(默认步长为1)。下面通过几个实例来介绍:
创建一个从1到5的数值序列,包含区间终点5,步长为1
scala> 1 to 5
res0: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)
scala> 1.to(5)
res0: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)
创建一个从1到5的数值序列,不包含区间终点5,步长为1
scala> 1 until 5
res1: scala.collection.immutable.Range = Range(1, 2, 3, 4)
创建一个从1到10的数值序列,包含区间终点10,步长为2
scala> 1 to 10 by 2
res2: scala.collection.immutable.Range = Range(1, 3, 5, 7, 9)
创建一个Float类型的数值序列,从0.5f到5.9f,步长为0.8f
scala> 0.5f to 5.9f by 0.8f
res3: scala.collection.immutable.NumericRange[Float] = NumericRange(0.5, 1.3, 2.1, 2.8999999, 3.6999998, 4.5, 5.3)
在Scala编程中,经常需要用到打印语句。
scala> print("My name is:"); print("yanpenggong")
My name is:yanpenggong
scala>
上述代码执行后,会得到连在一起的一行结果。
如果要每次打印后追加一个换行符,实现换行的效果,就要采用println语句:
scala> println("My name is:"); println("yanpenggong")
My name is:
yanpenggong
scala>
如果要打印整型变量的值,可以使用下面语句:
scala> val i = 7
val i: Int = 7
scala> println(i)
7
scala>
此外,Scala还带有C语言风格的格式化字符串的printf函数:
scala> val i = 5;
val i: Int = 5
scala> val j = 8;
val j: Int = 8
scala> printf("My name is %s. I have %d apples and %d eggs.\n", "yanpenggong", i, j)
My name is yanpenggong. I have 5 apples and 8 eggs.
scala>
Scala需要使用java.io.PrintWriter实现把数据写入到文本文件。
这里使用 Vscode 进行编写,运行程序时,使用scala 3.5_读写文件.scala
。
import java.io.PrintWriter
import java.io.File
object ReadAndWriteFile{
def main(args: Array[String]): Unit = {
// 写入文本文件
println("===Write constent to file===")
val out = new PrintWriter("./output/3.5_output.txt")
// val out = new PrintWriter(new File("./output/3.5_output.txt")) // 方法2
for (i <- 1 to 6) out.println(i)
out.close()
}
}
上面代码中,new PrintWriter(“output.txt”)中只给出相对文件名,并没有给出完全文件路径,采用相对路径,这时,文件就会被保存到启动Scala REPL时的当前目录下。
需要注意的是,必须要执行out.close()语句,才会看到output.txt文件被生成,如果没有执行out.close()语句,我们就无法看到生成的output.txt文件。
可以使用Scala.io.Source的getLines方法实现对文件中所有行的读取。
import scala.io.Source
object ReadAndWriteFile{
def main(args: Array[String]): Unit = {
// 读取文本中的行
println("===Read file lines===")
val inputFile = Source.fromFile("./output/3.5_output.txt")
val lines = inputFile.getLines
for (line <- lines) println(line)
}
}
if语句是许多编程语言中都会用到的控制结构。在Scala中,执行if语句时,会首先检查if条件是否为真,如果为真,就执行对应的语句块,如果为假,就执行下一个条件分支。
object IfConditionalExpression{
def main(args: Array[String]): Unit = {
val x = 6
if (x > 0) {
println("This is a psitive number!")
} else if (x == 0) {
println("This is a zero")
} else {
println("This is not a positive number!")
}
// Scala中的if表达式的值可以赋值给变量
val a = if (x >0) {
1} else {
-1}
println(f"a: $a")
}
}
输出:
(base) kungs@kungsMacPro 第4章_控制结构 % scala 4.1_if条件表达式.scala
This is a psitive number!
a: 1
(base) kungs@kungsMacPro 第4章_控制结构 %
object WhileCycle{
def main(args: Array[String]): Unit = {
// 方法1 while语句
println("方法1 while语句: ")
var i = 9
while (i > 0) {
i -= 1
printf(f"i is $i\n")
}
// 方法2 do-while语句
println("方法2 do-while语句: ")
var j = 0
do {
j += 1
printf("j is %d\n", j)
} while (j < 5)
}
}
输出:
(base) kungs@kungsMacPro 第4章_控制结构 % scala 4.2_while循环.scala
方法1 while语句:
i is 8
i is 7
i is 6
i is 5
i is 4
i is 3
i is 2
i is 1
i is 0
方法2 do-while语句:
j is 1
j is 2
j is 3
j is 4
j is 5
(base) kungs@kungsMacPro 第4章_控制结构 %
Scala中的for循环语句格式如下:
for (变量<-表达式) 语句块
其中,“变量<-表达式”被称为“生成器(generator)”。
object ForCycle {
def main(args: Array[String]): Unit = {
// i 不需要提前变量声明,可以在for语句括号中的表达式直接使用,
// `<-`表示之前的要遍历后面1到5的所有值。
for (i <- 1 to 3) {
println(f"遍历 i: $i")}
// 设置步长=2
for (i <- 1 to 5 by 2) {
println(f"步长2对应的i: $i")}
// 有时候不想打印出所有结果,可以通过满足制定条件进行输出,即"守卫(guard)"
// 这里只输出1到5之间到所有偶数
for (i <- 1 to 5 if i%2==0) {
println(f"guard i: $i")}
// 支持多个生成器的情形,用`;`隔开
for (i <- 1 to 2; j <- 1 to 3) {
println(f"$i * $j = ${
i*j}")}
// 给每个生成器添加一个`守卫(guard)`
for (i <- 1 to 5 if i%2==0; j <- 1 to 3 if j != i) {
println(f"guard($i * $j = ${
i*j})")}
}
}
输出:
(base) kungs@kungsMacPro 第4章_控制结构 % scala 4.3_for循环.scala
遍历 i: 1
遍历 i: 2
遍历 i: 3
步长2对应的i: 1
步长2对应的i: 3
步长2对应的i: 5
guard i: 2
guard i: 4
1 * 1 = 1
1 * 2 = 2
1 * 3 = 3
2 * 1 = 2
2 * 2 = 4
2 * 3 = 6
guard(2 * 1 = 2)
guard(2 * 3 = 6)
guard(4 * 1 = 4)
guard(4 * 2 = 8)
guard(4 * 3 = 12)
(base) kungs@kungsMacPro 第4章_控制结构 %
有时候,我们需要对上述过滤后的结果进行进一步的处理,这时,就可以采用yield
关键字,对过滤后的结果构建一个集合。
object ForCycle {
def main(args: Array[String]): Unit = {
// for推导式, 得到的会是一个vector
val vectors = for (i <- 1 to 5 if i%2==0) yield i
println(f"vectors: $vectors")
}
}
输出:
(base) kungs@kungsMacPro 第4章_控制结构 % scala 4.3_for循环.scala
vectors: Vector(2, 4)
(base) kungs@kungsMacPro 第4章_控制结构 %
数组是编程中经常用到的数据结构,一般包括定长数组和变长数组。旨在快速掌握最基础和常用的知识,因此,只介绍定长数组。
定长数组,就是长度不变的数组,在Scala中使用Array进行声明,如下:
object DataStructureArray {
def main(args: Array[String]): Unit = {
val IntValueArr = new Array[Int](3) // 声明一个长度为3的整数型数组,每个数组元素初始化为0
IntValueArr(0) = 22 // 给第1个数组元素赋值为22
IntValueArr(1) = 66 // 给第2个数组元素赋值为66
IntValueArr(2) = 45 // 给第3个数组元素赋值为45
IntValueArr.foreach(println) // 遍历打印数组
}
}
输出:
(base) kungs@kungsMacPro 第5章_数据结构 % scala 5.1_数组.scala
22
66
45
(base) kungs@kungsMacPro 第5章_数据结构 %
需要注意的是,在Scala中,对数组元素的应用,是使用圆括号,而不是方括号,也就是使用IntValueArr(0),而不是IntValueArr[0]。
下面声明一个字符串数组:
object DataStructureArray {
def main(args: Array[String]): Unit = {
val StrValueArr = new Array[String](3) // 声明一个长度为3的字符串数组,每个数组元素初始化为null
StrValueArr(0) = "Scala"
StrValueArr(1) = "Spsark"
StrValueArr(2) = "Hadoop"
StrValueArr.foreach(println)
for (i <- 0 to 2) {
println(f"StrValueArr(${
i}): ${
StrValueArr(i)}")}
}
}
输出:
(base) kungs@kungsMacPro 第5章_数据结构 % scala 5.1_数组.scala
Scala
Spsark
Hadoop
StrValueArr(0): Scala
StrValueArr(1): Spsark
StrValueArr(2): Hadoop
(base) kungs@kungsMacPro 第5章_数据结构 %
实际上,Scala提供了更加简洁的数组声明和初始化方法,如下:
val IntValueArr_2 = Array(22, 66, 45)
val StrValueArr_2 = Array("Scala", "Spark", "Hadoop")
从上面代码可以看出,都不需要给出数组类型,Scala会自动根据提供的初始化数据来推断出数组的类型。
列表有头部和尾部的概念,可以使用intList.head
来获取上面定义的列表的头部,值是1,使用intList.tail
来获取上面定义的列表的尾部,值是List(2,3),可以看出,头部是一个元素,而尾部则仍然是一个列表。
object DataStructureList {
def main(args: Array[String]): Unit = {
val intlist = List(1, 2, 3)
println(f"intList: ${
intList}")
println(f"head: ${
intlist.head}")
println(f"tail: ${
intlist.tail}")
}
}
输出:
(base) kungs@kungsMacPro 第5章_数据结构 % scala 5.2_列表\(List\).scala
intList: List(1, 2, 3)
head: 1
tail: List(2, 3)
(base) kungs@kungsMacPro 第5章_数据结构 %
由于列表的头部是一个元素,可以使用::
操作,在列表的头部增加新的元素,得到一个新的列表。
object DataStructureList {
def main(args: Array[String]): Unit = {
val intList = List(1, 2, 3)
val intListOther = 0::intList
println(f"intListOther: $intListOther")
}
}
输出:
(base) kungs@kungsMacPro 第5章_数据结构 % scala 5.2_列表\(List\).scala
intListOther: List(0, 1, 2, 3)
(base) kungs@kungsMacPro 第5章_数据结构 %
注意,上面操作执行后,intList不会发生变化,依然是List(1,2,3),intListOther是一个新的列表List(0,1,2,3)
::
操作符是右结合的,因此,如果要构建一个列表List(1,2,3),实际上也可以采用下面的方式:
object DataStructureList {
def main(args: Array[String]): Unit = {
val new_intList = 1::2::3::Nil
println(f"new_intList: $new_intList")
}
}
输出:
(base) kungs@kungsMacPro 第5章_数据结构 % scala 5.2_列表\(List\).scala
new_intList: List(1, 2, 3)
(base) kungs@kungsMacPro 第5章_数据结构 %
上面代码中,Nil表示空列表。也可以使用:::
操作符对不同的列表进行连接得到新的列表:
object DataStructureList {
def main(args: Array[String]): Unit = {
val intList1 = List(1, 2)
val intList2 = List(3, 4)
val intList3 = intList1:::intList2
println(f"intList3: $intList3")
}
}
输出:
(base) kungs@kungsMacPro 第5章_数据结构 % scala 5.2_列表\(List\).scala
intList3: List(1, 2, 3, 4)
(base) kungs@kungsMacPro 第5章_数据结构 %
注意,执行上面操作后,intList1和intList2依然存在,intList3是一个全新的列表。
实际上,Scala还为列表提供了一些常用的方法,比如,如果要实现求和,可以直接调用sum方法,如下:
object DataStructureList {
def main(args: Array[String]): Unit = {
val intList = List(1, 2, 3)
// 求和
println(f"sum: ${
intList.sum}")
}
}
输出:
(base) kungs@kungsMacPro 第5章_数据结构 % scala 5.2_列表\(List\).scala
sum: 6
(base) kungs@kungsMacPro 第5章_数据结构 %
元组是不同类型的值的聚集。元组和列表不同,列表中各个元素必须是相同类型,而元组可以包含不同类型的元素。
object DataStructureTuple {
def main(args: Array[String]): Unit = {
val tuple = ("Scala", 2022, 7.18)
// 遍历元组中的元素,并打印
tuple.productIterator.foreach(i => println(f"tuple value_i:${
i}"))
println(f"tuple_1:${
tuple._1}")
println(f"tuple_2:${
tuple._2