Scala學習筆記

大數(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):

  1. 使用Boolean型的控制變量

  2. 使用嵌套函數(shù),可以從函數(shù)中return。

  3. 使用Breaks對象中的break方法,這種方法不常用也不推薦使用。

異常處理

異常是在程序執(zhí)行期間發(fā)生的事件,它會中斷正在執(zhí)行的程序的正常指令流。為了能及時有效處理程序中的運行錯誤,必須使用異常類。

Scala通過拋出異常方法的方式來終止相關代碼的運行,不必通過返回值。

異常處理流程

  1. 拋出異常

  2. 系統(tǒng)查找可以接受該異常的異常處理器

  3. 控制器在離拋出點最近的處理器中恢復

  4. 如果沒有找到符合要求的異常處理器,則程序退出

與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)換、伴生對象/伴生類等。這里有我的一些學習代碼記錄,可以參考一下。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • scala文檔 包含內(nèi)容 1基礎 2控制結(jié)構(gòu)和函數(shù) 3數(shù)組相關操作 4映射和元組 5類 6對象 7.包和引入(x)...
    zlcook閱讀 1,047評論 0 3
  • scala學習筆記 第2章 變量和數(shù)據(jù)類型 基本數(shù)據(jù) scala的核心數(shù)據(jù)為四種 :字面量、值、變量、類型 值使...
    485b1aca799e閱讀 2,240評論 0 1
  • 這篇文章是我跟著視頻學,再加上看博客總結(jié)的Scala關鍵知識點,用來開發(fā)Spark完全夠用。 第一節(jié):基礎 變量聲...
    大數(shù)據(jù)Zone閱讀 769評論 0 6
  • 目錄 9.文件和正則表達式 10.特質(zhì) 11.操作符 12.高階函數(shù) 9.文件和正則表達式 Source.from...
    zlcook閱讀 463評論 0 2
  • 教材:快學Scalamarkdown閱讀:https://www.zybuluo.com/mdeditor cha...
    hakase_nano閱讀 1,098評論 0 1

友情鏈接更多精彩內(nèi)容