學習路上的新起點:大數據Scala + Spark +(HDFS + HBase),本文主要介紹下Scala的基本語法和用法吧。最后再簡單介紹一種Java開發工具IntelliJ IDEA的使用。
Scala
Scala語言是一種面向對象語言,結合了命令式(imperative)和函數式(functional)編程風格,其設計理念是創造一種更好地支持組件的語言。
Object-Oriented Meets Functional
特性
- 多范式(Multi-Paradigm)編程語言,類似Java、C#;
- 繼承面向對象編程和函數式編程的特性;
- 面向對象:[1]. 子類繼承,[2]. 混入(Mixin)機制;
- 函數式:支持高階函數、嵌套函數,模式匹配,支持柯里化(Currying);
- 類型推斷(Type Inference):根據函數體中的具體內容推斷方法的返回值類型
- 更高層的並發模型,可伸縮;
- 靜態類型,編譯時檢查;
- 允許與Java互操作(將來會支持與.Net互操作);
與Java對比
- 如果用作服務器端開發,Scala的簡潔、靈活性是Java無法比擬的;
- 如果是企業級應用開發、Web開發,Java的強大是Scala無法匹敵的;
Scala和Groovy
Groovy的優勢在於易用性以及與Java無縫銜接,更注重實效思想,腳本化;
Scala的優勢在於性能和一些高級特性,靜態類型,更具有學術思想;
關於Lift
一個使用了Scala的開源的Web應用框架,可以使用所有的Java庫和Web容器。其他框架:Play、Bowler等。
推薦:Play Framework 和 Slick,具體學習待續... ...
- Play:The High Velocity Web Framework For Java and Scala
- Slick:Functional Relational Mapping for Scala
環境
Scala SDK: scala-2.10.4.msi
安裝路徑最好不要帶空格和中文字符,否則,在 cmd 命令行窗口執行 scala -version 會報錯 此時不應有 \scala\bin\scala.bat) 的問題
安裝完成后,確保 ..\lib 目錄下包含如下文件:
scala-compiler.jar, scala-library.jar, scala-reflect.jar
注意,.msi 版本是安裝版,.zip 解壓即可。
在 cmd 命令行中,測試:
Note:本地可以安裝多個版本的 Scala SDK,以 scala 2.11 為例:
// scala-2.11.8.zip Scala_2.11_Home:D:\scala-2.11.8 Path:%Path%; %Scala_2.11_Home%\bin
使用時,用哪個SDK,在Path變量中就將哪個SDK放在前面即可。
基礎語法問題
Scala程序的執行入口是提供main方法的獨立單例對象。每個Scala函數都有返回值,每個Scala表達式都有返回結果。
在Scala中沒有靜態的概念,所有東西都是面向對象的。其實object單例對象只是對靜態的一種封裝而已,在.class文件層面,object單例對象就是用靜態(static)來實現的。
一切皆為對象
Scala中所有的操作符都是方法,操作符面向的使用者都是對象
Scala編程的一個基本原則上,能不用var,盡量不用var,能不用mutable變量,盡量不用mutable變量,能避免函數的副作用,盡量不產生副作用。
- 對於任何對象,如果在其后面使用(),將調用該對象的apply方法,arr(0) == arr.apply(0)
- 如果對某個使用()的對象賦值,將該對象的update方法,arr(0)=value == arr.update(0,value)
如果一個方法只有一個參數,可以不用 括號和. 來調用這個方法。
字段 + 方法 + 函數
scalac編譯器會為類中的var字段自動添加setter和getter方法,會為類中的val方法自動添加getter方法。 其中,getter方法和字段名相同。
在源碼中,所有對字段的顯示訪問,都會在class中編譯成通過getter和setter方法來訪問。
方法作用於對象,方法是對象的行為。方法定義格式:
def 方法名稱(參數列表):返回值 = { 方法體 }
函數定義格式:
val 函數名稱 = { (參數列表) => { 函數體 } },即 val 函數名稱 = { Lambda表達式 } 或 val 函數名稱 : (入參類型 => 出參類型) = { 入參 => 函數體 }
方法名意味着方法調用,函數名只代表函數本身。
關於方法(Method)和函數(Function)
- 函數是一等公民,使用val語句可以定義函數,def語句定義方法;
- 函數是一個對象,繼承自FuctionN,函數對象有curried,equals,isInstanceOf,toString等方法,而方法不具有;
- 函數是一個值,可以給val變量賦值,方法不是值、也不可以給val變量賦值;
- 通過將方法轉化為函數的方式 method _ 或 method(_) 實現給val變量賦值;
- 若 method 有重載的情況,方法轉化為函數時必須指定參數和返回值的類型;
- 某些情況下,編譯器可以根據上下文自動將方法轉換為函數;
- 無參數的方法 method 沒有參數列表(調用:method),無參數的函數 function 有空列表(調用:function());
- 方法可以使用參數序列,轉換為函數后,必須用 Seq 包裝參數序列;
- 方法支持默認參數,函數不支持、會忽略之;
參考:學習Scala:Scala中的字段和方法; Scala中Method和Function的區別 - 簡書;
參數
- 按名傳參:By-name parameter,def fun(param: => T)。Evaluated every time it's used within the body of the function
- 按值傳參:By-value parameter。Evaluated before entry into the function/method
推薦將按值傳參(def fun(msg: String))改為按名稱傳參(def fun(msg: =>String))。這樣參數會等到實際使用的時候才會計算,延遲加載計算,可以減少不必要的計算和異常。
case class
case class PubInterfaceLog(interfaceId: Int, busiType: String, respTime: Long, publicId: Long)
最重要的特性是支持模式匹配,Scala官方表示:
It makes only sense to define case classes if pattern matching is used to decompose data structures.
其他特性如下:
- 編譯器自動生成對應的伴生對象和 apply() 方法;
- 實例化時,普通類必須 new,而 case class 不需要;
- 主構造函數的參數默認是 public 級別,默認在參數為 val;
- 默認實現了 equals 和 hashCode,默認是可以序列化的,也就是實現了Serializable,toString 更優雅;
- 默認生成一個
copy
方法,支持從實例 a 以部分參數生成另一個實例 b;
apply() 方法
apply方法是Scala提供的一個語法糖
- 類名+括號,調用對象的apply方法
- 對象名+括號,調用類的apply方法
對apply方法的簡單測試:(其中,帶 new -- class ApplyTest,不帶 new -- object ApplyTest)
class ApplyTest { println("class ApplyTest") def apply() { println("class APPLY method") } } object ApplyTest { println("object ApplyTest") def apply() = { println("object APPLY method") new ApplyTest() } } // 對象名+括號,調用類的apply方法 val a1 = new ApplyTest() a1() // == a1.apply() // 輸出 class ApplyTest, class APPLY method // 類名+括號,調用對象的apply方法 val a2 = ApplyTest() // 輸出 object ApplyTest, object APPLY method, class ApplyTest val a2 = ApplyTest() a2() // 輸出 object ApplyTest, object APPLY method, class ApplyTest, class APPLY method val a3 = ApplyTest // 輸出 object ApplyTest val a3 = ApplyTest a3() // 輸出 object ApplyTest, object APPLY method, class ApplyTest
幾個零碎的知識點
- Array 長度不可變, 值可變;Tuple 亦不可變,但可以包含不同類型的元素;
- List 長度不可變, 值也不可變,::: 連接兩個List,:: 將一個新元素放到List的最前端,空列表對象 Nil
- _:通配符,類似Java中的*,下划線 "_ " 的用法總結
- _=:自定義setter方法;
- _*:參數序列化,將參數序列 Range 轉化為 Seq
- i to j : [i, j]; i until j : [i, j)
- lazy val表示延遲初始化,無lazy var
映射 + 對偶 + 元組
映射由對偶組成,映射是對偶的集合。對偶即鍵值對,鍵->值,(鍵, 值),是最簡單的元組。元組是不同類型的值的聚集。
映射是否可變表示整個映射是否可變,包括元素值、映射中元素個數、元素次序等:
- 不可變映射:直接用(scala.collection.mutable.)Map聲明,維持元素插入順序,支持 += 、-=;
- 可變映射:用scala.collection.immutable.Map聲明,不維持元素插入順序,支持 +、-;
構造器
(1)主構造器
class Student(var ID : String, var Name : String) { println("主構造器!") }
- 主構造器直接跟在類名后面,主構造器的參數最后會被編譯成字段
- 主構造器執行時,會執行類中所有的語句
- 如果主構造器參數聲明時不加val或var,相當於聲明為private級別
(2)從構造器
class Student(var ID : String, var Name : String) { println("主構造器!") var Age : Int = _ var Address : String = _ private val Email : String = Name + ID + "@163.com" println("從構造器!") def this(ID:String, Name:String, Age:Int, Address:String) { this(ID, Name) this.Age = Age this.Address = Address } }
- 從構造器定義在類內部,方法名為this
- 從構造器必須先調用已經存在的主或從構造器
集合
Scala語言的一個設計目標是可以同時利用面向對象和面向函數的方法編碼,因此提供的集合類分成了可以修改的集合類和不可以修改的集合類兩大類型。Scala除了Array和List,還提供了Set和Map兩種集合類。
- 通過 scala.collection.JavaConversions.mapAsScalaMap 可將 Java 的 Map 轉換為 Scala 類型的 Map;
- 通過 scala.collection.JavaConversions.mapAsJavaMap 可將 Scala 的映射轉換為 Java 類型的映射;
- toMap 方法將對偶集合轉化為映射
一致目標:所有皆為對象 + 函數式編程,在變量和函數返回值可能不會引用任何值的時候使用 Option 類型
- 在沒有值的時候,使用None;
- 如果有值可以引用,使用Some來包含這個值;
None 和 Some 均為 Option 的子類,但是 None 被聲明為一個對象,而不是一個類.
伴生對象
當單例對象(object Name,Singleton Object)與類(class Name)同名時,該單例對象稱作類的伴生對象(Companion Object),該類稱作單例對象的伴生類。沒有伴生類的單例對象稱為孤立對象(Standalone Object),最常見的就是包含程序入口main()的單例對象。
- 同名且在同一個源文件中定義
- 可以互相訪問其私有成員
- 伴生對象的屬性、方法指向全局單例對象
MODULE$
伴生類的屬性、方法被定義為是對象相關的
單例對象在第一次訪問時初始化,不可以new、不能帶參數,而類可以new、可以帶參數。
- 伴生對象中定義的字段和方法, 對應同名.class類中的靜態方法,對應同名$.class虛構類中的成員字段和方法
- 伴生類中定義的字段和方法, 對應同名.class類中的成員字段和成員方法
同名.class類中的靜態方法,會訪問單例的虛構類的實例化對象, 將相關的邏輯調用轉移到虛構類中的成員方法中,即:.class類提供程序的入口,$.class提供程序的邏輯調用。
- 伴生對象中的邏輯,轉移到$.class虛構類中去處理
- 伴生類中的邏輯,轉移到.class同名類中的成員方法中去處理
通過伴生對象來訪問伴生類的實例,提供了控制實例個數的機會。虛構類的單例性,保證了伴生對象中信息的唯一性。
那么:伴生對象如何體現單例呢?
因為伴生類不是單例的,如何實現單例模式呢?方法2種:
(1)將伴生類中的所有邏輯全部移到單例對象中,去除伴生類, 單例對象成為孤立對象, 該孤立對象天然就是單例的
(2)若必須存在伴生類,如何保證伴生類是單例的? 將伴生類的主構造器私有, 並在伴生對象中創建一個伴生類的對象, 該對象就是唯一的
class A private { } object A { val single = new A() } // true var a1 = A.single var a2 = A.single; println("a1 eq a2 : " + (a1 eq a2))
參考:學習Scala:孤立對象的實現原理; 學習Scala:伴生對象的實現原理;
Trait
特質,類似有函數體的 Interface,利用關鍵字 with 混入。
IO讀寫
Scala本身提供 Read API,但是 Write API 需要利用Java的API,文件不存在會自動創建。
(1)Read
import scala.io.Source val content = Source.fromURL(url).getLines() // 網絡資源 val lines = Source.fromFile(filePath).getLines() // 文件(2)Write
import java.io.File import java.io.PrintWriter import java.io.FileWriter import java.io.BufferedWriter // 文件首次寫入,ok; 不支持追加 val writer = new PrintWriter(new File("sqh.txt")) writer.println("xxx") // 支持追加;但是有換行問題 val writer = new FileWriter(new File("sqh.txt"), true) writer.write("xxx") // 支持追加,換行解決方法1 val writer = new BufferedWriter(new FileWriter(new File("sqh.txt"), true)) writer.write("xxx") writer.newLine() // 支持追加,換行解決方法2 val writer = new PrintWriter(new FileWriter(new File("sqh.txt"), true)) writer.println("xxx") writer.flush() writer.close注意,關閉流前,先刷新。PrintWriter 不支持追加,FileWriter 支持追加,但存在換行問題。可以將 FileWriter 包裝成 BufferedWriter 或 PrintWriter 解決換行問題。
參考
關於IntelliJ Idea的使用,參見:https://www.cnblogs.com/wjcx-sqh/p/11129718.html