學好Spark必須要掌握的Scala技術點
前言
Scala是以JVM為運行環境的面向對象的函數式編程語言,它可以直接訪問Java類庫并且與Java框架進行交互操作。
正如之前所介紹,Spark是用Scala語言編寫的,Kafka server端也是,那么深入學習Scala對掌握Spark、Kafka是必備掌握技能。
本篇文章主要介紹,在學習、編寫Spark程序時,至少要掌握的Scala語法,多以示例說明。建議在用Scala編寫相關功能實現時,邊學習、邊應用、邊摸索以加深對Scala的理解和應用。
1. 變量、表達式、循環、Option、方法和函數
1.1 聲明變量
def main(args: Array[String]): Unit = {
//使用val定義的變量值是不可變的,相當于java里用final修飾的變量
val i = 1
//使用var定義的變量是可變的,在Scala中鼓勵使用val
var s = "hello"
//Scala編譯器會自動推斷變量的類型,必要的時候可以指定類型
//變量名在前,類型在后
val str: String = "hello"
}
1.2 表達式
1.2.1 條件表達式
def main(args: Array[String]): Unit = {
val x = 1
// 判斷x是否大于0,將最終結果賦給y,打印y
// 二者等效, Scala語言強調代碼簡潔
// var y = if(x > 0) {x} else {-1}
// val y = if(x > 0) x else -1
// 支持混合類型表達式,返回值類型是Any
// var y = if(x > 0) x else "no"
// 如果缺失else,相當于if (x > 0) 1 else ()
// scala表達式中有一個Unit類,寫作(),相當于java中void
// val y = if(x > 0) 1
// if和else if
val f = if (x < 0) 0 else if (x >= 1) 1 else -1
println(y)
}
1.2.2 塊表達式
def main(args: Array[String]): Unit = {
val x = 0
// scala中{}可包含一系列表達式,塊中運行最終結果為塊的值
val result = {
if(x < 0) -1 else if(x >= 1) 1 else "error"
}
println(result)
}
1.3 循環
Scala里面while循環和Java中的while循環使用方式類似,這里主要以for循環為例:
def main(args: Array[String]): Unit = {
// 表達式1 to 10返回一個Range區間,每次循環將區間中的一個值賦給i
for (i <- 1 to 3) {
println(i)
}
//i代表數組中的每個元素
val arr = Array("a", 1, "c")
for (i <- arr) {
println(i)
}
//高級for循環
//每個生成器都可以帶一個條件,注意:if前面沒有分號
//相當于雙層for循環,i每獲得一個值對1to3進行全部遍歷并賦值給j然后進行條件判斷
for (i <- 1 to 3; j <- 1 to 3 if (i != j)) {
println(i + j)
}
//for推導式:如果for的循環體以yield開頭,則該循環會構建一個集合
// 每次迭代生成集合中的一個元素 集合類型為Vector
var v = for (i <- 1 to 3) yield i * i
println(v)
//遍歷一個數組,to:包頭包尾;until:包頭不包尾
for (i <- arr.length - 1) {
println(arr(i))
}
for(i <- 0 until arr.length) {
println(arr(i))
}
}
1.4 Option類型
在Scala中Option類型樣例類用來表示可能存在或也可能不存在的值(Option的子類有Some和None)。Some包裝了某個值,None表示沒有值:
def main(args: Array[String]): Unit = {
val map = Map("a"->1,"b"->2)
//根據key獲取value匹配match中的邏輯有值返回Some類型(已封裝數據),無值返回None
val v = map.get("b") match {
case Some(i) => i
case None => 0
}
println(v)
//更好的方式
val value = map.getOrElse("c",0)
println(value)
}
1.5 方法和函數
Scala中的+、-、*、/、%等操作符的作用與Java一樣,位操作符&、|、^、>>、<<也一樣。但在Scala中:這些操作符實際上是方法。例如:a + b是a.+(b)方法調用的簡寫:a 方法 b可以寫成 a.方法(b)。
方法的返回值類型可以不寫,編譯器可以自動推斷出來,但是對于遞歸函數,必須指定返回類型。
def str = "a" 成立,定義一個字符串

在函數式編程語言中,函數可以像任何其他數據類型一樣被傳遞和操作:

偏函數:
//偏函數,它是PartialFunction[-A,+B]的一個實例,A代表參數類型,B代表返回值類型,常用作模式匹配(后文闡述)。
def func1: PartialFunction[String, Int] = {
case "one" => 1
case "two" => 2
case _ => -1
}
def func2(num: String): Int = num match {
case "one" => 1
case "two" => 2
case _ => -1
}
def main(args: Array[String]) {
println(func1("one"))
println(func2("three"))
}
2. 數組、映射、元組、集合
2.1 數組
import scala.collection.mutable.ArrayBuffer
//scala導包比如導入scala.collection.mutable下所有的類:scala.collection.mutable._
object ArrayDemo {
def main(args: Array[String]): Unit = {
println("======定長數組======")
// 初始化一個長度為8的定長數組,所有元素初始化值為0
var arr1 = new Array[Int](8)
// 底層調用的apply方法
var arr2 = Array[Int](8)
//toBuffer會將數組轉換成數組緩沖
println(arr1.toBuffer) // ArrayBuffer(0, 0, 0, 0, 0, 0, 0, 0)
println(arr1(2)) // 使用()來訪問元素
println("=======變長數組(數組緩沖)======")
val varr = ArrayBuffer[Int]()
//向數組緩沖尾部追加一個或多個元素(+=)
varr += 1
varr += (2, 3)
//追加一個數組用 ++=
varr ++= Array(4, 5)
varr ++= ArrayBuffer(6, 7)
//指定位置插入元素-1和3;參數1:指定位置索引,參數2:插入的元素可以是多個
// def insert(n: Int, elems: A*) { insertAll(n, elems) }
varr.insert(0,-1,3)
varr.remove(0) //刪除指定索引處的元素
//從指定索引處開始刪除,刪除多個元素;參1:指定索引,參2:刪除個數
varr.remove(0,2)
// 從0索引開始刪除n個元素
// varr.trimStart(2)
//從最后一個元素開始刪除,刪除指定個數的元素(length-n max 0)
varr.trimEnd(2)
)
//reduce ==>非并行化集合調用reduceLeft
//(((1+8)+3)+5)...
println(varr.reduce((x,y)=> x+y))
println(varr.reduce(_+_))
println(varr.reduce(_-_))
Array("one","two","three").max //two,字符串比較大小,按照字母表順序
Array("one","two","three").mkString("-")//以"-"作為數組中元素間的分隔符one-two-three
Array("one","two","three").mkString("1","-","2")//1one-two-three2
//參1:arr元素的數組個數,參2:arr中每個數組中元素個數
val arr = Array.ofDim[Int](2, 3)
arr.head arr.last //數組的第一個和最后一個元素
}
}
yield關鍵字將原始的數組進行轉換會產生一個新的數組,原始的數組不變
def main(args: Array[String]) {
//定義一個數組
val arr = Array(1, 2, 3, 4, 5, 6, 7, 8, 9)
//將偶數取出乘以10后再生成一個新的數組
val res = for (e <- arr if e % 2 == 0) yield e * 10
println(res.toBuffer)
//filter過濾接收一個返回值為boolean的函數,過濾掉返回值為false的元素
//map將數組中的每一個元素取出來(_)進行相應操作
val r = arr.filter(_ % 2 == 0).map(_ * 10)
println(r.toBuffer)
//數組元素求和,最大值,排序
println(arr.sum+":"+arr.max+":"+arr.sorted.toBuffer)
}
2.2 映射
在Scala中,把哈希表這種數據結構叫做映射,類似于Java中的Map。
在Scala中,有兩種Map:
不可變Map:scala.collection.immutable.Map(可以存儲一些配置或參數供多個線程訪問,保證線程安全,具體還要結合業務實際場景),內容不可更改
可變Map:scala.collection.mutable.Map==>類似于Java中的HashMap,可以進行put、get、remove等操作,內容可變

map += ("c" -> 3) map += (("d",4)) 增加元素 -=移除元素 +/-增加或移除一個元素并返回一個新的集合
注意:通常我們在創建一個集合時會用val這個關鍵字修飾一個變量,那么就意味著該變量的引用不可變,該引用中的內容是不是可變還取決于這個引用指向的集合的類型
2.3 元組
映射是K/V對偶的集合,對偶是元組的最簡單形式,元組可以裝著多個不同類型的值,元組是不可變的

zip命令可以將多個值綁定在一起(將兩個數組/集合的元素一一對偶):

注意:如果兩個數組的元素個數不一致,拉鏈操作后生成的數組的長度為較小的那個數組的元素個數
對于元組val t = (1, 3.14, "Fred"),val (first, second, _) = t // second等于3.14
2.4 集合
Scala的集合有三大類:序列Seq、集Set、映射Map,所有的集合都擴展自Iterable特質。集合分可變(mutable)和不可變(immutable)兩種類型,immutable類型的集合初始化后長度和內容都不能改變(注意與val修飾的變量進行區別)
2.4.1 Seq/List
在Scala中列表要么為空(Nil表示空列表)要么是一個head元素加上一個tail列表。
9 :: List(5, 2) :: 操作符是將給定的頭和尾創建一個新的列表【注意::: 操作符是右結合的,如9 :: 5 :: 2 :: Nil相當于 9 :: (5 :: (2 :: Nil))】
def main(args: Array[String]): Unit = {
//創建一個不可變集合
val lt = List(1, 2, 3)
/*//添加元素到lt前面生成一個新的List
val lt2 = ("a", -1, 0) :: lt
val lt3 = lt.::(0)
val lt4 = 0 +: lt
val lt5 = lt.+:(0)
println(lt + "==>" + lt2 + "==" + lt3 + "==" + lt4 + "==" + lt5)
val lt6 = lt :+ 4
println("添加元素到后面:"+lt6 )*/
//合并兩個集合,lt0在lt前面
val lt0 = List(4,5,6,7)
val lt7 = lt0.union(lt)
val lt8 = lt0 ++ lt
println(lt7 +":"+lt8 )
println("lt0在lt后面"+lt ++ lt0)
//將兩個集合中的元素一一綁定,如果元素數不一致以較少元素集合為準
println(lt0.zip(lt).toMap)
//將lt0插入到lt前面生成一個新的集合
println(lt0 ++: lt)
println(lt.:::(lt0))
}
def main(args: Array[String]): Unit = {
// 構建一個可變列表,初始有3個元素1,2,3 alt+enter導包
val lst0 = ListBuffer[Int](1,2,3)
//創建一個空的可變列表
val lst1 = new ListBuffer[Int]
//向lst1中追加元素,注意:沒有生成新的集合
lst1 += 4
lst1.append(5)
println(lst1)
//將lst1中的元素最近到lst0中, 注意:沒有生成新的集合
println(lst0 ++= lst1)
//將lst0和lst1合并成一個新的ListBuffer 注意:生成了一個集合
println(lst0 ++ lst1)
//將元素追加到lst0的后面生成一個新的ListBuffer
val lst3 = lst0 :+ 5
println(lst3)
}
def main(args: Array[String]): Unit = {
// 構建一個可變列表,初始有3個元素1,2,3 alt+enter導包
val lst0 = ListBuffer[Int](1,2,3)
//創建一個空的可變列表
val lst1 = new ListBuffer[Int]
//向lst1中追加元素,注意:沒有生成新的集合
lst1 += 4
lst1.append(5)
println(lst1)
//將lst1中的元素最近到lst0中, 注意:沒有生成新的集合
println(lst0 ++= lst1)
//將lst0和lst1合并成一個新的ListBuffer 注意:生成了一個集合
println(lst0 ++ lst1)
//將元素追加到lst0的后面生成一個新的ListBuffer
val lst3 = lst0 :+ 5
println(lst3)
}
def main(args: Array[String]): Unit = {
/* //創建一個List
val lst0 = List(1,7,9,8,0,3,5,4,6,2)
//將lst0中每個元素乘以10后生成一個新的集合
val lst1 = lst0.map(_*10)
val lst2 = for(i <- lst0) yield i * 10
println(lst1 + ":" +lst2)
//將lst0中的偶數取出來生成一個新的集合
val lst3 = lst0.filter(_ % 2 == 0)
val lst4 = for(i <- lst0 if(i % 2 == 0)) yield i
println(lst3 + ":" +lst4)
//將lst0排序后生成一個新的集合
val lst5 = lst0.sorted//升序
val lst6 = lst0.sortWith(_>_)//降序
val lst7 = lst0.sortBy(x => x)//升序
println(lst5 + ":" +lst6 + ":" +lst7)
//反轉順序
println(lst5.reverse)
//將lst0中的元素4個一組,類型為Iterator[List[Int]]
val it = lst0.grouped(4)
// println(it.toBuffer)
//將Iterator轉換成List
val lst8 = it.toList
//將多個list壓扁成一個List
println(lst8.flatten)
//先按空格切分,再壓平
val lines = List("hello tom hello jerry", "hello jerry", "hello kitty")
lines.map(_.split(" ")).flatten
lines.flatMap(_.split(" "))//
//并行計算求和
println(lst0.par.sum)
println(lst0.par.reduce(_+_))//非指定順序
println(lst0.par.reduceLeft(_+_))//指定順序
//折疊:有初始值(無特定順序)
val lst11 = lst0.fold(100)((x, y) => x + y)
//折疊:有初始值(有特定順序)
val lst12 = lst0.foldLeft(100)((x, y) => x + y) */
//聚合
val arr = List(List(1, 2, 3), List(3, 4, 5), List(2), List(0))
// println(arr.flatten.sum)
/*先局部求和,再匯總
_+_.sum:第一個下劃線是初始值和后面list.sum和,_.sum是list的和,非并行化時只初始化1次只攜帶1次
_+_:初始值和list元素和的和 */
val result = arr.aggregate(10)(_+_.sum,_+_)
val res = arr.aggregate(10)((x,y)=>x+y.sum,(a,b)=>a+b)
println(result+":"+res)
val l1 = List(5,6,4,7)
val l2 = List(1,2,3,4)
//求并集
val r1 = l1.union(l2)
//求交集
val r2 = l1.intersect(l2)
//求差集
val r3 = l1.diff(l2)
println(r3)
}
2.4.2 Set
def main(args: Array[String]): Unit = {
//不可變Set
/* val set1 = new HashSet[Int]()
//將元素和set1合并生成一個新的set,原有set不變
val set2 = set1 + 4
//set中元素不能重復
val set3 = set1 ++ Set(5, 6, 7)
val set0 = Set(1,3,5) ++ set3
println(set0)*/
//創建一個可變的HashSet
val set1 = new mutable.HashSet[Int]()
//向HashSet中添加元素
set1 += 2
//add等價于+=
set1.add(4)
set1 ++= Set(1,3,5)
println(set1)
//刪除一個元素
set1 -= 5
set1.remove(2)
println(set1)
}
3. 類、對象、繼承和trait
3.1 類
3.1.1 類的定義
Scala中,可以在類中定義類、以在函數中定義函數、可以在類中定義object;可以在函數中定義類,類成員的缺省訪問級別是:public
//在Scala中,類不用聲明為public
//Scala源文件中可以包含多個類,所有這些類都具有公有可見性
class Person {
//val修飾的變量是只讀屬性,相當于Java中final修飾的變量,只提供get()
val id = "1"
//var修飾的變量,提供get()和set()
var age: Int = 18
//類私有字段,只有本類和本類的伴生對象可以訪問
private var name = "zs"
//對象私有字段只有本類能訪問
private[this] val pet = "xq"
}
/**(單例對象,靜態對象)
* 伴生對象:與class名相同,并且在同一個文件中*/
object Person {
def main(args: Array[String]): Unit = {
val p = new Person
// p.id = "2"http:// p.pet
println(p.id +":"+p.age+":"+p.name)
p.name = "ls"
p.age = 20
println(p.age+":"+p.name)
}
}
3.1.2 構造器
Scala主要分主構造器和輔助構造器兩種:
主構造器里面的變量會被執行,方法會被加載,調用的方法會被執行
輔助構造器(相當于重載的構造函數)不可以直接調用超類的主構造器
/**每個類都有主構造器,主構造器的參數直接放置類名后面,可以在主構造器中對字段賦值,對于主構造器中參數已賦值的在new的時候可以不再賦值
private[com.bigdata] class Study{}:只有com.bigdata或其子包下的類能訪問Stu
class Stu(name:String){}:構造參數name沒有val、var修飾
相當于private[this] name:String ,只有本類能訪問
class Stu private(name:String){}:private修飾主構造器(私有構造器),只有伴生對象能new */
class Student(val name: String, val age: Int) {
//主構造器會執行類定義中的所有語句
println("執行主構造器")
try {
println("讀取文件")
throw new IOException("io exception")
} catch {
case e: NullPointerException => println("打印異常Exception : " + e)
case e: IOException => println("打印異常Exception : " + e)
} finally {
println("執行finally部分")
}
private var gender = "male"
//用this關鍵字定義輔助構造器
def this(name: String, age: Int, gender: String){
//每個輔助構造器必須以主構造器或其他的輔助構造器的調用開始
this(name, age)
this.gender = gender
}
}
3.1.3 Class
Scala中類可以通過classOf[A]獲取類型,Object單例/伴生只能通過.getClass獲取。
classOf和getClass區別:
getClass方法得到的是Class[A]的某個子類,而classOf[A]得到是正確的 Class[A],但是去比較的話,這兩個類型是equals為true的。這種細微的差別,體現在類型賦值時,因為java里的Class[T]是不支持協變的,所以無法把一個 Class[_ < : A] 賦值給一個 Class[A]。
類型檢查和轉換:
Scala Java obj.isInstanceOf[C]:判斷obj是否屬于C類型 obj instanceof C obj.asInstanceOf[C]:轉換 (C)obj classOf[C] C.class |
3.2 對象
3.2.1 單例對象和伴生對象
1.單例對象 在Scala中沒有靜態方法和靜態字段,但是可以使用object這個語法結構來達到同樣的目的。主要作用: 1)存放工具方法和常量 2)高效共享單個不可變的實例 3)單例模式 2.伴生對象 單例對象,不需要new,用【類名.方法】調用單例對象中的方法 伴生對象 在scala的類中,與類名相同且與該類在同一個文件的對象叫伴生對象。類和伴生對象之間可以相互訪問私有的方法和屬性,但類的字段被private[this]修飾的只有本類能訪問
3.2.2 應用程序對象
Scala程序都必須從一個對象的main方法開始,可以通過擴展App特質,不寫main方法。
object AppObjectDemo extends App{
//不用寫main方法
println("Scala")
}
3.2.3 apply和unapply方法
通常我們會在類的伴生對象中定義apply方法,當遇到類名(參數1,...參數n)時apply方法會被調用。
apply方法有點類似于java中的構造函數,接受構造參數變成一個對象。
unapply方法就剛好相反,它是接收一個對象,從對象中提取出相應的值,主要用于模式匹配(后文闡述)中。
def main(args: Array[String]) {
//調用了Array伴生對象的apply方法
//def apply(x: Int, xs: Int*): Array[Int]
//arr1中只有一個元素5
val arr1 = Array(5)
//def apply[T: ClassTag](xs: T*): Array[T] = {}
var arr2 = Array[Int](8)
println(arr1.toBuffer)
//new了一個長度為5的array,數組里面包含5個null(沒有指定泛型)
var arr2 = new Array(5)
}
3.3 繼承和trait
在Scala中繼承類的方式和Java一樣都是使用extends關鍵字,繼承多個類后面有with關鍵字。
Scala中沒有接口,而是trait即特質,類似Java1.8中的接口,其中可以包含抽象方法也可以有已實現的方法。
在Scala中重寫一個非抽象的方法(沒有被實現)必須使用override修飾符,抽象方法可以使用也可以不使用override。
trait Flyable {
def fly(): Unit = { println("I can fly") }
}
abstract class Animal {
def run(): Int
val name: String
}
/*class Human extends Animal with Flyable
class Human extends Flyable with其他trait(特質)
*/
class Human extends Animal with Flyable {
val name = "lz"
//t1、t2、、tn打印n次ABC,t1=(1, 2, 3,4)(a->1,b->2,c->3,d->4)
val t1, t2,t3, (a, b, c,d) = {
println("ABC")
(1, 2, 3,4)
}
//scala中重寫一個非抽象方法必須使用override關鍵字
override def fly(): Unit = { println("123") }
//重寫抽象方法可以使用也可以不使用override關鍵字
def run(): Int = { 1 }
}
object Main {
def main(args: Array[String]): Unit = {
val h = new Human
println(h.t1+":"+h.a+":"+h.b+":"+h.c+":"+h.t2)
}
}
4. 模式匹配和樣例類
4.1 模式匹配
Scala有一個十分強大的模式匹配機制,可以應用到很多場合:如替代Java中的switch語句、類型檢查等。
并且Scala還提供了樣例類,對模式匹配進行了優化,可以快速進行匹配。
// 1. 匹配字符串
import scala.util.Random
object Case01 extends App {
val arr = Array(1, 2, 3)
val i = arr(Random.nextInt(arr.length))
i match {
case 1 => {println(i)}
case 2 => {println(i)}
//下劃線_:代表匹配其他所有類似于switch語句中default
case _ => {println(i)}
}
}
// 2. 匹配類型
import scala.util.Random
object Case02 extends App {
val arr = Array("a", 1, -2.0, Case02)
val elem = arr(Random.nextInt(arr.length))
println(elem)
elem match {
case x:Int => {println("Int"+x)}
//模式匹配時可以加守衛條件,如果不符合守衛條件,走case_
case y:Double if(y>=0) => println("Double "+ y)
case Case02 => {println("Int"+Case02)}
case _ => {println("default")}
}
}
// 3. 匹配數組
object Case03 extends App {
/* val arr = Array(0, 3, 5)
arr match {
case Array(1,x,y) => println(x+":"+y)
case Array(0) => println("only 0")
//表示匹配首元素是0的數組
case Array(0,_*) => println("0 ...")
case _ => println("else")
}*/
/* val lst = List(0,-1,1,2)
//head首元素,tail除首元素之外的元素 take從1開始取
println(lst.head+":"+lst.tail+":"+lst.take(1))
lst match {
//首元素0,Nil代表空列表
case 0 :: Nil => println("only 0")
//只有兩個元素
case x :: y :: Nil => println(s"x:$x--y:$y")
case 0 :: x => println(s"0...$x")//head和tail
case _ => println("else")
}*/
val tup = (-1.2, "a", 5)
tup match {
//元組有幾個元素,case后跟的元組也要有幾個元素
case (1, x, y) => println(s"hello 123 $x , $y")
case (_, z, 5) => println(z) //前兩個元素為任意值
case _ => println("else")
}
val lst1 = 9 :: (5 :: (2 :: Nil))
val lst2 = 9 :: 5 :: 2 :: List()
println(lst2+":"+lst1)//952:952
}
4.2 樣例類
可用于模式匹配、封裝數據(多例)。case class多例,后面跟構造函數;case object是單例的:
import scala.util.Random
case class Task(id:String)
case class HeartTime(private val time:Long)//構造case class可new可不new
case object CheckTimeOut
object Main {
def main(args: Array[String]) {
val arr = Array(CheckTimeOut, HeartBeat(88888), Task("0001"))
val a = arr(Random.nextInt(arr.length))
a match {
case Task(id) => {println(s"$id")}//取id值 固定寫法
case HeartTime(time) => {println(s"$time")}
case CheckTimeOutTask => {
println("check")
}
//匹配其他情況
case _ => prinln("do something")
}
}
}
5. 高階函數
Scala中的高階函數包含:作為值的函數、匿名函數、閉包、柯里化等,可以把函數作為參數傳遞給方法或函數。
5.1 作為值的函數
定義函數時格式:val 變量名 = (輸入參數類型和個數) => 函數實現和返回值類型和個數。
=:表示將函數賦給一個變量
=>:左面表示輸入參數名稱、類型和個數,右邊表示函數的實現和返回值類型和參數個數

5.2 匿名函數
在Scala中,你不需要給每一個函數命名,沒有將函數賦給變量的函數叫做匿名函數

5.3 方法轉換為函數
在Scala中,法和函數是不一樣的,最本質的區別是函數可以做為參數傳遞到方法中,方法可以被轉換成函數。
5.4 柯里化
柯里化指的是將原來接收多個參數的方法或函數變成新的接收一個一個的參數的方法的過程。
5.5 隱式轉換
對類進行增強,關鍵字implicit。如Int沒有to(),而RichInt有to(),我們只需要在某個地方將Int轉為RichInt,然后在用的地方import隱式轉換就可以直接使用to(),示例:

除了上述介紹的語法之外,像協變、逆變、actor也需要大家掌握。
至于akka,如果大家使用的是老版本Spark,如Spark1.X,也建議結合actor好好學習,Spark老版本通信框架是用akka和netty結合的,當然后面完全是用netty了。