1. 程式人生 > >spark學習筆記一:scala語言基礎

spark學習筆記一:scala語言基礎

這篇文章是《scala程式設計》的筆記。

Scala基於java,是一種函數語言程式設計+程序式程式設計的混合語言。

可以使用直譯器互動執行,也可以編譯成jar包。

變數

Scala 有兩種變數, val (引用不可變)和 var(引用可變)

變數的定義和賦值語句是:

val msg2: java.lang.String = "Helloagain, world!"

msg2是變數名,java.lang.String是變數型別,用冒號分隔,等號後邊是變數值。

以上賦值語句可以簡化為如下所示,因為在Scala程式裡java.lang型別的簡化名都可使用:

val msg3: String = "Hello yet again,world!"

由於scala有型別推斷: type inference能力,Scala能自動理解你省略的型別的能力:

val msg = "Hello, world!"

scala的賦值語句不像java一樣返回變數本身,而返回的是一個空值(Unit,類似於java中的void)

函式

函式定義

函式定義的語句是:

def max(x: Int, y: Int): Int = {

if (x > y) x

else y
}

def是函式定義關鍵字,max(x: Int,y: Int)是函式名和引數,可以看出,scala的變數和變數名的組合都是 變數名:變數型別。:int是返回值型別。函式定義中,最後一條語句的返回值作為函式的返回值,所以不需要return語句。

如果函式是遞迴的、或者使用了return,你就必須顯式地定義函式結果型別。然而在max的例子裡,你可以不用寫結果型別,編譯器也能夠推斷它,同樣,如果函式僅由一個句子組成,你可以可選地不寫大括號。這樣,你就可以把max函式寫成這樣:

defmax2(x: Int, y: Int) = if (x > y) x else y

對於沒有返回值的函式,返回值型別應該指定為:Unit(大寫的U)。 Scala 的 Unit 型別比較接近 Java 的 void 型別,而且實際上 Java 裡每一個返回 void 的方法都被對映為 Scala 裡返回 Unit 的方法。

函式的定義可巢狀:

def fun1():Unit = {

         def fun2():Unit = {

}

fun2

}

fun2只在fun1中可見。

函式定義中的=不可少,如果沒有=的話,scala會預設返回型別是Unit,這樣,函式真正要返回的東西就被轉換為Unit型別了。這樣的函式被稱名過程,如下所示:

def fun1(){printnl(this is a procedure)}

另一種定義一種函式的方式是函式文字: function literal,這是匿名函式的定義方式,如下所示:

arg=> println(arg)

=>左邊的arg代表的是引數列表,如果有多個引數,就應該是(arg1,arg2)的形式,各個引數可以跟上型別,如arg:Int這樣。=>右邊代表的是函式體。函式文字一般用在需要傳入函式作為引數的函式呼叫裡。比如,foreach(下面會介紹),使用foreach的方法就是args.foreach(arg=> println(arg)),傳入了一個函式文字,代表了對args(是個陣列)每一個元素的處理方式。當然,既然要傳入的是函式,也可以寫成args.foreach(println),這其實是println_的簡寫方式。

函式注意事項

函式可以帶有可變數量的引數,如:

scala>def echo(args: String*) =

for (arg <- args) println(arg)

args 的型別實際上是 Array[String],如果引數要傳入給這個echo,應該這樣echo(arr:_*)。

以字首“unary_”開頭的函式或方法,可以把操作資料放在操作符右邊。如Float類的unary_-()函式,其對應的是一元操作符-,使用的時候可以是-2.0這樣。

函式呼叫

呼叫函式的方式:

1.      func(arg1,arg2)

2.      呼叫某物件的某個只帶一個引數的函式時,如obj.fuc(arg),可以寫成這種形式:obj fuc arg,這在for語句裡特別有用,可以for (i <- 0 to 2){}這樣。

使用函式

函式的使用:

作為引數時,函式型別的表示方式如下:

def  func(f:()  =>  String)  =  println(f())

其中,引數f的型別是()=>String(這個很像函式文字的形式,但不是函式文字),代表f是一個函式,無輸入引數,有一個String型別的輸出。

函式不可以直接賦值給變數,如果要把函式賦值給變數,需要用到偏應用化技術。

需要使用函式時,有三種方式:

1.      直接傳入函式名,如args.foreach(println)

2.      使用函式檔案,如args.foreach(x=>println(x))

3.      使用偏應用函式,如args.foreach(println _),這個在之後介紹。

Scala裡陣列的第一個元素是array(0),不是像Java裡的arrya[0]。

偏應用函式

首先說明佔位符概念,為了使函式文字更簡潔,可以把下劃線當做一個或更多引數的佔位符,只要每個引數在函式文字內僅出現一次。比如,sum _ 等價於arg => sum(arg)。

偏應用函式,就是使用佔位符’_’來代佔位一個或多個引數。

如果你正在寫一個省略所有引數的偏應用程式表示式,如 println _或 sum _, 而且在程式碼的那個地方正需要一個函式,你可以去掉下劃線從而表達得更簡明,比如:

someNumbers.foreach(println_)

你可以寫成:

someNumbers.foreach(println)

最後一種格式僅在需要寫函式的地方,如例子中的 foreach 呼叫,才能使用。編譯器知道這種情異常況需要一個函式,因為 foreach 需要一個函式作為引數傳入。在不需要函式的情況下,嘗試使用這種格式將引發一個編譯錯誤,就如:val c = sum

閉包

可以這麼認為:閉包就是函式+引用環境,這個引用環境可能包含了函式中用到的自由變數的引用。比如:

scala>var more = 1

more:Int = 1

scala>val addMore = (x: Int) => x + more

現在,這個addMore就是一個閉包,在需要使用函式的時候,可以把addMore傳入,addMore這個物件引用環境中,more值一直是1,就算你在後文中把more重新賦值,addMore中的more也還是引用的原來那個1。

函式柯里化

即把有多個引數的函式的定義,如:

def  sum(x:Int,y:Int)  =  x  +  y

改寫成如下的形式:

def  sum2(x:Int)(y:Int)  =  x  +  y

這實際上等價為如下的函式定義方式:

def  first(x:Int)=(y:Int)  =>  x+y

可見,如果呼叫first(1),返回的是一個閉包,這個閉包的函式相當於:(y:Int)  =>  1+y。

柯里化的作用,是複用函式。比如,如果呼叫var sumWith1 = sum2(1); val sum = sumWith1(1),那麼,sum2(1)這個閉包就被複用了。

異常

產生異常和方式和java一樣:

thrownew RuntimeException("n must be even")

捕獲異常的方式有些不同:

try {

val f = newFileReader("input.txt")

// Use and close file

}catch {

case ex: FileNotFoundException => //Handle missing file

case ex: IOException => // Handle otherI/O error

}finally {

}

try-catch-finally也產生返回值,返回的值是try{}語句塊的返回值(沒異常的話)或對應異常的case語句塊的返回值,finally的返回值是會被拋棄的,因為finally只應該做一些清理的工作。

內建語法和結構

if的用法

if(k>10) {

     println("k is more than 10")

} elseif(k<0) {

     println("k is less than 0")

} else{

         println("equal")

}

If是個表示式,它本身是有返回值的,返回的值是符合條件的分支語句的最後一個表示式。

Match的用法

Match類似switch語句,如下所示:

firstArgmatch {
case "salt" => println("pepper")
case "chips" => println("salsa")
case "eggs" => println("bacon")
case _ => println("huh?")
}

與switch的區別是,case後面可跟任何型別的常量;分支後沒有break(隱含了break);而且match可以返回值,返回的值是分個分支的返回值。

While的用法

while (i< args.length) {

println(args(i))

i += 1

}

Do{

}while()

While不是表示式,沒有返回值。

Foreach的用法

args.foreach(arg=> println(arg))

引數需要傳入帶一個引數的函式,這個引數將應用在args的每個元素上。

for的用法

for (i<- 0 to 2){}

for (i<- array){}

for (i<- array if i < 0; if i > -10)

for {

file <- filesHere

iffile.getName.endsWith(".scala")

line <- fileLines(file)

trimmed =line.trim
iftrimmed.matches(pattern)

} println(file+ ": " + trimmed)

def scalaFiles=

for {

file <- filesHere

iffile.getName.endsWith(".scala")

} yield {file}

0 to 2實際上產生的是一個數組,它會被scala直譯器翻譯成0.to(2)。如果你不想包括被列舉的Range的上邊界,可以用until替代to。

把if子句加到for括號裡可以實現過濾器: filter,過濾出符合條件的值。多個過濾條件用分號分隔。

還可以使用巢狀列舉,即加入多個<-子句(各個<-子句可以跟隨過濾器)。如果願意的話,你可以使用大括號代替小括號環繞發生器和過濾器。使用大括號的一個好處是你可以省略一些使用小括號必須加的分號。

上例中,名為 trimmed 的變數被從半當中引入 for 表示式,並被初始化為 line.trim 的結果值。之後的for表示式就可以在兩個地方使用這個新變數,一次在if中,一次在 println中,這種技術被稱為mid-stream(流間)變數繫結。

只要在 for之前加上關鍵字 yield,如上所示,每次迴圈產生的值被組合成一個array。這會使用for產生一個返回值。

Array(陣列)的使用

valgreetStrings = new Array[String](3)

greetStrings(0)= "Hello"

Array是陣列關鍵字,方括號和小括號共同引數化: parameterize了陣列greetStrings,代表陣列元素型別是String,長度為3。

陣列使用小括號來引入指定下標的元素,greetStrings(0)實際上會被直譯器翻譯成greetString.apply(0),所以這是apply函式的簡寫方式。

當對帶有括號幷包括一到若干引數的變數賦值時,編譯器將把它轉化為對帶有括號裡引數和等號右邊的物件的 update 方法的呼叫:

greetStrings(0)= "Hello"

會被直譯器翻譯為:

greetStrings.update(0,"Hello")

陣列的初始化可以寫成:

valnumNames = Array("zero", "one", "two")

這個Array和上面的newArray實際上不是一個東西,這裡的Array實際上是Array類的伴生物件,也就是說它是一個object,並且“伴生”(伴隨的是Array類)的。所以以上語句相關於是:

valnumNames = Array.apply("zero", "one", "two")

List(列表)的使用

List基本上和Array一樣,區別是List物件所引用的實體不可變(並不是引用不可變,而是引用的東西本身不可變)。

建立List的方法和Array一樣:

valoneTwoThree = List(1, 2, 3)

List有個叫“ ::: ”的方法實現疊加功能:

valoneTwo = List(1, 2)
val threeFour = List(3, 4)
val oneTwoThreeFour = oneTwo ::: threeFour

List 最常用的操作符是發音為“ cons”的‘:: ’。 Cons 把一個新元素組合到已有 List的最前端,然後返回結果 List。 例如:

valtwoThree = list(2, 3)

valoneTwoThree = 1 :: twoThree

valoneTwoThreeFour = 1 :: 2 :: 3 :: 4 :: Nil

如果一個方法被用作操作符標註,如a* b,那麼方法被左運算元呼叫,就像 a.*(b)。除非方法名以冒號結尾。這種情況下,方法被右運算元呼叫。因此, 1 :: twoThree 裡, :: 方法被twoThree呼叫,傳入1,像這樣:twoThree.::(1)。

Tuple(元組)的使用

一般的建立方法是:

valpair = (99, "Luftballons")

println(pair._1)

valsix = ('u', 'r', 'the', 1, 4, "me")

pair就是一個二元元組,它的型別是Tuple2[Int, String]。Six是一個六元元組,它的型別是Tuple6[Char, Char,String, Int,Int, String]。Scala目前最多支援Tuple22。Pair._1取得元組的第一個元素,注意下標是從1開始,這和Array及List不同,這是因為因對於擁有靜態型別元組的其他語言,如Haskell和ML,從1開始是傳統的設定。

之所以用pair._1的方式取用元素,而不是pair(0)這樣,是因為List的apply方法始終返回同樣的型別,但是元組裡的或許型別不同。_1 可以有一個結果型別,_2是另外一個。

Set的使用

對特質(java中的介面)的擴充套件(java中的實現)關係如下圖所示:



可見,set有兩類:可變集和不可變集。一般使用方法如下:

varjetSet = Set("Boeing", "Airbus")

jetSet+= "Lear"

println(jetSet.contains("Cessna"))

預設建立的是不可變集,除非import scala.collection.mutable.Set。

+=這個操作,對於不可變集,直譯器翻譯成了jetSet = jetSet + "Lear",返回的是一個新的set;對於可變集,+=是一個函式,直譯器翻譯成jetSet.+=(“Lear”)。

Map的使用

對特質的擴充套件關係如下圖所示:



與set一樣分為可變map和不可變map兩類。一般使用方法如下:

importscala.collection.mutable. Map

valtreasureMap = Map[Int, String] ()

treasureMap+= (1 -> "Go to island.")

treasureMap+= (2 -> "Find big X on ground.")

treasureMap+= (3 -> "Dig.")

預設建立的是不可變map,除非importscala.collection.mutable.Map。使用->和+=方法把鍵/值對新增到Map裡。->方法可以呼叫Scala程式裡的任何物件,並返回一個包含鍵和值的二元元組。

其他

Scala使用單引號表示char型別,使用雙引號表示java.lang.String型別。

Scala的==操作符已經被仔細地加工過,首先檢查左側是否為null,如果不是,呼叫equals方法。由於equals是一個方法,因此比較的精度取決於左手邊的引數。又由於已經有一個自動的null檢查,因此你不需要手動再檢查一次了。例如,以下的表示式返回都是true:

List(1,2, 3) == List(1, 2, 3)

1 ==1.0

("he"+ "llo") == "hello"

變數的作用範圍和java類似。

函式式(functional)風格程式設計方式的一些概念

函式式語言有First-ClassFunction的特性,即函式是該語言中的基本物件。或者說,該語言中物件的最基本形態是函式。也就是函式物件,或者說,函式是第一等級/頭等的物件,其餘的物件繼承自函式。比如Java語言,所有的物件都是Object類的例項。不存在First-Class Function。而Javascript語言中,所有的物件的原型都是Function物件。具有First-Class Function的特性。

把函式當作變數(物件)進行操作

嘗試不用任何 var 程式設計

寫沒有副作用的函式(副作用就是除了返回某個值以外,還會對上下文的環境有影響,如改變某全域性變數)。比如,就算要列印東西,也可以也返回字串,由呼叫函式來列印。

沒有break和continue的語句,這兩個特性可以使用其他的方式來替代。

鼓勵使用遞迴呼叫,因為這是函數語言程式設計的一大特徵,在遞迴呼叫函式在函式體中最後一個被呼叫時,編譯器可以優化成迭代的形式。

定義如下:

class GetSum(arg1:Int, arg2:Int) {

require(arg1 != 0)

         val mem1 = 0

         val mem2 = 1

private var sum = mem1 + mem2

def this(n: Int) = this(n, 1)

def func(): Int = {

         sum

}

}

Scala 編譯器將把你放在類內部的任何不是欄位的部分或者方法定義的程式碼,編譯進主構造器。

Scala 裡主構造器之外的構造器被稱為從構造器:auxiliary constructordef,如:

this(n:Int) = this(n, 1)

Scala 裡的每一個從構造器的第一個動作都是呼叫同一個類裡面其他的構造器。換句話說就是,每個 Scala 類裡的每個從構造器都是以“this(...) ”形式開頭的。被呼叫的構造器既可以是主構造器,也可以是從文字上來看早於呼叫構造器的其它從構造器。這個規則的根本結果就是每一個 Scala 的構造器呼叫終將結束於對類的主構造器的呼叫。因此主構造器是類的唯一入口點。

預設情況下,使用者定義的類繼承了定義在java.lang.Object 類上的 toString 實現。toString的返回值將用於需要列印這個類的場景。預設只是列印類名,一個@符號和一個十六進位制數。toStrin的結果主要是想通過提供可以用在除錯時的語句列印,日誌訊息,測試錯誤報告和直譯器,除錯器輸出的資訊來嘗試對程式設計師提供幫助。過載這個函式即可實現自定義的輸出。

Required是主構造器中的先決條件: precondition。Require在輸入的表示式為false時將通過丟擲 IllegalArgumentException 來阻止物件被構造。

類中的this關鍵字指向當前物件例項。

如果要讓直譯器自動在某些場合完成型別轉換,比如在1 + new GetSum(1,2)時候把1轉換成GetSum的物件,可以使用隱式轉換:

implicitdef intToGetSum(x: Int) = new GetSum(x)

請注意隱式轉換要起作用,需要定義在作用範圍之內。如果你把隱式方法定義放在類之內,它就不在直譯器的作用範圍。

object(單例物件)

Scala 有單例物件:singleton object。除了用 object 關鍵字替換了 class 關鍵字以外,
單例物件的定義看上去就像是類定義:

當單例物件與某個類共享同一個名稱時,他被稱作是這個類的伴生物件: companion object。你必須在同一個原始檔裡定義類和它的伴生物件。類被稱為是這個單例物件的伴生類: companion class。類和它的伴生物件可以互相訪問其私有成員。Scala的main函式就是定義在單例物件裡。