大數據分析技術--第2章Scala語言基礎

第2章Scala語言基礎

2.1 Scala語言概述
2.2 Scala基礎知識
2.3 Scala面向對象編程基礎
2.4 Scala函數式編程基礎

2.1 Scala語言概述
2.1.1 計算機的緣起
2.1.2 編程範式
2.1.3 Scala簡介

2.1.1 計算機的緣起

  • 數學家阿隆佐•邱奇(Alonzo Church)設計了“λ演算”,這是一套用於研究函數定義、函數應用和遞歸的形式系統
  • λ演算被視爲最小的通用程序設計語言
  • λ演算的通用性就體現在,任何一個可計算函數都能用這種形式來表達和求值
  • 演算是一個數理邏輯形式系統,強調的是變換規則的運用,而非實現它們的具體機器
  • 英國數學家阿蘭·圖靈採用了完全不同的設計思路,提出了一種全新的抽象計算模型——圖靈機

在這裏插入圖片描述

  • 圖靈機是現代計算機的鼻祖。現有理論已經證明,λ演算和圖靈機的計算能力是等價的
  • 馮·諾依曼(John Von Neumann)將圖靈的理論物化成爲實際的物理實體,成爲了計算機體系結構的奠基者
  • 1945年6月,馮·諾依曼提出了在數字計算機內部的存儲器中存放程序的概念,這是所有現代計算機的範式,被稱爲“馮·諾依曼結構”
    在這裏插入圖片描述
    2.1.2 編程範式
  • 編程範式是指計算機編程的基本風格或典範模式。常見的編程範式主要包括命令式編程和函數式編程。面向對象編程就屬於命令式編程,比如C++、Java等
  • 命令式語言是植根於馮·諾依曼體系的,一個命令式程序就是一個馮·諾依曼機的指令序列,給機器提供一條又一條的命令序列讓其原封不動地執行
  • 函數式編程,又稱泛函編程,它將計算機的計算視爲數學上的函數計算
  • 函數編程語言最重要的基礎是λ演算。典型的函數式語言包括Haskell、Erlang和Lisp等
  • 一個很自然的問題是,既然已經有了命令式編程,爲什麼還需要函數式編程呢?
  • 爲什麼在C++、Java等命令式編程流行了很多年以後,近些年函數式編程會迅速升溫呢?
  • 命令式編程涉及多線程之間的狀態共享,需要鎖機制實現併發控制
  • 函數式編程不會在多個線程之間共享狀態,不需要用鎖機制,可以更好並行處理,充分利用多核CPU並行處理能力
    在這裏插入圖片描述
    2.1.3 Scala簡介
    Scala是一門類Java的多範式語言,它整合了面向對象編程和函數式編程的最佳特性。
  • Scala運行於Java虛擬機(JVM)之上,並且兼容現有的Java程序
  • Scala是一門純粹的面向對象的語言
  • Scala也是一門函數式語言
    在這裏插入圖片描述
    2.1.4 Scala的安裝
    (1)Linux系統的安裝
  • Scala運行在Java虛擬機(JVM)之上,因此只要安裝有相應的Java虛擬機,所有的操作系統都可以運行Scala程序,包括Window、Linux、Unix、Mac OS等。後續的Spark操作都是在Linux系統下進行的
  • Scala的詳細安裝教程可以參考此鏈接
    (2)在Linux系統中安裝Java
    第1種安裝方式:直接通過命令安裝 OpenJDK 7
sudo apt-get install openjdk-7-jre openjdk-7-jdk

配置 JAVA_HOME 環境變量

vim ~/.bashrc


使配置立即生效:

source ~/.bashrc    # 使變量設置生效

第2種安裝方式:直接通過命令安裝 default-jdk

sudo apt-get install default-jre default-jdk

配置 JAVA_HOME 環境變量

vim ~/.bashrc

在文件最前面添加如下單獨一行(注意,等號“=”前後不能有空格),然後保存退出:

export JAVA_HOME=/usr/lib/jvm/default-java

在這裏插入圖片描述
使配置立即生效:

source ~/.bashrc    # 使變量設置生效

檢驗一下是否設置正確

 echo $JAVA_HOME     # 檢驗變量值
 java -version
 $JAVA_HOME/bin/java -version  # 與直接執行 java -version 一樣

如果設置正確的話,$JAVA_HOME/bin/java -version 會輸出 java 的版本信息,且和 java -version 的輸出結果一樣
(3)安裝Scala

Spark可在Java 8,Python 2.7 + / 3.4 +和R 3.1+上運行。對於Scala API,Spark2.4.5使用Scala 2.12。您將需要使用兼容的Scala版本(2.12.x)。
請注意,自Spark 2.2.0起已刪除了對Java 7,Python 2.6和2.6.5之前的舊Hadoop版本的支持。從2.3.0版本開始,不再支持Scala 2.10。從Spark 2.4.1開始不支持Scala 2.11,它將在Spark 3.0中刪除。

-這邊使用的Spark版本是2.4.5,其對應的Scala版本是2.12.11
登錄Scala官網,下載scala-2.12.11.tgz

 sudo tar -zxf scala-2.12.11.tgz -C /usr/local   # 解壓到/usr/local中
 cd /usr/local/
 sudo mv ./scala-2.12.11/ ./scala         # 將文件夾名改爲scala
 sudo chown -R hadoop ./scala        # 修改文件權限,用hadoop用戶擁有對scala目錄的權限

//如果之前沒有創建hadoop用戶可先創建hadoop用戶
powershell sudo useradd -m hadoop -s /bin/bash #這條命令創建了可以登陸的hadoop 用戶,並使用 /bin/bash 作爲 shell
sudo passwd hadoop #設置hadoop密碼
sudo adduser hadoop sudo #爲 hadoop 用戶增加管理員權限

把scala命令添加到path環境變量中

vim ~/.bashrc
export PATH=$PATH:/usr/local/scala/bin

使配置立即生效:

source ~/.bashrc    # 使變量設置生效

檢測配置是否成功:

 scala -version

啓動Scala解釋器:

scala
scala>   //可以在命令提示符後面輸入命令

2.1.5 HelloWorld
(1)通過HelloWorld程序瞭解Scala解釋器的使用方法
在Shell命令提示符界面中輸入“scala”命令後,會進入scala命令行提示符狀態:

scala>   //可以在命令提示符後面輸入命令

在這裏插入圖片描述
其中的換行|按Ctrl+Eenter鍵即可實現
使用命令“:quit”退出Scala解釋器,如下所示:

scala> :quit

(2)在Scala解釋器中運行腳本文件
用“:load”命令導入腳本,一次運行多行程序:
使用文本編輯器(比如vim)創建一個代碼文件Test.scala

//代碼文件爲/usr/local/scala/mycode/Test.scala
println("This is the first line")
println("This is the second line")
println("This is the third line")

在Scala REPL中執行如下命令運行該代碼文件:

scala> :load  /usr/local/scala/mycode/Test.scala
Loading /usr/local/scala/mycode/Test.scala…
This is the first line
This is the second line
This is the third line

(3)通過編譯打包的方式運行Scala程序

//代碼文件爲/usr/local/scala/mycode/HelloWorld.scala
object HelloWorld {
  def main(args: Array[String]) {
  println("Hello, world!");
 }
}

使用scalac命令進行編譯(編譯的結果爲Java字節碼)

cd /usr/local/scala/mycode
scalac  HelloWorld.scala

使用scala或者java命令運行字節碼文件

 scala -classpath . HelloWorld
 java  -classpath  .:/usr/local/scala/lib/scala-library.jar  HelloWorld

2.2 Scala基礎知識

2.2.1基本數據類型和變量
2.2.2 輸入輸出
2.2.3 控制結構
2.2.4 數據結構

2.2.1基本數據類型和變量

  • 1.基本數據類型
  • 2.基本操作
  • 3.變量

1. 基本數據類型

  • Scala的數據類型包括:Byte、Char、Short、Int、Long、Float、Double和Boolean(注意首字母大寫)
  • 和Java不同的是,在Scala中,這些類型都是“類”,並且都是包scala的成員,比如,Int的全名是scala.Int。對於字符串,Scala用java.lang.String類來表示字符串

在這裏插入圖片描述
2.字面量(literal)

val i = 123            //123就是整數字面量
val i = 3.14           //3.14就是浮點數字面量
val i = true            //true就是布爾型字面量
val i = 'A'              //'A'就是字符字面量
val i = “Hello”       //“Hello”就是字符串字面量

3.操作符

  • 算術運算符:加(+)、減(-) 、乘(*) 、除(/) 、餘數(%);
  • 關係運算符:大於(>)、小於(<)、等於(==)、不等於(!=)、大於等於(>=)、小於等於(<=)
  • 邏輯運算符:邏輯與(&&)、邏輯或(||)、邏輯非(!);
  • 位運算符:按位與(&)、按位或(|)、按位異或(^)、按位取反(~)等
  • 賦值運算符:=及其與其它運算符結合的擴展賦值運算符,例如+=、%=
    操作符優先級:算術運算符 > 關係運算符 > 邏輯運算符 > 賦值運算符

在Scala中,操作符就是方法
例如,5 + 3和(5).+(3)是等價的

a 方法 b 等價於 a.方法(b)

scala> val sum1 = 5 + 3         //實際上調用了 (5).+(3)
sum1: Int = 8
scala> val sum2 = (5).+(3)     //可以發現,寫成方法調用的形式,和上面得到相同的結果
sum2: Int = 8

3.操作符——富包裝類

  • 對於基本數據類型,除了以上提到的各種操作符外,Scala還提供了許多常用運算的方法,只是這些方法不是在基本類裏面定義,而是被封裝到一個對應的富包裝類中
  • 每個基本類型都有一個對應的富包裝類,例如Int有一個RichInt類、String有一個RichString類,這些類位於包scala.runtime中
  • 當對一個基本數據類型的對象調用其富包裝類提供的方法,Scala會自動通過隱式轉換將該對象轉換爲對應的富包裝類型,然後再調用相應的方法。例如:3 max 5

4.變量
Scala有兩種類型的變量:

  • val:是不可變的,在聲明時就必須被初始化,而且初始化以後就不能再賦值

  • var:是可變的,聲明的時候需要進行初始化,初始化以後還可以再次對其賦值

基本語法:

val 變量名:數據類型 = 初始值
var 變量名:數據類型 = 初始值

類型推斷機制(type inference):根據初始值自動推斷變量的類型,使得定義變量時可以省略具體的數據類型及其前面的冒號

scala> val myStr = "Hello World!"
myStr: String = Hello World!

當然,我們也可以顯式聲明變量的類型:

scala> val myStr2 : String = "Hello World!"
myStr2: String = Hello World!
scala> println(myStr)
Hello World!

myStr是val變量,因此,一旦初始化以後,就不能再次賦值

scala> myStr = "Hello Scala!"
<console>:27: error: reassignment to val
          myStr = "Hello Scala!"
                          ^

var變量初始化以後,可以再次賦值

scala> var myPrice : Double = 9.9
myPrice: Double = 9.9

scala> myPrice = 10.6
myPrice: Double = 10.6
注意:在REPL環境下,可以重複使用同一個變量名來定義變量,而且變量前的修飾符和其類型都可以不一致,REPL會以最新的一個定義爲準
scala> val a = "Xiamen University"
a: String = Xiamen University
scala> var a = 50
a: Int = 50

2.2.2 輸入輸出

  • 1.控制檯輸入輸出語句
  • 2.讀寫文件

1. 控制檯輸入輸出語句

  • 從控制檯讀入數據方法:readInt、readDouble、readByte、readShort、readFloat、readLong、readChar readBoolean及readLine,分別對應9種基本數據類型,其中前8種方法沒有參數,readLine可以不提供參數,也可以帶一個字符串參數的提示
  • 所有這些函數都屬於對象scala.io.StdIn的方法,使用前必須導入,或者直接用全稱進行調用

從控制檯讀入數據方法

scala> import io.StdIn._
import io.StdIn._
scala> var i=readInt()
54
i: Int = 54
scala> var f=readFloat
1.618
f: Float = 1.618
scala> var b=readBoolean
true
b: Boolean = true
scala> var str=readLine("please input your name:")
please input your name:Li Lei
str: String = Li Lei

向控制檯輸出信息方法:

  • print()和println(),可以直接輸出字符串或者其它數據類型,其中println在末尾自動換行。
scala> val i=345
i: Int = 345
scala> print("i=");print(i) 
//兩條語句位於同一行,不能省略中間的分號
i=345
scala> println("hello");println("world")
hello
world
  • C語言風格格式化字符串的printf()函數
scala> val i = 34
i: Int = 34
scala> val f=56.5
f: Double = 56.5
scala> printf("I am %d years old and weight %.1f Kg.","Li Lie",i,f)
I am 34 years old and weight 56.5 Kg.

print()、println()和printf() 都在對象Predef中定義,該對象默認情況下被所有Scala程序引用,因此可以直接使用Predef對象提供的方法,而無需使用scala.Predef.的形式。
s字符串和f字符串:Scala提供的字符串插值機制,以方便在字符串字面量中直接嵌入變量的值。

基本語法:s " …$變量名… "  或 f " …$變量名%格式化字符… "
scala> val i=10
i: Int = 10
scala> val f=3.5
f: Double = 3.5452
scala> val s="hello"
s: String = hello
scala> println(s"$s:i=$i,f=$f")    //s插值字符串
hello:i=10,f=3.5452
scala> println(f"$s:i=$i%-4d,f=$f%.1f")   //f插值字符串
hello:i=10  ,f=3.5

2.讀寫文件

  • 寫入文件
    Scala需要使用java.io.PrintWriter實現把數據寫入到文件,PrintWriter類提供了printprintln兩個寫方法
scala> import java.io.PrintWriter
scala> val outputFile = new PrintWriter("test.txt")
scala> outputFile.println("Hello World")
scala> outputFile.print("Spark is good")
scala> outputFile.close()
  • 讀取文件
    可以使用scala.io.SourcegetLines方法實現對文件中所有行的讀取

在這裏插入圖片描述

2.2.3 控制結構

  1. if條件表達式
  2. while循環
  3. for循環
  4. 異常處理
  5. 對循環的控制

1. if條件表達式

if (表達式) {
	語句塊1
}
else {
	語句塊2
}

在這裏插入圖片描述
在這裏插入圖片描述
有一點與Java不同的是,Scala中的if表達式的值可以賦值給變量

在這裏插入圖片描述
2. while循環

while (表達式){
		循環體
}
do{
		循環體
}while (表達式)

在這裏插入圖片描述
在這裏插入圖片描述
3. for循環
基本語法

for (變量 <- 表達式) {語句塊}

其中,“變量<-表達式”被稱爲“生成器(generator)”

在這裏插入圖片描述

  • “守衛(guard)”的表達式:過濾出一些滿足條件的結果。基本語法:
for (變量 <- 表達式 if 條件表達式) 語句塊

在這裏插入圖片描述

  • Scala也支持“多個生成器”的情形,可以用分號把它們隔開,比如:

在這裏插入圖片描述

  • for推導式:for結構可以在每次執行的時候創造一個值,然後將包含了所有產生值的集合作爲for循環表達式的結果返回,集合的類型由生成器中的集合類型確定
for (變量 <- 表達式) yield {語句塊}

在這裏插入圖片描述
4. 異常處理
Scala不支持Java中的“受檢查異常”(checked exception),將所有異常都當作“不受檢異常”(或稱爲運行時異常)
Scala仍使用try-catch結構來捕獲異常

import java.io.FileReader 
import java.io.FileNotFoundException 
import java.io.IOException 
try { 
  val f = new FileReader("input.txt") 
	// 文件操作 
} catch { 
  case ex: FileNotFoundException => 
	// 文件不存在時的操作 
  case ex: IOException => 
   // 發生I/O錯誤時的操作
} finally { 
  file.close() // 確保關閉文件 
} 

5. 對循環的控制
爲了提前終止整個循環或者跳到下一個循環,Scala沒有break和continue關鍵字
Scala提供了一個Breaks類(位於包scala.util.control)。Breaks類有兩個方法用於對循環結構進行控制,即breakable和break

在這裏插入圖片描述

import util.control.Breaks._ //導入Breaks類的所有方法
val array = Array(1,3,10,5,4)
breakable{
for(i<- array){
       if(i>5) break //跳出breakable,終止for循環,相當於Java中的break
println(i)
    }
}
// 上面的for語句將輸出1,3
 
for(i<- array){
    breakable{
        if(i>5) break 
                            //跳出breakable,終止當次循環,相當於Java的continue       println(i)
    }
}// 上面的for語句將輸出1,3,5,4

2.2.4 數據結構
1.數組(Array)
2.元組(Tuple)
3.容器(Collection)
4.序列(Sequence)
5.集合(Set)
6.映射(Map)
7.迭代器(Iterator)

1.數組(Array)

  • 數組:一種可變的、可索引的、元素具有相同類型的數據集合
  • Scala提供了參數化類型的通用數組類Array[T],其中T可以是任意的Scala類型,可以通過顯式指定類型或者通過隱式推斷來實例化一個數組

聲明一個整型數組

val intValueArr = new Array[Int](3)  //聲明一個長度爲3的整型數組,每個數組元素初始化爲0
intValueArr(0) = 12 //給第1個數組元素賦值爲12
intValueArr(1) = 45  //給第2個數組元素賦值爲45
intValueArr(2) = 33 //給第3個數組元素賦值爲33

聲明一個字符串數組

val myStrArr = new Array[String](3) //聲明一個長度爲3的字符串數組,每個數組元素初始化爲null
 myStrArr(0) = "BigData"
 myStrArr(1) = "Hadoop"
 myStrArr(2) = "Spark"
 for (i <- 0 to 2) println(myStrArr(i))

可以不給出數組類型,Scala會自動根據提供的初始化數據來推斷出數組的類型

val intValueArr = Array(12,45,33)
val myStrArr = Array("BigData","Hadoop","Spark")

多維數組的創建:調用Array的ofDim方法

val  myMatrix = Array.ofDim[Int](3,4)  //聲明一個3行4列的二維數組,類型實際就是Array[Array[Int]]
val  myCube = Array.ofDim[String](3,2,4)  //聲明一個多維數組,類型實際是Array[Array[Array[Int]]]

可以使用多級圓括號來訪問多維數組的元素,例如myMatrix(0)(1)返回第一行第二列的元素

2. 元組(Tuple)

  • 元組是對多個不同類型對象的一種簡單封裝。定義元組最簡單的方法就是把多個元素用逗號分開並用圓括號包圍起來。
    使用下劃線“_”加上從1開始的索引值,來訪問元組的元素
    在這裏插入圖片描述
    如果需要在方法裏返回多個不同類型的對象,Scala可以通過返回一個元組來實現
    3.容器(Collection)
    Scala提供了一套豐富的容器(collection)庫,包括序列(Sequence)、集合(Set)、映射(Map)
    Scala用了三個包來組織容器類,分別是
    scala.collection 、scala.collection.mutable和scala.collection.immutable
    scala.collection
    封裝了可變容器和不可變容器的超類或特質,定義了可變容器和不可變容器的一些通用操作

scala.collection包中容器的宏觀層次結構

在這裏插入圖片描述
4.序列(Sequence)
序列(Sequence): 元素可以按照特定的順序訪問的容器。序列中每個元素均帶有一個從0開始計數的固定索引位置
序列容器的根是collection.Seq特質。其具有兩個子特質 LinearSeq和IndexedSeqLinearSeq序列具有高效的 head 和 tail 操作,而IndexedSeq序列具有高效的隨機存儲操作
實現了特質LinearSeq的常用序列有列表(List)和隊列(Queue)。實現了特質IndexedSeq的常用序列有可變數組(ArrayBuffer)和向量(Vector)

-序列(Sequence)——列表(List)

  • 列表: 一種共享相同類型的不可變的對象序列。定義在scala.collection.immutable包中
    不同於Javajava.util.List****,scala的List一旦被定義,其值就不能改變,因此聲明List時必須初始化
var strList=List("BigData","Hadoop","Spark")
  • 列表有頭部和尾部的概念,可以分別使用head和tail方法來獲取
  • head返回的是列表第一個元素的值
  • tail返回的是除第一個元素外的其它值構成的新列表,這體現出列表具有遞歸的鏈表結構
  • strList.head將返回字符串”BigData”,strList.tail返回List (“Hadoop”,“Spark”)
  • 構造列表常用的方法是通過在已有列表前端增加元素,使用的操作符爲::,例如:
val otherList="Apache"::strList

執行該語句後strList保持不變,而otherList將成爲一個新的列表:

List("Apache","BigData","Hadoop","Spark")

Scala還定義了一個空列表對象Nil,藉助Nil,可以將多個元素用操作符::串起來初始化一個列表
Scala還定義了一個空列表對象Nil,藉助Nil,可以將多個元素用操作符::串起來初始化一個列表

val intList = 1::2::3::Nil  與val intList = List(1,2,3)等效

注意:除了head、tail操作是常數時間O(1),其它按索引訪問的操作都需要從頭開始遍歷,因此是線性時間複雜度O(N)。

序列(Sequence)——向量(Vector)
Vetor可以實現所有訪問操作都是常數時間。

scala> val vec1=Vector(1,2)
vec1: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)
scala> val vec2 = 3 +: 4 +: vec1
vec2: scala.collection.immutable.Vector[Int] = Vector(3, 4, 1, 2)
scala> val vec3 = vec2 :+ 5
vec3: scala.collection.immutable.Vector[Int] = Vector(3, 4, 1, 2, 5)
scala> vec3(3)
res6: Int = 2

序列(Sequence)——Range

  • Range類:一種特殊的、帶索引的不可變數字等差序列。其包含的值爲從給定起點按一定步長增長(減小)到指定終點的所有數值
  • Range可以支持創建不同數據類型的數值序列,包括Int、Long、Float、Double、Char、BigInt和BigDecimal等

(1)創建一個從1到5的數值序列,包含區間終點5,步長爲1

scala> val r=new Range(1,5,1)在這裏插入圖片描述在這裏插入圖片描述
(2)創建一個從1到5的數值序列,不包含區間終點5,步長爲1
在這裏插入圖片描述
(3)創建一個從1到10的數值序列,包含區間終點10,步長爲2
在這裏插入圖片描述
(4)創建一個Float類型的數值序列,從0.5f到5.9f,步長爲0.3f

在這裏插入圖片描述
5.集合(Set)

  • 集合(set):不重複元素的容器(collection)
  • 列表(List)中的元素是按照插入的先後順序來組織的,但是,“集合”中的元素並不會記錄元素的插入順序,而是以“哈希”方法對元素的值進行組織,所以,它允許你快速地找到某個元素
    集合包括可變集和不可變集,分別位於scala.collection.mutable包和scala.collection.immutable包,缺聲明情況下創建的是不可變集
var mySet = Set("Hadoop","Spark")
mySet += "Scala" 

如果要聲明一個可變集,則需要提前引入scala.collection.mutable.Set

import scala.collection.mutable.Set
val myMutableSet = Set("Database","BigData")
myMutableSet += "Cloud Computing" 

6.映射(Map)

  • 映射(Map):一系列鍵值對的容器。鍵是唯一的,但值不一定是唯一的。可以根據鍵來對值進行快速的檢索
  • Scala 的映射包含了可變的和不可變的兩種版本,分別定義在包scala.collection.mutable 和scala.collection.immutable 裏。默認情況下,Scala中使用不可變的映射。如果想使用可變映射,必須明確地導入scala.collection.mutable.Map
val university = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University","PKU"->"Peking University")

如果要獲取映射中的值,可以通過鍵來獲取

println(university("XMU"))

對於這種訪問方式,如果給定的鍵不存在,則會拋出異常,爲此,訪問前可以先調用contains方法確定鍵是否存在

val xmu = if (university.contains("XMU")) university("XMU") else 0 println(xmu)

可變的映射

import scala.collection.mutable.Map
val university2 = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University","PKU"->"Peking University")
university2("XMU") = "Ximan University" //更新已有元素的值
university2("FZU") = "Fuzhou University" //添加新元素

也可以使用**+=**操作來添加新的元素

university2 += ("TJU"->"Tianjin University") //添加一個新元素
university2 += ("SDU"->"Shandong University","WHU"->"Wuhan University") //同時添加兩個新元素

7.迭代器(Iterator)

  • 迭代器(Iterator)不是一個容器,而是提供了按順序訪問容器元素的數據結構
  • 迭代器包含兩個基本操作:next和hasNextnext可以返回迭代器的下一個元素,hasNext用於檢測是否還有下一個元素
val iter = Iterator("Hadoop","Spark","Scala")
while (iter.hasNext) {
    println(iter.next())
}

建議:除next和hasnext方法外,在對一個迭代器調用了某個方法後,不要再次使用該迭代器

2.3 Scala面向對象編程基礎
2.3.1 類
2.3.2 對象
2.3.3 繼承
2.3.4 特質
2.3.5 模式匹配
2.3.6 包

2.3.1 類

  1. 類的定義
  2. 類成員的可見性
  3. 方法的定義方式
  4. 構造器

1.類的定義

class Counter{
       //這裏定義類的字段和方法
}

字段定義:用val或var關鍵字進行定義

def 方法名(參數列表):返回結果類型={方法體}
class Counter {
  var value = 0
  def increment(step:Int):Unit = { value += step}
  def current():Int = {value}
}

使用new關鍵字創建一個類的實例

val myCounter = new Counter
myCounter.value = 5 //訪問字段
myCounter. increment(3) //調用方法
println(myCounter.current) //調用無參數方法時,可以省略方法名後的括號

2. 類成員的可見性
Scala類中所有成員的默認可見性爲公有,任何作用域內都能直接訪問公有成員

除了默認的公有可見性,Scala也提供private和protected,其中,private成員只對本類型和嵌套類型可見;protected成員對本類型和其繼承類型都可見

爲了避免直接暴露public字段,建議將字段設置爲private,對於private字段,Scala採用類似Java中的getter和setter方法,定義了兩個成對的方法value和value_= 進行讀取和修改

scala> :load /usr/local/scala/mycode/Counter.scala
Loading /usr/local/scala/mycode/Counter.scala…
defined class Counter
scala> val myCounter = new Counter
myCounter: Counter = Counter@f591271
scala> myCounter.value_=(3) //爲privateValue設置新的值
scala> println(myCounter.value)//訪問privateValue的當前值
3

Scala語法中有如下規範,當編譯器看到以value和value=這種成對形式出現的方法時,它允許用戶去掉下劃線,而採用類似賦值表達式的形式

myCounter.value= 3 // 等效於myCounter.value_=(3)

3. 方法的定義方式
基本語法:

def 方法名(參數列表):返回結果類型={方法體}

方法參數前不能加上val或var,所有的方法參數都是不可變類型
無參數的方法定義時可以省略括號,這時調用時也不能帶有括號;如果定義時帶有括號,則調用時可以帶括號,也可以不帶括號
方法名後面的圓括號()可以用大括號{}來代替
如果方法只有一個參數,可以省略點號(.)而採用中綴操作符調用方法
如果方法體只有一條語句,可以省略方法體兩邊的大括號

scala> :load /usr/local/scala/mycode/Counter1.scala
Loading /usr/local/scala/mycode/Counter1.scala…
defined class Counter
scala> val c=new Counter
c: Counter = Counter@30ab4b0e
scala> c increment 5 //中綴調用法
scala> c.getValue()     //getValue定義中有括號,可以帶括號調用
res0: Int = 0
scala> c.getValue // getValue定義中有括號,也可不帶括號調用
res1: Int = 0
scala> c.current() // current定義中沒有括號,不可帶括號調用
<console>:13: error: Int does not take parameters
       c.current()
                ^
scala> c.current  // current定義中沒有括號,只能不帶括號調用
res3: Int = 0

當方法的返回結果類型可以從最後的表達式推斷出時,可以省略結果類型
如果方法返回類型爲Unit,可以同時省略返回結果類型和等號,但不能省略大括號

class Counter {
    var value = 0
    def increment(step:Int) { value += step }//賦值表達式的值爲Unit類型
    def current()= value //根據value的類型自動推斷出返回類型爲Int型
}

4.構造器
Scala類的定義主體就是類的構造器,稱爲主構造器。在類名之後用圓括號列出主構造器的參數列表
主構造器的參數前可以使用val或var關鍵字,Scala內部將自動爲這些參數創建私有字段,並提供對應的訪問方法

scala> class Counter(var name:String) //定義一個帶字符串參數的簡單類
defined class Counter
scala> var mycounter = new Counter("Runner")
mycounter: Counter = Counter@17fcc4f7
scala> println(mycounter.name) //調用讀方法
Runner
scala> mycounter.name_=("Timer") //調用寫方法
scala> mycounter.name = "Timer"// 更直觀地調用寫方法,和上句等效
mycounter.name: String = Timer

如果不希望將構造器參數成爲類的字段,只需要省略關鍵字var或者val
Scala類可以包含零個或多個輔助構造器(auxiliary constructor)。輔助構造器使用this進行定義,this的返回類型爲Unit
每個輔助構造器的第一個表達式必須是調用一個此前已經定義的輔助構造器或主構造器,調用的形式爲“this(參數列表)”

//代碼文件爲/usr/local/scala/mycode/Counter2.scala
class Counter {
    private var value = 0 
    private var name = ""
    private var step = 1 //計算器的默認遞進步長
    println("the main constructor")
    def this(name: String){ //第一個輔助構造器
        this() //調用主構造器
        this.name = name
        printf("the first auxiliary constructor,name:%s\n",name)
    }
    def this (name: String,step: Int){ //第二個輔助構造器
        this(name) //調用前一個輔助構造器
        this.step = step
       printf("the second auxiliary constructor,name:%s,step:%d\n",name,step)
    }
    def increment(step: Int): Unit = { value += step}
    def current(): Int = {value}
}
scala> :load /usr/local/scala/mycode/Counter2.scala
Loading /usr/local/scala/mycode/Counter2.scala…
defined class Counter
scala> val c1=new Counter
the main constructor
c1: Counter = Counter@319c6b2
 
scala> val c2=new Counter("the 2nd Counter")
the main constructor
the first auxiliary constructor,name:the 2nd Counter
c2: Counter = Counter@4ed6c602
 
scala> val c3=new Counter("the 3rd Counter",2)
the main constructor
the first auxiliary constructor,name:the 3rd Counter
the second auxiliary constructor,name:the 3rd Counter,step:2
c3: Counter = Counter@64fab83b

2.3.2 對象
1.單例對象
2.apply方法
3.update方法
4.unapply方法

1.單例對象
Scala採用單例對象singleton object)來實現與Java靜態成員同樣的功能
使用object 關鍵字定義單例對象,且單例對象的使用與一個普通的類實例一樣:

//代碼文件爲/usr/local/scala/mycode/Person.scala
object Person {
    private var lastId = 0  //一個人的身份編號
    def newPersonId() = {
        lastId +=1
        lastId
    }
}
scala> :load /usr/local/scala/mycode/Person.scala
Loading /usr/local/scala/mycode/Person.scala…
defined object Person
scala> printf("The first person id: %d.\n",Person.newPersonId())
The first person id: 1.
scala> printf("The second person id:%d.\n",Person.newPersonId())
The second person id:2.
scala> printf("The third person id: %d.\n",Person.newPersonId())
The third person id: 3.

伴生對象和孤立對象
當一個單例對象和它的同名類一起出現時,這時的單例對象被稱爲這個同名類的“伴生對象”(companion object)。相應的類被稱爲這個單例對象的“伴生類”
類和它的伴生對象必須存在於同一個文件中,可以相互訪問私有成員
沒有同名類的單例對象,被稱爲孤立對象standalone object)。一般情況下,Scala程序的入口點main方法就是定義在一個孤立對象裏

//代碼文件爲/usr/local/scala/mycode/Person1.scala
class Person(val name:String){
    private val id = Person.newPersonId() //調用了伴生對象中的方法
    def info() {
        printf("The id of %s is %d.\n",name,id)
    }
}
object Person {
    private var lastId = 0  //一個人的身份編號
    def newPersonId() = {
        lastId +=1
        lastId
    }
    def main(args: Array[String]) {
        val person1 = new Person("Lilei")
        val person2 = new Person("Hanmei")
        person1.info()
        person2.info()
    }
}
$ scalac /usr/local/scala/mycode/Person1.scala
$ scala –classpath . Person
The id of Lilei is 1.
The id of Hanmei is 2.

2. apply方法
思考下行代碼的執行過程:

val myStrArr = Array("BigData","Hadoop","Spark")

Scala自動調用Array類的伴生對象Array中的一個稱爲apply的方法,來創建一個Array對象myStrArr
apply方法調用約定:用括號傳遞給類實例或單例對象名一個或多個參數時,Scala 會在相應的類或對象中查找方法名爲apply且參數列表與傳入的參數一致的方法,並用傳入的參數來調用該apply方法
例:類中的apply方法

//代碼文件爲/usr/local/scala/mycode/TestApplyClass.scala
class TestApplyClass {
    def apply(param: String){
	     println("apply method called: " + param)
}
}
scala> :load /usr/local/scala/mycode/TestApplyClass.scala
Loading /usr/local/scala/mycode/TestApplyClass.scala…
defined class TestApplyClass
scala> val myObject = new TestApplyClass
myObject: TestApplyClass = TestApplyClass@11b352e9
scala> myObject("Hello Apply")// 自動調用類中定義的apply方法,等同於下句
apply method called: Hello Apply
scala> myObject.apply("Hello Apply")  //手動調用apply方法
apply method called: Hello Apply

伴生對象中的apply方法:將所有類的構造方法以apply方法的形式定義在伴生對象中,這樣伴生對象就像生成類實例的工廠,而這些apply方法也被稱爲工廠方法

//代碼文件爲/usr/local/scala/mycode/MyTestApply.scala
class Car(name: String) {
    def info() {
        println("Car name is "+ name)
    }
}
object Car {
    def apply(name: String) = new Car(name) //調用伴生類Car的構造方法
}
object MyTestApply{
    def main (args: Array[String]) {
    val mycar = Car("BMW") //調用伴生對象中的apply方法
    mycar.info() //輸出結果爲“Car name is BMW”
    }
}

爲什麼要設計apply方法?

  • 保持對象和函數之間使用的一致性
  • 面向對象:“對象.方法” VS 數學:“函數(參數)”
  • Scala中一切都是對象,包括函數也是對象。Scala中的函數既保留括號調用樣式,也可以使用點號調用形式,其對應的方法名即爲apply
scala> def add=(x:Int,y:Int)=>x+y  //add是一個函數
add: (Int, Int) => Int
scala> add(4,5)   //採用數學界的括號調用樣式
res2: Int = 9
scala> add.apply(4,5) //add也是對象,採用點號形式調用apply方法
res3: Int = 9
  • Scala的對象也可以看成函數,前提是該對象提供了apply方法
//代碼文件爲/usr/local/scala/mycode/MyTestApply.scala
class Car(name: String) {
    def info() {
        println("Car name is "+ name)
    }
}
object Car {
    def apply(name: String) = new Car(name) //調用伴生類Car的構造方法
}
object MyTestApply{
    def main (args: Array[String]) {
    val mycar = Car("BMW") //調用伴生對象中的apply方法
    mycar.info() //輸出結果爲“Car name is BMW”
    }
}

3. update方法

-與apply方法類似的update方法也遵循相應的調用約定:當對帶有括號幷包括一到若干參數的對象進行賦值時,編譯器將調用對象的update方法,並將括號裏的參數和等號右邊的值一起作爲update方法的輸入參數來執行調用

scala>val myStrArr = new Array[String](3)  //聲明一個長度爲3的字符串數組,每個數組元素初始化爲null
scala>myStrArr(0) = "BigData" //實際上,調用了伴生類Array中的update方法,執行myStrArr.update(0,"BigData")
scala>myStrArr(1) = "Hadoop" //實際上,調用了伴生類Array中的update方法,執行myStrArr.update(1,"Hadoop")
scala>myStrArr(2) = "Spark" //實際上,調用了伴生類Array中的update方法,執行myStrArr.update(2,"Spark")

4. unapply方法

  • unapply方法用於對對象進行解構操作,與apply方法類似,該方法也會被自動調用
  • 可以認爲unapply方法是apply方法的反向操作,apply方法接受構造參數變成對象,而unapply方法接受一個對象,從中提取值
//代碼文件爲/usr/local/scala/mycode/TestUnapply.scala
class Car(val brand:String,val price:Int) {
    def info() {
        println("Car brand is "+ brand+" and price is "+price)
    }
}
object Car{
	def apply(brand:String,price:Int)= {
                          println("Debug:calling apply ... ")
                          new Car(brand,price)
                  }
	def unapply(c:Car):Option[(String,Int)]={
                           println("Debug:calling unapply ... ")
                           Some((c.brand,c.price))
                 }
}
object TestUnapply{
      def main (args: Array[String]) {
               var Car(carbrand,carprice) = Car("BMW",800000)
               println("brand: "+carbrand+" and carprice: "+carprice)
       }
}

2.3.3 繼承

  1. 抽象類
  2. 擴展類
  3. Scala的類層次結構
  4. Option類

1. 抽象類

如果一個類包含沒有實現的成員,則必須使用abstract關鍵詞進行修飾,定義爲抽象類

abstract class Car(val name:String) {
    val carBrand:String //字段沒有初始化值,就是一個抽象字段
    def info() //抽象方法
    def greeting() {
        println("Welcome to my car!")
    }
}

關於上面的定義,說明幾點:
(1)定義一個抽象類,需要使用關鍵字abstract
(2)定義一個抽象類的抽象方法,不需要關鍵字abstract,只要把方法體空着,不寫方法體就可以
(3)抽象類中定義的字段,只要沒有給出初始化值,就表示是一個抽象字段,但是,抽象字段必須要聲明類型,否則編譯會報錯
2. 擴展類
Scala只支持單一繼承,而不支持多重繼承。在類定義中使用extends關鍵字表示繼承關係。定義子類時,需要注意:
重載父類的抽象成員(包括字段和方法)時,override關鍵字是可選的;而重載父類的非抽象成員時,override關鍵字是必選的
只能重載val類型的字段,而不能重載var類型的字段。因爲var類型本身就是可變的,所以,可以直接修改它的值,無需重載
在這裏插入圖片描述
編譯執行後,結果爲:

在這裏插入圖片描述
3.Scala的類層次結構

在這裏插入圖片描述

  • Null是所有引用類型的子類,其唯一的實例爲null,表示一個“空”對象,可以賦值給任何引用類型的變量,但不能賦值給值類型的變量
  • Nothing是所有其它類型的子類,包括Null。Nothing沒有實例,主要用於異常處理函數的返回類型
    4.Option類
  • Scala提供null是爲了實現在JVM與其它Java庫的兼容性,但是,除非明確需要與Java庫進行交互,否則,Scala建議儘量避免使用這種可能帶來bug的null,而改用Option類
  • Option是一個抽象類,有一個具體的子類Some和一個對象None,其中,前者表示有值的情形,後者表示沒有值
    當方法不確定是否有對象返回時,可以讓返回值類型爲Option[T],其中,T爲類型參數。對於這類方法,如果確實有T類型的對象需要返回,會將該對象包裝成一個Some對象並返回;如果沒有值需要返回,將返回None
scala> case class Book(val name:String,val price:Double)
defined class Book
scala> val books=Map("hadoop"->Book("Hadoop",35.5),
     | "spark"->Book("Spark",55.5),
     | "hbase"->Book("Hbase",26.0)) //定義一個書名到書對象的映射
books: scala.collection.immutable.Map[String,Book] =… 
scala> books.get("hadoop") //返回該鍵所對應值的Some對象
res0: Option[Book] = Some(Book(Hadoop,35.5))
scala> books.get("hive") // 不存在該鍵,返回None對象
res1: Option[Book] = None
scala> books.get("hadoop").get //Some對象的get方法返回其包裝的對象
res2: Book = Book(Hadoop,35.5)
scala> books.get("hive").get // None對象的get方法會拋出異常
java.util.NoSuchElementException: None.get
  …
scala> books.get("hive").getOrElse(Book("Unknown name",0))
res4: Book = Book(Unknown name,0.0)

2.3.4 特質(trait)
1.特質概述
2. 特質的定義
3. 把特質混入類中
4. 把多個特質混入類中

1.特質概述

  • Java中提供了接口,允許一個類實現任意數量的接口
  • Scala中沒有接口的概念,而是提供了“特質(trait)”,它不僅實現了接口的功能,還具備了很多其他的特性
  • Scala的特質是代碼重用的基本單元,可以同時擁有抽象方法和具體方法
  • Scala中,一個類只能繼承自一個超類,卻可以實現多個特質,從而重用特質中的方法和字段,實現了多重繼承

2.特質的定義
使用關鍵字trait定義特質

trait Flyable {
          var maxFlyHeight:Int  //抽象字段
          def fly() //抽象方法
          def breathe(){ //具體的方法
                println("I can breathe")
         }
 }
  • 特質既可以包含抽象成員,也可以包含非抽象成員。包含抽象成員時,不需要abstract關鍵字
  • 特質可以使用extends繼承其它的特質
    3. 把特質混入類中
    可以使用extends或with關鍵字把特質混入類中
  • 如果特質中包含抽象成員,則該類必須爲這些抽象成員提供具體實現,除非該類被定義爲抽象類
class Bird(flyHeight:Int) extends Flyable{
     var maxFlyHeight:Int = flyHeight  //重載特質的抽象字段
     def fly(){
            printf("I can fly at the height of %d.",maxFlyHeight)
      } //重載特質的抽象方法
}
  • 把上面定義的特質Flyable和類Bird封裝到一個代碼文件Bird.scala中:
//代碼文件爲/usr/local/scala/mycode/Bird.scala
trait Flyable {
       var maxFlyHeight:Int  //抽象字段
       def fly() //抽象方法
       def breathe(){ //具體的方法
              println("I can breathe")
       }
 }

class Bird(flyHeight:Int) extends Flyable{

       var maxFlyHeight:Int = flyHeight  //重載特質的抽象字段
       def fly(){
            printf("I can fly at the height of %d",maxFlyHeight)
       } //重載特質的抽象方法
}

在Scala REPL中執行如下代碼並觀察效果:

scala> :load /usr/local/scala/mycode/Bird.scala
Loading /usr/local/scala/mycode/Bird.scala...
defined trait Flyable
defined class Bird
 
scala> val b=new Bird(100)
b: Bird = Bird@43a51d00
 
scala> b.fly()
I can fly at the height of 100
scala> b.breathe()
I can breathe
  • 如果要混入多個特質,可以連續使用多個with
    //代碼文件爲/usr/local/scala/mycode/Bird2.scala
trait Flyable {
       var maxFlyHeight:Int  //抽象字段
       def fly() //抽象方法
       def breathe(){ //具體的方法
             println("I can breathe")
       }
 }
trait HasLegs {
       val legs:Int   //抽象字段
       def move(){printf("I can walk with %d legs",legs)}
}
class Animal(val category:String){
       def info(){println("This is a "+category)}
}
class Bird(flyHeight:Int) extends Animal("Bird") with Flyable with HasLegs{
         var maxFlyHeight:Int = flyHeight //重載特質的抽象字段
         val legs=2 //重載特質的抽象字段
         def fly(){
               printf("I can fly at the height of %d",maxFlyHeight)
         }//重載特質的抽象方法
}

可以在Scala REPL中執行如下代碼查看執行效果:

scala> :load /usr/local/scala/mycode/Bird2.scala
Loading /usr/local/scala/mycode/Bird2.scala...
defined trait Flyable
defined trait HasLegs
defined class Animal
defined class Bird
 
scala> val b=new Bird(108)
b: Bird = Bird@126675fd
scala> b.info
This is a Bird
scala> b.fly
I can fly at the height of 108
scala> b.move
I can walk with 2 legs

2.3.5 模式匹配
1.match語句
2. case類

1. match語句
最常見的模式匹配是match語句,
match語句用在當需要從多個分支中進行選擇的場景

//代碼文件爲/usr/local/scala/mycode/TestMatch.scala
import scala.io.StdIn._
println("Please input the score:")
val grade=readChar()
grade match{
	case 'A' => println("85-100")
	case 'B' => println("70-84")
	case 'C' => println("60-69")
	case 'D' => println("<60")
	case _ => println("error input!")
}
  • 通配符**_**相當於Java中的default分支
  • match結構中不需要break語句來跳出判斷,Scala從前往後匹配到一個分支後,會自動跳出判斷
  • case後面的表達式可以是任何類型的常量,而不要求是整數類型
//代碼文件爲/usr/local/scala/mycode/TestMatch1.scala
import scala.io.StdIn._
println("Please input a country:")
val country=readLine()
country match{
		case "China" => println("中國")
		case "America" => println("美國")
		case "Japan" => println("日本")
		case _ => println("我不認識!")
}
  • 除了匹配特定的常量,還能匹配某種類型的所有值
    //代碼文件爲/usr/local/scala/mycode/TestMatch2.scala
for (elem <- List(6,9,0.618,"Spark","Hadoop",'Hello)){
	val str  = elem match  {
	    case i: Int => i + " is an int value."//匹配整型的值,並賦值給i
	    case d: Double => d + " is a double value." //匹配浮點型的值
	    case "Spark"=>"Spark is found." //匹配特定的字符串
	    case s: String => s + " is a string value." //匹配其它字符串
	    case _ =>"unexpected value:"+ elem  //與以上都不匹配
              }
              println(str)
}
6 is an int value.
9 is an int value.
0.618 is a double value.
Spark is found.
Hadoop is a string value.
unexpected value:'Hello
  • 可以在match表達式的case中使用守衛式guard)添加一些過濾邏輯
//代碼文件爲/usr/local/scala/mycode/TestMatch3.scala
for (elem <- List(1,2,3,4)){
     elem match {
           case _ if (elem%2==0) => println(elem + " is even.")
           case _ => println(elem + " is odd.")
     }
}
 1 is odd.
2 is even.
3 is odd.
4 is even.
4

2. case類

  • case類是一種特殊的類,它們經過優化以被用於模式匹配
  • 當定義一個類時,如果在class關鍵字前加上case關鍵字,則該類稱爲case類
  • Scala爲case類自動重載了許多實用的方法,包括toString、equals和hashcode方法
  • Scala爲每一個case類自動生成一個伴生對象,其包括模板代碼
    1個apply方法,因此,實例化case類的時候無需使用new關鍵字
    1個unapply方法,該方法包含一個類型爲伴生類的參數,返回的結果是Option類型,對應的類型參數是N元組,N是伴生類中主構造器參數的個數。Unapply方法用於對對象進行解構操作,在case類模式匹配中,該方法被自動調用,並將待匹配的對象作爲參數傳遞給它

例如,假設有如下定義的一個case類:

case class Car(brand: String, price: Int)

則編譯器自動生成的伴生對象是:

object  Car{
            def apply(brand:String,price:Int)= new Car(brand,price)
            def unapply(c:Car):Option[(String,Int)]=Some((c.brand,c.price))
}
//代碼文件爲/usr/local/scala/mycode/TestCase.scala
case class Car(brand: String, price: Int) 
val myBYDCar = Car("BYD", 89000)
val myBMWCar = Car("BMW", 1200000)
val myBenzCar = Car("Benz", 1500000)
for (car <- List(myBYDCar, myBMWCar, myBenzCar)) {
       car match{
              case Car("BYD", 89000) => println("Hello, BYD!")
              case Car("BMW", 1200000) => println("Hello, BMW!")
              case Car(brand, price) => println(“Brand:”+ brand +, Price:”+price+, do you want it?”)   
       }
 }
Hello, BYD!
Hello, BMW!
Brand:Benz, Price:1500000, do you want it?

2.3.6 包

  • 包的定義
  • 引用包成員

1… 包的定義

  • 爲了解決程序中命名衝突問題,Scala也和Java一樣採用包(package)來層次化、模塊化地組織程序
package  autodepartment
class MyClass
  • 爲了在任意位置訪問MyClass類,需要使用autodepartment.MyClass
    通過在關鍵字package後面加大括號,可以將程序的不同部分放在不同的包裏。這樣可以實現包的嵌套,相應的作用域也是嵌套的
package xmu {
       package autodepartment {
            class ControlCourse{
		...
           }
      }
     package csdepartment {
           class  OSCourse{
                val cc = new autodepartment.ControlCourse
           }
     }
}

2.引用包成員

  • 可以用import 子句來引用包成員,這樣可以簡化包成員的訪問方式
import xmu.autodepartment.ControlCourse
class MyClass{
	var myos=new ControlCourse
}
  • 使用通配符下劃線()引入類或對象的所有成員
    import scala.io.StdIn.

    var i=readInt()
    var f=readFloat()
    var str=readLine()
  • Scala 隱式地添加了一些引用到每個程序前面,相當於每個Scala程序都隱式地以如下代碼開始:
import java.lang._
import scala._
import Predef._

2.4 Scala函數式編程基礎

  • 2.4.1 函數定義與使用
  • 2.4.2 高階函數
  • 2.4.3 針對容器的操作
  • 2.4.4 函數式編程實例WordCount

2.4.1 函數定義與使用
定義函數最通用的方法是作爲某個類或者對象的成員,這種函數被稱爲方法,其定義的基本語法爲

def 方法名(參數列表):結果類型={方法體}

字面量包括整數字面量、浮點數字面量、布爾型字面量、字符字面量、字符串字面量、符號字面量、函數字面量和元組字面量

  • 函數字面量可以體現函數式編程的核心理念
  • 在函數式編程中,函數是“頭等公民”,可以像任何其他數據類型一樣被傳遞和操作,也就是說,函數的使用方式和其他數據類型的使用方式完全一致了
  • 這時,我們就可以像定義變量那樣去定義一個函數,由此導致的結果是,函數也會和其他變量一樣,開始有“值”
  • 就像變量的“類型”和“值”是分開的兩個概念一樣,函數式編程中,函數的“類型”和“值”也成爲兩個分開的概念,函數的“值”,就是“函數字面量”

下面一點點引導大家更好地理解函數的“類型”和“值”的概念
現在定義一個大家比較熟悉的傳統類型的函數,定義的語法和我們之前介紹過的定義“類中的方法”類似(實際上,定義函數最常用的方法是作爲某個對象的成員,這種函數被稱爲方法):

def counter(value: Int): Int = { value += 1}

上面定義個這個函數的“類型”如下:

(Int) => Int

實際上,只有多個參數時(不同參數之間用逗號隔開),圓括號纔是必須的,當參數只有一個時,圓括號可以省略,如下:

Int => Int

上面就得到了函數的“類型”
下面看看如何得到函數的“值”、
實際上,我們只要把函數定義中的類型聲明部分去除,剩下的就是函數的“值”,如下:

(value) => {value += 1} //只有一條語句時,大括號可以省略

注意:上面就是函數的“值”,需要注意的是,採用“=>”而不是“=”,這是Scala的語法要求
現在,我們再按照大家比較熟悉的定義變量的方式,採用Scala語法來定義一個函數。
聲明一個變量時,我們採用的形式是:

val num: Int = 5

照葫蘆畫瓢,我們也可以按照上面類似的形式來定義Scala中的函數:

val counter: Int => Int = { (value) => value += 1 }

從上面可以看出,在Scala中,函數已經是“頭等公民”,單獨剝離出來了“值”的概念,一個函數“值”就是函數字面量。這樣,我們只要在某個需要聲明函數的地方聲明一個函數類型,在調用的時候傳一個對應的函數字面量即可,和使用普通變量一模一樣
我們不需要給每個函數命名,這時就可以使用匿名函數,如下:

(num: Int) => num * 2

上面這種匿名函數的定義形式,我們經常稱爲“Lambda表達式”。“Lambda表達式”的形式如下:

(參數) => 表達式
//如果參數只有一個,參數的圓括號可以省略

我們可以直接把匿名函數存放到變量中,下面是在Scala解釋器中的執行過程:

scala> val myNumFunc: Int=>Int = (num: Int) => num * 2 
myNumFunc: Int => Int = <function1> //這行是執行返回的結果
scala> println(myNumFunc(3)) 
//myNumFunc函數調用的時候,需要給出參數的值,這裏傳入3
6

實際上,Scala具有類型推斷機制,可以自動推斷變量類型,比如下面兩條語句都是可以的:

val number: Int = 5
val number =5 //省略Int類型聲明

所以,上面的定義中,我們可以myNumFunc的類型聲明,也就是去掉“Int=>Int”,在Scala解釋器中的執行過程如下:

scala> val myNumFunc = (num: Int) => num * 2
myNumFunc: Int => Int = <function1>
scala> println(myNumFunc(3))
6

下面我們再嘗試一下,是否可以繼續省略num的類型聲明,在Scala解釋器中的執行過程如下:
在這裏插入圖片描述
可以看出,解釋器會報錯,因爲,全部省略以後,實際上,解釋器也無法推斷出類型

  • 當函數的每個參數在函數字面量內僅出現一次,可以省略“=>”並用下劃線“_”作爲參數的佔位符來簡化函數字面量的表示,第一個下劃線代表第一個參數,第二個下劃線代表第二個參數,依此類推
scala> val counter = (_:Int) + 1 //有類型時括號不能省略,等效於“x:Int=>x+1”
counter: Int => Int = <function1>
scala> val add = (_:Int) + (_:Int) //等效於“(a:Int,b:Int)=>a+b”
add: (Int, Int) => Int = <function2>
scala> val m1=List(1,2,3)
m1: List[Int] = List(1, 2, 3)
scala>val m2=m1.map(_*2)//map接受一個函數作爲參數,相當於“m1.map(x=>x*2)”,參數的類型可以根據m1的元素類型推斷出,所以可以省略。
m2: List[Int] = List(2, 4, 6)

2.4.2 高階函數

  • 高階函數:當一個函數包含其它函數作爲其參數或者返回結果爲一個函數時,該函數被稱爲高階函數

例:假設需要分別計算從一個整數到另一個整數的“連加和”、“平方和”以及“2的冪次和”

方案一:不採用高階函數

def powerOfTwo(x: Int): Int = {if(x == 0) 1 else 2 * powerOfTwo(x-1)}
def sumInts(a: Int, b: Int): Int = {
		if(a > b) 0 else a + sumInts(a + 1, b)
}
def sumSquares(a: Int, b: Int): Int = {
		if(a > b) 0 else a*a + sumSquares(a + 1, b)
}
def sumPowersOfTwo(a: Int, b: Int): Int = {
		if(a > b) 0 else powerOfTwo(a) + sumPowersOfTwo(a+1, b)
}

方案二:採用高階函數

def sum(f: Int => Int, a: Int, b: Int):Int = {
	if(a > b) 0 else f(a) + sum(f, a+1, b)
}
scala> sum(x=>x,1,5) //直接傳入一個匿名函數
//且省略了參數x的類型,因爲可以由sum的參數類型推斷出來
res8: Int = 15
scala> sum(x=>x*x,1,5) //直接傳入另一個匿名函數
res9: Int = 55
scala> sum(powerOfTwo,1,5) //傳入一個已經定義好的方法
res10: Int = 62

2.4.3 針對容器的操作

1 遍歷操作
2 映射操作
3 過濾操作
4 規約操作

1.遍歷操作

Scala容器的標準遍歷方法foreach

def foreach[U](f: Elem => U) :Unit
scala> val list = List(1, 2, 3)
list: List[Int] = List(1, 2, 3)
scala> val f=(i:Int)=>println(i)
f: Int => Unit = <function1>
scala> list.foreach(f)
1
2
3

簡化寫法:“list foreach(i=>println(i))”或“list foreach println”

scala> val university = Map("XMU" ->"Xiamen University", "THU" ->"Tsinghua University","PKU"->"Peking University")
university: scala.collection.mutable.Map[String,String] = ...
scala> university foreach{kv => println(kv._1+":"+kv._2)}
XMU:Xiamen University
THU:Tsinghua University
PKU:Peking University

簡化寫法:

university foreach{x=>x match {case (k,v) => println(k+":"+v)}}
university foreach{case (k,v) => println(k+":"+v)}

2 映射操作

  • 映射是指通過對容器中的元素進行某些運算來生成一個新的容器。兩個典型的映射操作是map方法和flatMap方法
  • map方法(一對一映射):將某個函數應用到集合中的每個元素,映射得到一個新的元素,map方法會返回一個與原容器類型大小都相同的新容器,只不過元素的類型可能不同
scala> val books =List("Hadoop","Hive","HDFS")
books: List[String] = List(Hadoop, Hive, HDFS)
scala> books.map(s => s.toUpperCase)
//toUpperCase方法將一個字符串中的每個字母都變成大寫字母
res56: List[String] = List(HADOOP, HIVE, HDFS)
scala> books.map(s => s.length) //將字符串映射到它的長度
res57: List[Int] = List(6, 4, 4) //新列表的元素類型爲Int
  • flatMap方法(一對多映射):將某個函數應用到容器中的元素時,對每個元素都會返回一個容器(而不是一個元素),然後,flatMap把生成的多個容器“拍扁”成爲一個容器並返回。返回的容器與原容器類型相同,但大小可能不同,其中元素的類型也可能不同
scala> books flatMap (s => s.toList)
res58: List[Char] = List(H, a, d, o, o, p, H, i, v, e, H, D, F, S)

3 過濾操作

  • 過濾:遍歷一個容器,從中獲取滿足指定條件的元素,返回一個新的容器
  • filter方法:接受一個返回布爾值的函數f作爲參數,並將f作用到每個元素上,將f返回真值的元素組成一個新容器返回
scala> val university = Map("XMU" ->"Xiamen University", "THU" ->"Tsinghua University","PKU"->"Peking University","XMUT"->"Xiamen University of Technology")
university: scala.collection.immutable.Map[String,String] = ...
 
//過濾出值中包含“Xiamen”的元素,contains爲String的方法
scala> val xmus = university filter {kv => kv._2 contains "Xiamen"}
universityOfXiamen: scala.collection.immutable.Map[String,String] = Map(XMU -> Xiamen University, XMUT -> Xiamen University of Technology)
 
scala> val l=List(1,2,3,4,5,6) filter {_%2==0}
//使用了佔位符語法,過濾能被2整除的元素
l: List[Int] = List(2, 4, 6)

4 規約操作

  • 規約操作:是對容器元素進行兩兩運算,將其“規約”爲一個值
  • reduce方法:接受一個二元函數f作爲參數,首先將f作用在某兩個元素上並返回一個值,然後再將f作用在上一個返回值和容器的下一個元素上,再返回一個值,依此類推,最後容器中的所有值會被規約爲一個值
scala> val list =List(1,2,3,4,5)
list: List[Int] = List(1, 2, 3, 4, 5)
scala>  list.reduce(_ + _) //將列表元素累加,使用了佔位符語法
res16: Int = 15
scala>  list.reduce(_ * _) //將列表元素連乘
res17: Int = 120
scala> list map (_.toString) reduce ((x,y)=>s"f($x,$y)")
res5: String = f(f(f(f(1,2),3),4),5) //f表示傳入reduce的二元函數

reduceLeft和reduceRight:前者從左到右進行遍歷,後者從右到左進行遍歷

在這裏插入圖片描述

scala> val list = List(1,2,3,4,5)
list: List[Int] = List(1, 2, 3, 4, 5)
scala> list reduceLeft {_-_}
res24: Int = -13
scala> list reduceRight {_-_}
res25: Int = 3
scala> val s = list map (_.toString)  //將整型列表轉換成字符串列表
s: List[String] = List(1, 2, 3, 4, 5)
scala> s reduceLeft {(accu,x)=>s"($accu-$x)"}
res28: String = ((((1-2)-3)-4)-5)//list reduceLeft{_-_}的計算過程
scala> s reduceRight {(x,accu)=>s"($x-$accu)"}
res30: String = (1-(2-(3-(4-5))))//list reduceRight{_-_}的計算過程
  • fold方法:一個雙參數列表的函數,從提供的初始值開始規約。第一個參數列表接受一個規約的初始值,第二個參數列表接受與reduce中一樣的二元函數參數
    foldLeft和foldRight:前者從左到右進行遍歷,後者從右到左進行遍歷
    在這裏插入圖片描述
scala> val list =List(1,2,3,4,5)
list: List[Int] = List(1, 2, 3, 4, 5)
scala> list.fold(10)(_*_)
res32: Int = 1200
scala> (list fold 10)(_*_) //fold的中綴調用寫法
res33: Int = 1200
scala> (list foldLeft 10)(_-_)//計算順序(((((10-1)-2)-3)-4)-5)
res34: Int = -5 
scala> (list foldRight 10)(_-_) //計算順序(1-(2-(3-(4-(5-10)))))
res35: Int = -7

2.4.4 函數式編程實例WordCount

import java.io.File
import scala.io.Source
//導入要用到映射Map
import collection.mutable.Map
//定義一個單例對象
object WordCount {
        //定義入口函數main函數
        def main(args: Array[String]) {
            //定義好要讀取的文件對象
            val dirfile=new File("testfiles")
            //列出目錄下的所有文件
            val files  = dirfile.listFiles
            //生成一個map對象,開始時爲空
            val results = Map.empty[String,Int]
            //對每個文件進行遍歷
            for(file <-files) {
                //傳進遍歷的文件到data
                val data= Source.fromFile(file)
                //遍歷每個元素形成一個集合
                val strs =data.getLines.flatMap{s =>s.split(" ")}
                //做詞頻統計
                strs foreach { word =>
                    if (results.contains(word))
                    results(word)+=1 else  results(word)=1
                    }
                }
                //統計結果進行打印
            results foreach{case (k,v) => println(s"$k:$v")}
        }
}

關於spark編程基礎的教程視頻可以參考此鏈接 且下面是關於此主講教師林子雨簡介
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章