
大數(shù)據(jù)開發(fā)中常用 Scala 進行功能開發(fā),而且大數(shù)據(jù)處理和計算框架 Flink 和 Spark 都是基于 Scala 開發(fā)的,學習 Scala 不僅是進行業(yè)務開發(fā)的前提,而且是深入研究大數(shù)據(jù)前言技術(shù)的基礎。
學習環(huán)境為Linux,以下內(nèi)容均以Linux為基礎, Mac 環(huán)境與 Linux 類似,Windows的話自行研究吧,一般不使用。下面記錄的是一些學習筆記,供自己翻閱和其他需要的人參考,建議參考其他Scala完整教程進行詳細深入學習。
Scala入門
Scala安裝
學習Scala需要Java環(huán)境,所以需要先安裝JDK,關于JDK的安裝,這里不再說明,直接下載并設置環(huán)境變量即可。
學習Scala的目的就是進行Spark程序開發(fā),因為Spark是基于Scala進行開發(fā)的,而且Scala編譯出的程序也運行在JVM上,與Java代碼可以進行相互調(diào)用,但是Scala語法更加靈活,Java也在不斷改進語法,向Scala和其他語言的優(yōu)秀設計思想學習。
這里直接通過IDE的方式進行學習,這也是Scala官網(wǎng)推薦的方法,這里IDE使用的是IDEA社區(qū)版,可以下載Scala插件包,很簡單,Google一下就能搞定。
安裝就這樣把!
不過也可以單獨下載到本地,解壓后執(zhí)行,就可以進入到命令行模式中,命令行模式跟python解釋器的操作有點像,但二者還是有區(qū)別的。
python是解釋型語言,程序是邊解釋邊運行的,但是Scala是編譯性語言,是要先編譯再執(zhí)行的,命令模式下也是快速進行編譯后執(zhí)行的,這是與python本質(zhì)不同的地方。
Scala命令行模式初體驗
下載了個scala安裝包,然后添加環(huán)境變量后啟動:
? ~ scala
Welcome to Scala 2.12.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_201).
Type in expressions for evaluation. Or try :help.
scala> 8 * 5
res0: Int = 40
Scala解釋器讀取一個表達式進行求值,然后打印出結(jié)果,再繼續(xù)接收下一個表達式,這個過程叫做讀取(Read)->求值(Eval)->打印(Print)->循環(huán)(Loop),也就是REPL。
其實說Scala是解釋器并不準確,因為它不像Python那樣是解釋型語言,它接受的表達式都是要編譯成字節(jié)碼然后交給Java虛擬機運行的,所以大部分Scala程序員更傾向于稱它為REPL。
Scala常用類型
| type | range |
|---|---|
| Byte | 8bit |
| Short | 16bit |
| Int | 32 |
| Long | 64 |
| Char | 16 |
| String | 字符序列 |
| Float | 32位IEEE754單精度浮點數(shù) |
| Double | 64位IEEE754單精度浮點數(shù) |
| Boolean | true or false |
| Unit | 無值 |
| Null | null或空引用 |
| Nothing | Nothing類型在Scala的類層級的最低端;它是任何其他類型的子類型 |
| Any | 所有其他類的超類 |
| AnyRef | 所有引用類的基類 |
Unit和其他語言中void等同,用作不返回任何結(jié)果的方法的結(jié)果類型,Unit只有一個實例值,寫成()。
與Java不同的是這些類型都是類,沒有Java中的基本類型,在Scala中可以直接對這些基礎類型調(diào)用方法。Scala并不刻意區(qū)分基本類型和引用類型。
也就是說在使用Scala編程的過程中不需要關注Java中的基本類型,只需要關注Scala中的類型即可,這些類型是封裝好的,可以直接調(diào)用類型中的方法進行操作,Scala中不需要包裝類型,Java中的Inboxing和Unboxing是在Scala中的編譯器中完成的。舉例來說,在Scala中創(chuàng)建了一個Int數(shù)組,最終在Scala解釋器中就會轉(zhuǎn)換成一個int[]數(shù)組,最終在JVM虛擬機中執(zhí)行就是int[]數(shù)組。
Scala中字符串相關操作
Scala中使用java.lang.String類來表示字符串。
Scala擴展了Scala的String的操作,通過StringOps類給字符串追加了上百種操作。
Scala將String對象隱式地轉(zhuǎn)換成StringOps對象,接著StringOps的方法可以被調(diào)用。
Scala中數(shù)值類型的相關操作
Scala提供了RichInt,RichDouble,RichChar等操作類。
Scala有BigInt和BigDecimal類,用于任意大小的數(shù)字,可以用常規(guī)的數(shù)學操作符來操作他們。
Scala中不使用強制轉(zhuǎn)換,而是使用方法進行類型的轉(zhuǎn)換。
值和變量聲明
使用val聲明常量,使用var聲明變量。
val answer = 0
answer = 1 //錯誤,answer是常量,不允許修改
var result = 0
result = 1 //正確,result是變量,可以更改
聲明變量是必須初始化,也就是賦值一個初始值。Scala可以根據(jù)初始值自動推斷變量類型,這與python類似,但是Scala會在編譯階段檢查類型,而Python只有在運行時才會。
聲明多個變量:
scala> val min, max =100
min: Int = 100
max: Int = 100
scala> var greeting, message : String = null
greeting: String = null
message: String = null
這樣將多個變量聲明在一行一般不利于閱讀,通常情況下還是每行初始化一個值:
scala> val min = 10
min: Int = 10
scala> val max = 100
max: Int = 100
scala> var greeting : String = null
greeting: String = null
scala> var message : String = null
message: String = null
Scala函數(shù)與方法
方法定義:
def 方法名(參數(shù)列表):方法返回值類型 = {方法體}
其中方法體中可以沒有return,方法體最后一行的值就是方法的返回值。如果想在方法體中間返回,就必須使用return。
不帶參數(shù)的方法調(diào)用一般省略圓括號,比如:
scala> "hello".distinct
res0: String = helo
Scala中沒有靜態(tài)方法,而是使用單例對象和伴生對象實現(xiàn)靜態(tài)方法和實例方法,這里單例對象是指以object為關鍵字定義的類,而伴生對象是單例對象在與class對象共存在一個文件中并共享類名的特殊稱謂,伴生對象中的就是靜態(tài)方法,實例對象中的就是實例方法:
class Account {
//實例方法
}
object Account {
//靜態(tài)方法
}
函數(shù)跟方法不同,函數(shù)的定義如下:
val 函數(shù)名=(參數(shù)列表) => {函數(shù)體}
這里需要說明的是遞歸函數(shù)必須指明函數(shù)返回值類型。
方法可以轉(zhuǎn)化成函數(shù),但是函數(shù)不能轉(zhuǎn)換成方法:
val 函數(shù)名 = 方法名_
在需要函數(shù)的地方提供一個方法,會自動轉(zhuǎn)換成函數(shù)。
方法與函數(shù)的區(qū)別:
| 函數(shù) | 方法 |
|---|---|
| 可以作為單獨表達式單獨存在 | 只有參數(shù)為空的方法可以單獨存在 |
| 必須有參數(shù)列表 | 可以沒有參數(shù)列表 |
| 函數(shù)名代表函數(shù)對象本身 | 方法名大表方法調(diào)用 |
| 函數(shù)不可轉(zhuǎn)換成方法 | 方法可以轉(zhuǎn)換成函數(shù) |
默認參數(shù)、帶名參數(shù)及邊長參數(shù)
方法定義時如果指定默認參數(shù)值,調(diào)用時可以不傳遞該參數(shù):
scala> def decorate(str:String, left:String = "[", right:String = "]") = left + str + right
decorate: (str: String, left: String, right: String)String
scala> decorate("Hello")
res1: String = [Hello]
scala> decorate("Hello", "<<<", ">>>")
res2: String = <<<Hello>>>
傳遞參數(shù)時也可以指定參數(shù)名,這種情況下就不需要與參數(shù)列表的順序一致,比如:
scala> decorate(left="<<<", right=">>>", str="Hello")
res3: String = <<<Hello>>>
未命名的參數(shù)一定要放在帶名字的參數(shù)之前:
scala> decorate("Hello", left="<<<")
res4: String = <<<Hello]
定義方法時允許指定最后一個參數(shù)可以重復(也就是邊長參數(shù)):
scala> def sum(args : Int*) = {
| var result = 0
| for (arg <- args) result += arg
| result
| }
sum: (args: Int*)Int
scala> val s = sum(1, 3, 4, 7, 4)
s: Int = 19
Scala條件表達式
跟Java條件表達式相同,比較特別的地方是Scala中可以將條件表達式的值賦值給變量:
if (x > 0) s = 1 else s = -1
or
val s = if (x > 0) 1 else -1
可以理解為Scala中的if/else將Java中的if/else與條件表達式?:結(jié)合在了一起。
如果if和else分支返回的結(jié)果類型一致,則表達式的類型就是分支類型;如果if和else分支類型不一致,則表達式類型就是兩個分支類型的公共超類型。
scala> val y = if(x > 0) "positive" else -1
y: Any = positive
如果條件表達式只有if沒有else,那么else返回的就是空,也就是說每種表達式都要有返回值,即使不寫出來,也會返回空,如:
if(x > 0) 1
等同于
if(x > 0) 1 else ()
Scala循環(huán)表達式
分while、do...wihle、for三種,與Java一樣。其中Scala的for循環(huán)與Java有較大區(qū)別:
for(i <- range)
range可以使一個數(shù)字區(qū)間表示,如 i to j,或者 i until j
具體使用:
scala> var sum = 0
sum: Int = 0
scala> for(i <- 1 to 10)
| sum += i
scala> sum
res6: Int = 55
嵌套循環(huán)
Scala的for循環(huán)比Java靈活很多,比如使用Scala實現(xiàn)一個嵌套循環(huán):
scala> for(i <- 1 to 3; j <- 1 to 3) {
| println("i = " + i + " j = " + j)
| }
i = 1 j = 1
i = 1 j = 2
i = 1 j = 3
i = 2 j = 1
i = 2 j = 2
i = 2 j = 3
i = 3 j = 1
i = 3 j = 2
i = 3 j = 3
等價于:
scala> for(i <- 1 to 3) {
| for(j <- 1 to 3) {
| println("i = " + i + " j = " + j)
| }
| }
i = 1 j = 1
i = 1 j = 2
i = 1 j = 3
i = 2 j = 1
i = 2 j = 2
i = 2 j = 3
i = 3 j = 1
i = 3 j = 2
i = 3 j = 3
循環(huán)守衛(wèi)
另外在for循環(huán)中可以通過條件判斷將不想要的數(shù)據(jù)排除掉:
scala> for(i <- 1 to 3 if i != 2) {
| println(i + " ")
| }
1
3
等價于:
scala> for(i <- 1 to 3) {
| if(i != 2) {
| println(i + " ")
| }
| }
1
3
引入變量
Scala在for循環(huán)中還可以引入變量:
scala> for (i <- 1 to 3; from = 4 - i; j <- from to 3) {
| print((10 * i + j) + " ")
| }
13 22 23 31 32 33
等同于:
scala> for(i <- 1 to 3) {
| val from = 4 - i;
| for(j <- from to 3) {
| print((10 * i + j) + " ")
| }
| }
13 22 23 31 32 33
退出循環(huán)
Scala中沒有提供break和continue語句來退出訓話,一般情況下有三種方法退出循環(huán):
使用Boolean型的控制變量
使用嵌套函數(shù),可以從函數(shù)中return。
使用Breaks對象中的break方法,這種方法不常用也不推薦使用。
異常處理
異常是在程序執(zhí)行期間發(fā)生的事件,它會中斷正在執(zhí)行的程序的正常指令流。為了能及時有效處理程序中的運行錯誤,必須使用異常類。
Scala通過拋出異常方法的方式來終止相關代碼的運行,不必通過返回值。
異常處理流程
拋出異常
系統(tǒng)查找可以接受該異常的異常處理器
控制器在離拋出點最近的處理器中恢復
如果沒有找到符合要求的異常處理器,則程序退出
與Java不同的是,Scala所有的異常都是Throwable的子類,沒有受檢異常。
異常語法
拋出異常:
if(x >= 0) {
sqrt(x)
} else throw new IllegalArgumentEcxeption("x should not be negative")
捕獲異常:
try{
process(new URL("http://horstann.com/fred-tiny.gif"))
} catch {
case _: MalformedURLException => println("Bad URL: " + url)
case ex: IOException => ex.printStackTrace()
}
需要注意的是throw表達式有特殊的類型Nothing,另外異常捕獲時更通用的異常應該排在具體的異常之后。
finally語句
try/finally結(jié)構(gòu)中finally語句不管是否拋出異常都會被執(zhí)行
val in = new URL("http://horstmann.com/fred.gif").openStream()
try{
process(in)
} finally {
in.close()
}
try/catch和try/finally結(jié)合一起使用:
try{...} catch {...} finally {...}
Scala面向?qū)ο?/h1>
類的定義:屬性及方法
類是具有共同屬性和行為的對象的集合。類定義了對象的屬性和方法:
class Counter {
private var value = 0
def increment() {value += 1}
def current() = value
}
Scala中的類不聲明為public,一個Scala源文件中可以有多個類。
類成員的可見性
Scala中也有類似于Java的權(quán)限修飾符(public, private, protected):
Scala類中所有成員的默認可見性為公有,任何作用域內(nèi)都可以訪問公有成員。
除了默認的公有可見性,Scala也提供了private和protected,private成員只對本類型和嵌套類型可見,protected成員對本類型和其集成類型都可見。
對于private字段,Scala采用與Java類似的getter和setter方法進行讀取和修改,但是還稍微有些不同(???)。
類的使用
使用類需要做的就是構(gòu)造對象并按照通常的方式來調(diào)動方法:
val myCounter = new Counter //或new Counter()
myCounter.increment()
println(myCounter.current)
通過在方法定義時不帶()來強制方法調(diào)用時不加().
帶getter和setter的屬性
一對getter/setter通常被稱做屬性:
Scala為每個字段都提供getter和setter方法
以字段age為例,Scala中g(shù)etter和setter分別是age和age_=
任何時候都可以重新定義getter和setter方法。
Scala可以實現(xiàn)只讀屬性,但是不能實現(xiàn)只寫屬性。
自定義屬性
var foo:Scala自動合成一個getter和setter
val foo1:Scala自動合成一個getter
自定義foo和foo_=方法
只能自定義foo1方法,不能自定義foo1_=方法
將Scala字段標注為@BeanProperty時,會自動生成符合JavaBean規(guī)范的getter和setter方法:
| Scala字段 | 生成的方法 | 何時使用 |
|---|---|---|
| val/var name | 公有的name name_=(僅限var) | 實現(xiàn)一個可以被公開訪問并且背后是以字段形式保存的屬性 |
| @BeanProperty val/var name | 公有的name getName() name_=(僅限于var) setName(...)(僅限于var) | 與JavaBeans互操作 |
| private val/var name | 私有name name_=(僅限于var) | 用于將字段訪問限制在本類的方法,就和Java一樣。盡量使用private--除非你真的需要一個公有的屬性 |
| private[this] val/var name | 無 | 用于將字段訪問限制在同一個對象上調(diào)用的方法,并不經(jīng)常用到 |
| private[類名] val/var name | 依賴于具體實現(xiàn) | 將訪問權(quán)限賦予外部類,并不經(jīng)常用到 |
類構(gòu)造方法
Scala類的定義主體就是類的構(gòu)造器,稱為主構(gòu)造器。在類名之后用圓括號列出主構(gòu)造器的參數(shù)列表,主構(gòu)造器會執(zhí)行類定義中的所有語句;Scala自動為主構(gòu)造器的參數(shù)列表創(chuàng)建私有字段,并提供對應的訪問方法。
如果類名之后沒有參數(shù),則該類具備一個無參主構(gòu)造器。
class Person(val name: String, val age: Int) {
//(...)中的內(nèi)容就是主構(gòu)造器參數(shù)
...
}
在主構(gòu)造器參數(shù)前加不同的修飾符會生成不同的字段和方法:
| 主構(gòu)造器參數(shù) | 生成的字段和方法 |
|---|---|
| name: String | 對象私有字段,如果沒有方法使用name,則沒有該字段 |
| private val/var name: String | 私有字段,私有的getter/setter方法 |
| val/var name: String | 私有字段,公有的getter/setter方法 |
| @BeanProperty val/var name: String | 私有字段,公有的Scala版和JavaBean版的getter/setter方法 |
Scala類可以包含零個或多個輔助構(gòu)造器,輔助構(gòu)造器使用this進行定義,this的返回類型為Unit,每一個輔助構(gòu)造器的第一行代碼必須以一個對先前已定義的其他輔助構(gòu)造器或主構(gòu)造器的調(diào)用開始。
class Person { //無參主構(gòu)造器
private var name = ""
private var age = 0
def this(name: String) { //一個輔助構(gòu)造器
this() //調(diào)用主構(gòu)造器
this.name = name
}
def this(name: String, age: Int) { //另一個輔助構(gòu)造器
this(name) //調(diào)用前一個輔助構(gòu)造器
this.age = age
}
}
輔助構(gòu)造器不能使用val和var修飾參數(shù)。
object對象
Scala中object對象的屬性和方法默認都是靜態(tài)的,只有一個實例:
object Accounts {
private var lastNumber = 0
def newUniqueNumber() = {
lastNumber += 1
lastNumber
}
}
使用object對象時只需要使用object對象名就可以直接調(diào)用了,比如要調(diào)用newUniqueNumber方法:Accounts.newUniqueNumber()。對象的構(gòu)造器在該對象第一次被使用時調(diào)用,是個懶加載過程。
object對象不提供構(gòu)造器參數(shù)。
單例對象使用場景
作為存放工具函數(shù)或常量的地方,與Java中靜態(tài)變量和靜態(tài)常量一致。
高效地共享單個不可變實例
需要用單個實例來協(xié)調(diào)某個服務時(參考單例模式)
伴生對象
如果一個單例對象和它的同名類一起出現(xiàn)時,這時的單例對象被稱為這個同名類的“伴生對象”,相應的類被稱為這個單例對象的“伴生類”。
類和它的伴生對象必須存放在同一個文件中,可以相互訪問私有成員。
沒有同名類的單例對象被稱為孤立對象,一般情況下Scala程序的入口點main方法就是定義在一個孤立對象里。
//類的伴生對象可以被訪問,但是并不在類的作用域中
class Account {
val id = Account.newUniqueNumber()
private var balance = 0.0
def deposit(amount: Double) {
balance += amount
}
...
}
object Account {
private var lastNumber = 0
private def newUniqueNumber() = {
lastNumber += 1
lastNumber
}
}
apply方法
關于scala apply方法的講解可以參考這個:Scala學習筆記--apply 方法詳解.
apply方法調(diào)用約定:
用括號傳遞給實例或單例對象名一個或多個參數(shù)時,Scala會在相應的類或?qū)ο笾胁檎曳椒麨閍pply且參數(shù)列表與傳入的參數(shù)一致的方法,并用傳入的參數(shù)來調(diào)用該apply方法。
實例化單例對象時,并沒有使用到new,這是怎么做到的呢?這就是apply的作用。
//通常一個apply方法返回的是半生類的對象
class Account private (val id: Int, initialBalance: Double) {
private var balance = initialBalance
...
}
object Account{
def apply(initialBanance: Double) = new Account(newUniqueNumber(), initialBalance)
...
}
Array(100)和 new Array(100)有什么不同
| Array(100) | new Array(100) | |
|---|---|---|
| 調(diào)用方法 | apply(100) | this(100) |
| 輸出結(jié)果 | 輸出只有一個元素100的數(shù)組 | 輸出包含100個null元素的數(shù)組 |
為什么設計apply方法
保持對象和函數(shù)之間使用的一致性
面向?qū)ο螅簩ο?方法 數(shù)學:函數(shù)(參數(shù))
Scala中一切都是對象,包括函數(shù)也是對象。Scala中的函數(shù)既保留括號調(diào)用樣式,也可以使用點號調(diào)用形式,其對應的方法名即為apply。
unapply方法
unapply方法用于對對象進行解構(gòu)操作,與apply方法類似,該方法也會被自動調(diào)用。
可以認為unapply方法是apply方法的反向操作,apply方法接受構(gòu)造參數(shù)變成對象,而unapply方法接受一個對象從中取值。
方法重寫和字段重寫
方法重寫
Scala中重寫一個非抽象方法必須使用override修飾符:
public class Person {
...
override def toString = getClass.getName + "[name=" + name + "]"
}
繼承抽象類和特質(zhì)類時重寫方法可以不寫override修飾符,鉆石結(jié)構(gòu)中重寫方法時需要寫override修飾符,參考鏈接。
override修飾符可以再多種情況下給出錯誤提示:
拼錯重寫的方法名
在新方法中使用了錯誤的參數(shù)類型
在超類中引入了新的方法,但是這個新的方法與子類方法相抵觸。
字段重寫
Scala的字段由一個私有字段和取值器/改值器方法構(gòu)成
class Person(val name: String) {
override def toString = getClass.getName + "[name=" + name + "]"
}
class SecretAgent(codename: String) extends Person(codename) {
override val name = "secret" //不想暴露真名
override val toString = "secret" //或類名
}
重寫限制:
| 用val | 用def | 用var | |
|---|---|---|---|
| 重寫val | 子類有一個私有字段(與超類的字段名字相同)getter方法重寫超類的getter方法 | 錯誤 | 錯誤 |
| 重寫def | 子類有一個私有字段 getter方法重寫超類的方法 | 和java一樣 | var可以重寫getter/setter對。只重寫getter會報錯 |
| 重寫var | 錯誤 | 錯誤 | 僅當超類的var是抽象的才可以 |
抽象類
抽象方法
如果一個類包含沒有實現(xiàn)的成員,則必須使用abstract關鍵字進行修飾,定義為抽象類,該類不能實例化,必須由其子類繼承該抽象類后實現(xiàn)相應的成員,才能實例化繼承類。
abstract class Person(val name: String) {
def id: Int //沒有方法體,這是一個抽象方法
}
抽象類中,不需要對抽象方法使用abstract關鍵字,scala會自動判斷,只需要省去方法體即可。
某類至少存在一個抽象方法,則該類必須聲明稱抽象類
子類中重寫超類的抽象方法時,不需要加override關鍵字
抽象字段
抽象字段就是一個沒有初始值的字段
abstract class Person {
val id: Int //沒有初始化,這是一個帶有抽象的getter方法的抽象字段。
val name: String //另一個抽象字段,帶有抽象的getter和setter方法。
}
抽象字段必須聲明類型
子類重寫抽象字段時不需要寫override關鍵字。
trait特質(zhì)
Java中是不允許多重繼承的,Scala也不允許,多重繼承如下所示:
Person
-------|--------
| |
Student Employee
| |
-------|--------
Teaching Assistant
TA(Teaching Assistant)無法同時繼承Student和Employee,但是Scala中引入了一個叫做特質(zhì)(trait)的東西來實現(xiàn)多重繼承。
特質(zhì)用于在類之間共享程序接口和字段,類似于Java的接口。
類和對象可以擴展特質(zhì),但是特質(zhì)不能被實例化,因此特質(zhì)沒有參數(shù)。
trait Logger {
def log(msg: String) //這個是抽象方法
}
Java中一個類是可以實現(xiàn)多個接口的,在Scala中一個類可以實現(xiàn)多個特質(zhì),特質(zhì)跟Java的接口作用一摸一樣。
當作接口使用的特質(zhì)
特質(zhì)中未被實現(xiàn)的方法默認為抽象方法。
重寫特質(zhì)的抽象方法不需要加override關鍵字。
使用特質(zhì)時用extends關鍵字。
需要多個特質(zhì)時,用with關鍵字來添加額外的特質(zhì)。
Scala類中只能有一個超類,但是可以有任意數(shù)量的特質(zhì)。
特質(zhì)與Java中接口的不同是,特質(zhì)中的方法不需要一定是抽象的,也可以有具體實現(xiàn),但是讓特質(zhì)擁有具體行為存在一個弊端,那就是當特質(zhì)改變時,所有混入了該特質(zhì)的類都需要重新編譯。
trait ConsoleLogger {
def log(msg: String) {
println(msg)
}
}
繼承類的特質(zhì)
特質(zhì)也可以繼承類,特質(zhì)繼承類時,這個類會自動成為所有混入該特質(zhì)的超類。
如果特質(zhì)繼承的類擴展了另一個類,那么只有另一類是特質(zhì)的超類的一個子類才可以混入該特質(zhì)。
LoggedException是一個特質(zhì),它繼承了Logged和Exception這兩個類
class UnhappyException extends IOException with LoggedException
class UnhappyFrame extends JFrame with LoggedException //錯誤
帶有特質(zhì)的對象
在構(gòu)造單個對象時,可以為其添加特質(zhì)。
特質(zhì)可以將對象原本沒有的方法與字段加入對象中。
如果特質(zhì)和對象改寫了同一個超類的方法,則排在右邊的先被執(zhí)行。
trait Logged {
def log(msg: String) { }
}
class SavingsAccount extends Account with Logged {
def withdeaw(amount: Double) {
if(amount > balance) log("Insufficient funds")
else
...
}
}
特質(zhì)中的字段
特質(zhì)中的字體可以是具體的也可以是抽象的,如果有初始值那么字段就是具體的。
通常對于特質(zhì)中每一個具體字段,使用該字段的類都會獲得一個字段與之對應,這些字段不是被繼承的,他們只是簡單地加到了子類中。
特質(zhì)中未被初始化的字段在具體的子類中必須被重寫。
自身類型與結(jié)構(gòu)類型(不理解)
帶有自身類型的特質(zhì)只能被混入指定類型的子類
結(jié)構(gòu)類型只給出類必須擁有的方法而不是類的名稱。
trait LoggedException extends Logged {
this: Exception => def log() {
log(getMessage())
}
}
特質(zhì)即實現(xiàn)了Java的接口功能,又實現(xiàn)了抽象類的功能,在Scala中還是比較常見的。
case class 樣例類
case class 是一種特殊的類,經(jīng)過優(yōu)化后可以被用于模式匹配。
case class的聲明如下:
abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount
聲明case class時可以直接使用類名加參數(shù)的形式,此時會自動發(fā)生如下事件:
構(gòu)造器中每一個參數(shù)都成為val,除非被顯式地聲明為var
在伴生對象中提供apply方法可以不用new關鍵字就能構(gòu)造出相應的對象。
提供unapply方法讓模式匹配可以工作
生成toString, equals, hashCode和copy方法。
copy方法和帶名參數(shù)
樣例類的copy方法創(chuàng)建一個與現(xiàn)有對象值相同的新對象,可以用帶名參數(shù)修改某些屬性。
val amt = Currency(29.95, "EUR")
val price = amt.copy()
val price = amt.copy(value = 19.95) //Currency(19.95, "EUR")
樣例類的密封
當case class的超類使用關鍵字sealed修飾,則編譯器會校驗對該超類對象的模式匹配規(guī)則中,是否列出了可能的子case類,且該超類的子類只能出現(xiàn)在超類的文件中,形成封閉,而不能出現(xiàn)在其他文件中。
sealed abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount
模式匹配
語法:變量 match {case 值 => 代碼}
如果case值為下劃線,則代表不滿足以上所有情況。match case中只要一個case分支滿足條件并處理了,就不會繼續(xù)判斷下一個case分支了。
var sign = ...
val ch: Char = ...
ch match {
case '+' => sign = 1
case '-' => sign = -1
case _ => sign = 0
}
Scala模式匹配不會意外調(diào)入下一個分支。
在case后的條件判斷中可以在值后面加一個if條件,進行雙重過濾,比如:
ch match {
case '+' => sign = 1
case '-' => sign = -1
case _ if Character.isDigit(ch) => digit = Character.digit(ch, 10)
case _ => sign = 0
}
if后的條件可以是任意類型的Boolean類型。
如果case關鍵字后面跟著一個變量名,那么匹配到的這個變量值會被賦值到后面的表達式中:
str(i) match {
case '+' => sign = 1
case '-' => sign = -1
case ch => digit = Character.digit(ch, 10)
}
case還可以對類型進行模式匹配,case 變量 : 類型 => 代碼
obj match {
case x: Int => x
case s: String => Integer.parseInt(s)
case _: BigInt => Int.MaxValue
case _ => 0
}
case還可以匹配數(shù)組、列表和元組。
//匹配數(shù)組
arr match {
case Array(0) => "0" //匹配到數(shù)組只有一個元素0
case Array(x, y) => x + " " + y //匹配到數(shù)組有兩個元素
case Array(0, _*) => "0..." //匹配到數(shù)組第一個元素是0,后面有不確定個元素、
case _ => "something else"
}
//匹配元組
pair match{
case (0, _) => "0 ..." //匹配到第一個元素是0的元組
case (y, 0) => y + " 0" //匹配到末尾元素是0的元組
case _ => "neither is 0"
}
//列表
lst match {
case 0 :: Nil => "0" //匹配到列表只有一個元素0
case x :: y :: Nil => x + " " + y //匹配到列表有兩個元素
case 0 :: tail => "0 ..." //匹配到列表0開頭的列表
case _ => "somthing else"
}
Scala集合類
集合
Scala集合集成關系,關鍵特質(zhì)如下:
重點關注一下幾種集合:
| 集合 | 描述 |
|---|---|
| List | 元素以線性方式存儲,集合中可以存放重復對象 |
| Set | 集合中的元素不按特定方式排序且沒有重復對象 |
| Map | 鍵對象和值對象映射的集合,每一個元素都包含一個鍵值對 |
| Tuple | 元組是不同類型的值的集合 |
| Option | 表示有可能包含值的容器,也可能不包含值 |
Set
val set = Set(1, 2, 3)
println(set.getClass.getName)
println(set.exists(_ % 2 == 0)) //true
println(set.drop(1)) //Set(2, 3)
Set是不重復元素的集合
Set不保留元素的插入順序
缺省情況下,Set是以HashSet實現(xiàn)的,其元素根據(jù)hashCode方法的值進行組織。
如果使用的是sortedSet的話,里面存在鏈式hashSet可以記住元素的插入順序
可變集合和不可變集合:
默認情況下Scala使用的是不可變集合,如果想使用可變集合,需要引入
scala.collection.mutable.Set包。可變Set和不可變Set都有添加或刪除元素的操作,對不可變set進行操作會產(chǎn)生一個新的set,原來的set并沒有改變,對可變set進行操作,改變的是該set本身。
Map
//Map 初始化
val colors = Map("red" -> "#FF0000", "azure" -> "#F0FFFF")
//空哈希表,鍵為字符串,值為整數(shù)
var A: Map[Char, Int] = Map()
Map是一種可迭代的鍵值對結(jié)構(gòu)(key/value)
所有值都可以通過鍵來獲取。
Map中的鍵是唯一的
Map有可變與不可變之分,可變對象可以修改,不可變對象不能修改
默認scala使用不可變的Map,如果要使用可變Map需要顯式引入
import scala.collection.mutable.Map類
元組
val t = new Tuple3(1, 3.14, "Fred") //Tuple3表示有元組有3個元素
元組是不可變的,但是可以包含不同類型的元素
元組的值通過將單個值包含在圓括號中構(gòu)成
目前scala支持的元組最大長度是22
序列
不可變序列
不可變集合中添加新元素會生成一個新集合。
//vector
//1. 創(chuàng)建Vector對象
var v1 = Vector(1, 2, 3)
//2. 索引Vector
println(v1(0))
//3. 遍歷Vector
for(ele <- v1) {
print(ele + " ")
}
//4. 倒轉(zhuǎn)Vector
var v2 = Vector(1.1, 2.2, 3.3, 4.4)
for(ele <- v2.reverse) {
print(ele + " ")
}
//range
scala> Array.range(1, 10)
res0: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9)
scala> List.range(1, 10)
res1: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)
scala> Vector.range(1, 10)
res2: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9)
可變序列
Seq是一個特質(zhì),返回的是List:
scala> Seq(1, 2, 3)
res0: Seq[Int] = List(1, 2, 3)
//ArrayBuffer
//如果不想每次都是用全限定名,則可以先導入ArrayBuffer類
import scala.collection.multable.ArrayBuffer
val b = ArrayBuffer[Int]()
b += 1
b += (2, 3, 4, 5)
b ++= Array(6, 7, 8, 9, 10)
b.trimEnd(5)
b.insert(5, 6)
b.remove(1)
b.remove(1, 3)
b.toArray
b.toBuffer
集合操作
常用操作:
| 操作符 | 描述 | 集合類型 |
|---|---|---|
| coll :+ elem / elem +: coll | 有elem被追加到coll集合的尾部或頭部 | Seq |
| coll + elem / coll + (e1, e2, ...) | 添加了給定元素的到coll集合中 | Set Map |
| coll - elem / coll - (e1, e2, ...) | 移除coll中指定的元素 | Set, Map, ArrayBuffer |
| coll ++ coll2 / coll2 ++: coll | 兩個集合相加,返回包含了兩個集合的元素的新集合 | Iterable |
| coll -- coll2 | 從coll中移除coll2中的元素 | Set, Map, ArrayBuffer |
| elem :: lst / lst2 ::: lst | 和+:以及++:的作用相同 | List |
| list ::: list2 | 等同于list ++: list2 | List |
| set | set2 / set & set2 / set & ~set2 | 并集、交集和兩個集和的差異。 | |等同于++,&~等同于-- | Set |
| coll += elem / coll += (e1, e2, ...) / coll++= coll2 / coll -= elem / coll -= (e1, e2, ...) / coll --= coll2 | 通過添加或移除給定元素來修改coll | 可變集合 |
| elem +=: coll / coll2 ++=:coll | 通過向前追加給定元素或集合來修改coll | ArrayBuffer |
其中如果集合有序,可以加入:來確定集合運算后的順序。=表示對可變集合進行操作
追加元素
有先后次序追加:
scala> val list1 = List(1, 2, 3)
list1: List[Int] = List(1, 2, 3)
scala> val list2 = list1 :+ 4
list2: List[Int] = List(1, 2, 3, 4)
scala> val list3 = 5 +: list2
list3: List[Int] = List(5, 1, 2, 3, 4)
scala> list1 ++ list2
res1: List[Int] = List(1, 2, 3, 1, 2, 3, 4)
scala> list1 ::: list2
res2: List[Int] = List(1, 2, 3, 1, 2, 3, 4)
無先后次序追加:
scala> val set = Set(1, 3, 5, 7)
set: scala.collection.immutable.Set[Int] = Set(1, 3, 5, 7)
scala> set + 2
res3: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 7, 3)
構(gòu)建列表:
scala> "a" :: "b" :: Nil
res4: List[String] = List(a, b)
移除元素
set移除元素:
scala> val set1 = Set(1,2,3,4,5,6,7,8,9)
set1: scala.collection.immutable.Set[Int] = Set(5, 1, 6, 9, 2, 7, 3, 8, 4)
scala> set1 - 9
res5: scala.collection.immutable.Set[Int] = Set(5, 1, 6, 2, 7, 3, 8, 4)
scala> val set2 = Set(1, 2, 3)
set2: scala.collection.immutable.Set[Int] = Set(1, 2, 3)
scala> set1 -- set2
res7: scala.collection.immutable.Set[Int] = Set(5, 6, 9, 7, 8, 4)
改值操作
可變集合改值操作,就是在剛才不可變集合操作上加個=:
scala> import collection.mutable.Set
import collection.mutable.Set
scala> val set1 = Set(1, 2, 3)
set1: scala.collection.mutable.Set[Int] = Set(1, 2, 3)
scala> set1 += 4
res8: set1.type = Set(1, 2, 3, 4)
scala> set1 -= 4
res9: set1.type = Set(1, 2, 3)
scala> val set2 = Set(4, 5, 6)
set2: scala.collection.mutable.Set[Int] = Set(5, 6, 4)
scala> set1 ++ set2
res10: scala.collection.mutable.Set[Int] = Set(1, 5, 2, 6, 3, 4)
scala> set1 --= set2
res11: set1.type = Set(1, 2, 3)
Scala高級特性
隱式轉(zhuǎn)換
隱式轉(zhuǎn)換函數(shù)指的是那種以implicit關鍵字聲明的帶有單個參數(shù)的函數(shù)。這樣的函數(shù)將被自動應用,將值從一種類型轉(zhuǎn)換成另一種類型。
implicit def int2Fraction (n: Int) = Fraction(n, 1)
可以給隱式轉(zhuǎn)換起任何名字,建議使用source2target形式。
為什么需要隱式轉(zhuǎn)換?
java中使用final修飾的類是不能對其進行繼承,進而對其進行擴展的。要想擴展final修飾的類就需要對其進行包裝,先引用進來再進行操作,并提供一些擴展接口出去,這樣的代碼寫起來比較麻煩,而且明確支出擴展類名的話你也不知道要調(diào)用哪個類。
Scala中使用隱式轉(zhuǎn)換將final修飾的類隱式轉(zhuǎn)換成另一個類,在原類型的基礎上可以直接調(diào)用轉(zhuǎn)換后的類的方法,這就避免了java中的問題。
隱式轉(zhuǎn)換注意事項
對于隱式轉(zhuǎn)換,編譯器最關心的是它的類型簽名,即它將哪一種類型轉(zhuǎn)換成另一種類型,也就是說它應該只接受一個參數(shù)。同一個作用于下,隱式轉(zhuǎn)換函數(shù)名不能相同
不支持嵌套的隱式轉(zhuǎn)換。
隱式轉(zhuǎn)換函數(shù)的函數(shù)名可以是任意的,與函數(shù)名稱無關,只與函數(shù)簽名(函數(shù)參數(shù)和返回值類型)有關。
如果當前作用域中存在函數(shù)簽名相同但函數(shù)名不同的兩個隱式轉(zhuǎn)換函數(shù),則在進行隱式轉(zhuǎn)換時會報錯。
代碼能夠在不適用隱式轉(zhuǎn)換的前提下能編譯通過,就不會進行隱式轉(zhuǎn)換。
隱式轉(zhuǎn)換的應用
隱式轉(zhuǎn)換常見的用途就是擴展已有類,在不修改原有類的基礎上為其添加新的方法和成員。
//為java.io.File添加read方法
class RichFile(val from: File) {
def read = Source.fromFile(from.getPath).mkString
}
implicit def file2RichFile(from: File) = new RichFile(from)
以后直接在File類上面調(diào)用read方法的時候,會自動把這個類轉(zhuǎn)換成RichFile類,并且調(diào)用RichFile的read方法。
引入隱式轉(zhuǎn)換
Scala會考慮如下的隱式轉(zhuǎn)換函數(shù):
位于源或目標類型的伴生對象中的隱式函數(shù)(太難以理解)
位于當前作用域可以以單個標識符指代的隱式函數(shù)(太難以理解)
通俗來說就是隱式轉(zhuǎn)換可以在文件頭(即類的頭)進行轉(zhuǎn)換,也可以在方法中引入隱式轉(zhuǎn)換,這就可以限制隱式轉(zhuǎn)換的作用于在哪個位置,這根Scala引入其他類時添加的作用域是一樣的,可以在文件的作用域下,可以在類的作用域下,也可以在某個方法的作用域下。比如:
//引入局部化隱式轉(zhuǎn)換
object Main extends App {
import com.horstmann.impatient.FractionConversions._
val result = 3 * Franction(4, 5) //使用引入的轉(zhuǎn)換
println(result)
}
//選擇特定轉(zhuǎn)換
object FractionConversions {
...
implicit def fraction2Double(f: Fraction) = f.num * 1.0 / f.den
}
//排除特定轉(zhuǎn)換
import com.horstmann.impatient.FractionConversions.{
fraction2Double => _, _
} //引入除fratcion2Double外的所有成員
隱式轉(zhuǎn)換規(guī)則
-
當表達式的類型與預期類型不同時:
sqrt(Fraction(1, 4)) //將調(diào)用fraction2Double,因為sqrt預期的是一個Double -
當對象訪問一個不存在的成員時:
new File("Readme").read //將調(diào)用file2RichFile,因為File沒有read方法 -
當對象調(diào)用某個方法,而該方法的參數(shù)聲明與傳入?yún)?shù)不匹配時:
* Fraction(4, 5) //將調(diào)用int2Fraction,因為Int的*方法不接受Fraction作為參數(shù)
一下三種情況下編譯器不會嘗試使用隱式轉(zhuǎn)換:
如果代碼在不適用隱式轉(zhuǎn)換的前提下能夠通過編譯
編譯器不會嘗試同時執(zhí)行多個轉(zhuǎn)換
存在二義性的轉(zhuǎn)換是個錯誤。
總結(jié)
Scala 是一種很簡潔的函數(shù)式編程語言,學習的重點是 Scala 語法及其特性,對比著 Java 進行學習,比如特質(zhì)、隱式轉(zhuǎn)換、伴生對象/伴生類等。這里有我的一些學習代碼記錄,可以參考一下。