Spark本身就是使用Scala语言开发的,spark和flink的底层通讯都是基于的高并发架构akka开发,然而akka是用scala开发的,Scala与Spark可以实现无缝结合,因此,Scala顺理成章地成为了开发Spark应用的首选语言,大多数spark教程都是基于scala编写,学习一下scala也是很必要的。
这里只对scala在spark中使用的部分进行记录,不涉及一些复杂的scala使用,只保证能正常使用spark即可。
对于scale的话,学过kotlin的同学应该学起来还是很容易的,因为一些写法都很相近;即便没有学过kotlin对于java用户,经常使用java stream的也不会陌生,如果是写过java响应式的话(reactor3, netty)的也应该没有啥学习成本的。
一、变量Scala 只有两种类型的变量,分别使用关键字val和var进行声明。对于用val声明的变量,在声明时就必须被初始化,而且初始化以后就不能再赋新的值;对于用var声明的变量,是可变的,可以被多次赋值, val定义的变量可以重复定义。
二、输入|输出从控制台读写数据,可以使用以read为前缀的方法,包括:readInt、readDouble、readByte、readShort、readFloat、readLong、readChar、readBoolean及readLine,分别对应9种基本数据类型,其中,前8种方法没有参数,readLine可以不提供参数,也可以带一个字符串参数的提示。所有这些函数都属于对象scala.io.StdIn的方法:
scala的输出方法有三种:print、println和printf,其中printf不常用。其中print就是打印,println会在打印之后添加换行,printf有format的功能。
Scala提供了字符串插值机制,以方便在字符串字面量中直接嵌入变量的值。为了构造一个插值字符串,只需要在字符串字面量前加一个“s”字符或“f”字符,然后,在字符串中即可以用$插入变量的值,s插值字符串不支持格式化,f插值字符串支持在$变量后再跟格式化参数。
三、读写文件Scala使用类java.io.PrintWriter实现文本文件的创建与写入。尽管PrintWriter类也提供了printf函数,但是,它不能实现数值类型的格式化写入。为了实现数值类型的格式化写入,可以使用String类的format方法,或者用f插值字符串,和python3.6之后的format方式差不多。
Scala使用类scala.io.Source实现对文件的读取,最常用的方法是getLines方法,它会返回一个包含所有行的迭代器。
四、for循环与Java的for循环相比,Scala的for循环在语法表示上有较大的区别,同时,for也不是while循环的一个替代者,而是提供了各种容器遍历的强大功能,用法也更灵活。for 循环最简单的用法就是对一个容器的所有元素进行枚举,基本语法结构为:for (变量 <- 表达式) {语句块},其中,“变量<-表达式”被称为“生成器(Generator)”,该处的变量不需要关键字var或val进行声明,其类型为后面的表达式对应的容器中的元素类型,每一次枚举,变量就被容器中的一个新元素所初始化。
for循环不仅仅可以对一个集合进行完全枚举,还可以通过添加过滤条件对某一个子集进行枚举,这些过滤条件被称为“守卫式(Guard)”:
可以通过添加多个生成器实现嵌套的for循环,其中,每个生成器之间用分号隔开,例如:
五、数据结构 1.数组数组(Array)是一种可变的、可索引的、元素具有相同类型的数据集合,它是各种高级语言中最常用的数据结构。Scala 提供了参数化类型的通用数组类Array[T],其中,T 可以是任意的 Scala类型。Scala数组与Java数组是一一对应的。即Scala的Array[Int]可看作Java的Int[],Array[Double]可看作Java的Double[],Array[String]可看作Java的String[]。
Array提供了函数ofDim来定义二维和三维数组,用法如下:
2.元祖Scala的元组是对多个不同类型对象的一种简单封装。Scala提供了TupleN类(N的范围为1~22),用于创建一个包含N个元素的元组。构造一个元组的语法很简单的,只需把多个元素用逗号分开并用圆括号包围起来就可以了。
可以使用下划线“_”加上从1开始的索引值,来访问元组的元素:
还可以一次解包,直接赋值:
3.容器Scala用了三个包来组织容器类,分别是scala.collection、scala.collection.mutable和scala.collection.immutable。从名字即可看出scala.collection.immutable包是指元素不可变的容器;scala.collection.mutable包指的是元素可变的容器;而scala.collection封装了一些可变容器和不可变容器的超类或特质。
在 Iterable下的继承层次包括3个特质,分别是序列(Seq)、映射(Map)和集合(Set),这3种容器最大的区别是其元素的索引方式,序列是按照从0开始的整数进行索引的,映射是按照键值进行索引的,而集合是没有索引的。
4.序列序列(Sequence)是指元素可以按照特定的顺序访问的容器。在Scala的容器层级中,序列容器的根是collection.Seq特质,是对所有可变和不可变序列的抽象。序列中每个元素均带有一个从0开始计数的固定索引位置。特质Seq具有两个子特质LinearSeq和IndexedSeq,这两个子特质没有添加任何新的方法,只是针对特殊情况对部分方法进行重载,以提供更高效的实现。LinearSeq序列具有高效的head和tail操作,而IndexedSeq序列具有高效的随机存储操作。实现了特质LinearSeq的常用序列有列表(List)和队列(Queue)。实现了特质IndexedSeq的常用序列有可变数组(ArrayBuffer)和向量(Vector)。
IndexedSeq就好比java的ArrayList,LinearSeq就好比java的linkedList。
- List:immutable , LinearSeq
- Vector:immutable, IndexedSeq
- ListBuffer:mutable, LinearSeq
- ArrayBuffer:mutable,IndexedSeq
由于List是一个特质,因此不能直接用new关键字来创建一个列表:
vector常用操作:
ListBuffer使用:
5.集合Scala的集合(Set)是不重复元素的容器。相对于列表中的元素是按照索引顺序来组织的,集合中的元素并不会记录元素的插入顺序,而是以“哈希”方法对元素的值进行组织(不可变集在元素很少时会采用其他方式实现),所以,它可以支持快速找到某个元素。集合包括可变集和不可变集,分别位于scala.collection.mutable包和scala.collection.immutable包,缺省情况下创建的是不可变集。
6.映射映射(Map)是一系列键值对的容器。Scala提供了可变映射和不可变映射,分别定义在包scala.collection.mutable和scala.collection.immutable 里。默认情况下,Scala使用的是不可变映射。操作符“->”是定义二元组的简写方式。
六、面向对象作为一个运行在JVM上的语言,Scala毫无疑问首先是面向对象的语言。尽管在具体的数据处理部分,函数式编程在Scala中已成为首选方案,但在上层的架构组织上,仍然需要采用面向对象的模型,这对于大型的应用程序尤其必不可少。具体使用方式和java大同小异。
scala中函数式一等公民,对于编写方法这部分和java还是相差不叫大的和kotlin相似。
1.setter和getterclass Counter {
private var cnt:Int = 0
// 这里类似于java的setter和getter
def value: Int = cnt
def value_=(newValue:Int) {
if (newValue > 0) cnt = newValue
}
def increment(step:Int):Unit={cnt+=step}
}
2.函数定义
在Scala语言中,方法参数前不能加上val或var关键字来限定,所有的方法参数都是不可变类型,相当于隐式地使用了val关键字限定,如果在方法体里面给参数重新赋值,将不能通过编译。
class Counter {
private var cnt:Int = 0
// 不需要输入的函数可以不写(),这里函数调用的时候也不能写括号
def current=cnt
// 写()的行数调用的时候可以不写()
def current2()=cnt
def increment(step:Int):Unit={cnt+=step}
// 可以不写返回类型,但是必须得写{}
def increment2(step:Int){cnt+=step}
// 可以省略掉{}
def increment3(step:Int)= cnt+=step
// 可以在赋值的时候设置默认值
def increment4(step:Int=1)= cnt+=step
}
3.构造器
scala的构造器和kotlin的比较相似,整个类的定义主体就是类的构造器,称为主构造器,所有位于类方法以外的语句都将在构造过程中被执行。可以像定义方法参数一样,在类名之后用圆括号列出主构造器的参数列表。除了主构造器,Scala还可以包含零个或多个辅助构造器(AuxiliaryConstructor)。辅助构造器使用this进行定义,this的返回类型为Unit。
// 可以在创建类的时候直接构造
class Employee(var id:Int=1, var name:String="zhangsan") {
// 辅助构造器
def this(id:Int){
this()
this.id = id
}
// 第二辅助构造器
def this(name:String){
this()
this.name = name
}
def getInfo():Unit =println(f"ID: $id, 姓名:$name")
}
4.伴生对象
单例对象包括两种,即伴生对象(Companion Object)和孤立对象(StandaloneObject)。当一个单例对象和它的同名类一起出现时,这时的单例对象被称为这个同名类的“伴生对象”。没有同名类的单例对象,被称为孤立对象。
class Employee(var id:Int=1, var name:String="zhangsan") {
// 辅助构造器
def this(id:Int){
this()
this.id = id
}
// 第二辅助构造器
def this(name:String){
this()
this.name = name
}
private val gender = Employee.change()
def getInfo():Unit =println(f"ID: $id, 姓名:$name, 性别:$gender")
}
object Employee {
private var gender:String = "male"
def change(): String ={
gender = "female"
gender
}
def main(args: Array[String]): Unit = {
val v1 = new Employee();
val v2 = new Employee(2, "lisi")
v1.getInfo()
v2.getInfo()
}
}
apply方法:在Scala中,apply方法遵循如下的约定被调用:用括号传递给类实例或对象名一个或多个参数时,Scala会在相应的类或对象中查找方法名为apply且参数列表与传入的参数一致的方法,并用传入的参数来调用该apply方法。
class Employee(var id:Int=1, var name:String="zhangsan") {
private val gender = Employee.change()
def getInfo():Unit =println(f"ID: $id, 姓名:$name, 性别:$gender")
def apply(it: Int)=println(f"apply方法输入为: $it")
}
object Employee {
private var gender:String = "male"
def change(): String ={
gender = "female"
gender
}
def main(args: Array[String]): Unit = {
val v1 = new Employee();
val v2 = new Employee(2, "lisi")
v1(2)
v2.apply(3)
}
}
可以直接通过使用伴生对象生成一个类对象:
class Employee(var id:Int=1, var name:String="zhangsan") {
private val gender = Employee.change()
def getInfo():Unit =println(f"ID: $id, 姓名:$name, 性别:$gender")
}
object Employee {
private var gender:String = "male"
def change(): String ={
gender = "female"
gender
}
def apply(id:Int, name:String)=new Employee(id, name)
}
object Test{
def main(args: Array[String]): Unit = {
// 通过伴生对象生成Employee对象
val v1 = Employee(3, "wangwu")
v1.getInfo()
}
}
unapply方法用于对对象进行解构操作,与apply方法类似,该方法也会被自动调用。可以认为unapply方法是apply方法的反向操作,apply方法接受构造参数变成对象,而unapply方法接受一个对象,从中提取值。unapply方法包含一个类型为伴生类的参数,返回的结果是Option类型
class Employee(var id:Int=1, var name:String="zhangsan") {
private val gender = Employee.change()
def getInfo():Unit =println(f"ID: $id, 姓名:$name, 性别:$gender")
}
object Employee {
private var gender:String = "male"
def change(): String ={
gender = "female"
gender
}
def apply(id:Int, name:String)=new Employee(id, name)
def unapply(arg: Employee): Option[(Int, String, String)] = Some((arg.id, arg.name, arg.gender))
}
object Test{
def main(args: Array[String]): Unit = {
// 通过伴生对象生成Employee对象
val v1 = Employee(3, "wangwu")
v1.getInfo()
var Employee(id, name, gender) = v1
println(f"$id, $name, $gender")
}
}
5.特质
Java 中提供了接口,允许一个类实现任意数量的接口,相当于达到了多重继承的目的。但是,在Java 8以前,接口的一个缺点是,不能为接口方法提供默认实现,使得该接口的所有类都要重复相同的样板代码来实现接口的功能。为此,Scala从设计之初就对Java接口的概念进行了改进,使用“特质(Trait)”来实现代码的多重复用,它不仅实现了接口的功能,还具备了很多其他的特性。Scala的特质是代码重用的基本单元,可以同时拥有抽象方法和具体方法。Scala中,一个类只能继承自一个超类,却可以混入(Mixin)多个特质,从而重用特质中的方法和字段,实现了多重继承。
object Employee {
private var gender:String = "male"
def change(): String ={
gender = "female"
gender
}
def apply(id:Int, name:String)=new Employee(id, name)
def unapply(arg: Employee): Option[(Int, String, String)] = Some((arg.id, arg.name, arg.gender))
}
trait Workable{
var workTime:Int=8
def salary():Int // 抽象函数
// 具体函数
def isWorking:Boolean={
println("员工工作中")
true
}
}
trait Work extends Workable{
// 实现函数
override def salary(): Int = workTime * 1000
// 重载函数
override def isWorking: Boolean = {
println("员工在休息")
false
}
}
class Employee(var id:Int=1, var name:String="zhangsan") extends Work {
private val gender = Employee.change()
def info():Unit =println(f"ID: $id, 姓名:$name, 性别:$gender")
}
object Test{
def main(args: Array[String]): Unit = {
val e1 = Employee(4, "sange")
e1.isWorking
println(e1.salary())
}
}
七、函数式编程
在数学语言里,函数表示的是一种映射关系,其作用是对输入的值进行计算,并返回一个结果,函数内部对外部的全局状态没有任何影响,即在数学语言里,函数是没有副作用的。在编程语言里,我们把这种无副作用的函数称为纯函数,纯函数式编程正是借用了这种纯函数的概念。纯函数的行为表现出与上下文无关的透明性和无副作用性,即函数的调用结果只与输入值有关,而不会受到调用时间和位置的影响,另外,函数的调用也不会改变任何全局对象,这些特性使得多线程的并发应用中最复杂的状态同步问题不复存在。正是这一巨大优势,使得函数式编程在大数据应用和并发需求的驱动下,成为越来越流行的编程范式。
具体来说就是这种无副作用的编程方法,可以很简单的实现高并发,不用去关心并发带来的安全问题。
上面的代码相信学过es6的都比较熟了,就是lambda函数,其实各个语言都有类似的函数,也叫箭头函数(python里不是箭头)。
当函数的每个参数在函数字面量内仅出现一次,可以省略“=>”并用下划线作为参数的占位符来简化函数字面量的表示,第一个下划线代表第一个参数,第二个下划线代表第二个参数,依此类推。
1.curry化当函数的每个参数在函数字面量内仅出现一次,可以省略“=>”并用下划线作为参数的占位符来简化函数字面量的表示,第一个下划线代表第一个参数,第二个下划线代表第二个参数,依此类推。
可以通过Curry化过程,将一个多参数的普通函数转化为Curry化的函数。
2.常用函数式编程函数类似java8里的stream写法
object Functional {
def main(args: Array[String]): Unit = {
val map = Map(1->"张三", 2->"李四", 3->"王五")
// foreach
map.foreach(kv => println(f"${kv._1} -> ${kv._2}"))
map.foreach{kv => println(f"${kv._1} -> ${kv._2}")}
map.foreach{case(k,v) => println(f"$k -> $v")}
// map
map.map(kv=> f"${kv._1}:${kv._2}").foreach(println(_))
// flatmap
map.map(kv=> f"${kv._1}:${kv._2}").flatMap(s=>s.toList).foreach(s => print(f"$s "))
// filter
map.filter(kv => kv._1 == 1).foreach(println(_))
// reduce
println(map.keys.reduce(_+_+1).toString)
// fold
val list = List(1,2,3,4,5)
println((list foldRight (10))(_-_))
println((list foldLeft (10))(_-_))
//partition 通过逻辑判断分组
println(list.partition(_<4))
// groupby 通过计算的值作为key分组
println(list.groupBy(_%2==0))
// grouped 整体进行分组输入为组数两返回一个iterator
println(list.grouped(3).next())
// sliding 活动拆分为对应数量的组
println(list.sliding(3).next())
}
}



