1、 函數式編程內容及講課順序
1.1、函數式編程內容
函數式編程基礎
1、函數定義/聲明
2、函數運行機制
3、遞歸 // 難點 [最短路徑,郵差問題,迷宮問題]
4、過程
5、惰性函數和異常
函數式編程高級
1、值函數(函數字面量)
2、閉包
3、應用函數
4、抽象控制…
1.2、函數式編程授課順序
1、在scala中,函數式編程和麪向對象編程融合在一起,學習函數式編程式需要oop的知識,同樣學習oop需要函數式編程的基礎。
2、關係如下圖:
3、授課順序: 函數式編程基礎->面向對象編程->函數式編程高級
2、函數式編程內容
2.1、幾個概念的說明
在學習Scala中將方法、函數、函數式編程和麪向對象編程明確一下:
1、在scala中,方法和函數幾乎可以等同(比如他們的定義、使用、運行機制都一樣的),只是函數的使用方式更加的靈活多樣 [方法轉函數]。
2、函數式編程是從編程方式(範式)的角度來談的,可以這樣理解:函數式編程把函數當做一等公民,充分利用函數、 支持的函數的多種使用方式。
比如:
在Scala當中,函數是一等公民,像變量一樣,既可以作爲函數的參數使用,也可以將函數賦值給一個變量. ,函數的創建不用依賴於類或者對象,而在Java當中,函數的創建則要依賴於類、抽象類或者接口.
3、面向對象編程是以對象爲基礎的編程方式。
4、在scala中函數式編程和麪向對象編程融合在一起了 。
2.2、在學習Scala中將方法、函數、函數式編程和麪向對象編程關係分析圖
2.3、函數式編程小結
1、“函數式編程"是一種"編程範式”(programming paradigm)。
2、它屬於"結構化編程"的一種,主要思想是把運算過程儘量寫成一系列嵌套的函數調用。
3、函數式編程中,將函數也當做數據類型,因此可以接受函數當作輸入(參數)和輸出(返回值)。(增強了編程的粒度)
4、函數式編程中,最重要的就是函數。
3、爲什麼需要函數
4、函數的定義
4.1、基本語法
def 函數名 ([參數名: 參數類型], …)[[: 返回值類型] =] {
語句… //完成某個功能
return 返回值
}
1、函數聲明關鍵字爲def (definition)
2、[參數名: 參數類型], …:表示函數的輸入(就是參數列表), 可以沒有。 如果有,多個參數使用逗號間隔
3、函數中的語句:表示爲了實現某一功能代碼塊
4、函數可以有返回值,也可以沒有
5、返回值形式1: // def 函數名(參數列表) : 數據類型 = {函數體} // 返回值確定,清晰
6、返回值形式2: // def 函數名(參數列表) = {函數體} // 有返回值, 類型是推斷出來的
7、返回值形式3: // def 函數名(參數列表) {函數體} // 無返回值 Unit
8、如果沒有return ,默認以執行到最後一行的結果作爲返回值
4.2、快速入門案例
def main(args: Array[String]): Unit = {
val n1 = 10
val n2 = 20
println("res=" + getRes(1, 2, '+'))
}
// 定義函數/方法
def getRes(n1: Int, n2: Int, oper: Char) = {
if (oper == '+') {
n1 + n2 // 返回
} else if (oper == '-') {
n1 - n2
} else {
// 返回null
null
}
}
5、函數-調用機制
5.1、函數-調用過程
1、爲了讓大家更好的理解函數調用機制,看一個案例,並畫出示意圖,這個很重要,比如getSum計算兩個數的和,並返回結果
一個函數/方法在函數/方法體內又調用了本身,我們稱爲遞歸調用
當調用test(4) 和 test2(4) 上面兩段代碼分別輸出什麼?
遞歸調用並分析原因
5.2、函數遞歸調用的重要的規則和小結
1、程序執行一個函數時,就創建一個新的受保護的獨立空間(新函數棧楨)
2、函數的局部變量是獨立的,不會相互影響
3、遞歸必須向退出遞歸的條件逼近,否則就是無限遞歸,會造成死循環
4、當一個函數執行完畢,或者遇到return,就會返回,遵守誰調用,就將結果返回給誰。
5.3、使用Scala遞歸調用的應用案例
1、斐波那契數
使用遞歸的方式,求出斐波那契數1,1,2,3,5,8,13…
給你一個整數n,求出它的斐波那契數是多少?
println("fbn的結果是:" + fbn(7))
def fbn(n: Int): Int = {
// 分析
// 1、當n = 1 結果爲1
// 2、當n = 2 結果爲1
// 3、當n>2 時 結果就是前面兩個數的和
if (n == 1 || n == 2) {
1
} else {
fbn(n - 1) + fbn(n - 2)
}
}
2、求函數值
已知f(1) = 3 ; f(n)= 2 * f(n-1)+1
請使用遞歸的思想變成,求出f(n)的值?
println(f(2))
def f(n: Int): Int = {
if (n == 1) {
3
} else {
2 * f(n - 1) + 1
}
}
3、猴子喫桃子問題
有一堆桃子,猴子第一天吃了其中的一半,並再多吃了一個!以後每天猴子都喫其中的一半,然後再多喫一個,當到第10天時,想再喫時(還沒喫),發現只有一個桃子了。問題:最初有多少個桃子
思路分析:
1 day =10 桃子有 1
2 day = 9 桃子有 2*(day10[1] + 1)
3 day = 8 桃子有 (day9[4] + 1) * 2
println("最初有多少個桃子:" + peach(1))
def peach(day: Int): Int = {
if (day == 10) {
1
} else {
(peach(day + 1) + 1) * 2
}
}
5.4、函數注意事項和細節討論
1、函數的形參列表可以是多個, 如果函數沒有形參,調用時 可以不帶()
2、形參列表和返回值的數據類型可以是值類型和引用類型。
object Details01 {
def main(args: Array[String]): Unit = {
// 形參列表和返回值列表的數據類型可以是值類型和引用類型
val tiger = new Tiger
val tiger2 = test01(10, tiger)
println(tiger2.name) // jack
println(tiger.name)
println(tiger.hashCode()+" "+tiger2.hashCode())
}
def test01(n1: Int, tiger: Tiger): Tiger = {
println("n1=" + n1)
tiger.name = "jack"
tiger
}
}
class Tiger {
// 一個名字屬性
var name = ""
}
3、Scala中的函數可以根據函數體最後一行代碼自行推斷函數返回值類型。那麼在這種情況下,return關鍵字可以省略
4、因爲Scala可以自行推斷,所以在省略return關鍵字的場合,返回值類型也可以省略
5、如果函數明確使用return關鍵字,那麼函數返回就不能使用自行推斷了,這時要明確寫成 : 返回類型 = ,當然如果你什麼都不寫,即使有return 返回值爲()
def main(args: Array[String]): Unit = {
println(getSum2(10, 3)) // ()
}
// 如果寫了return,返回值類型就不能省略
def getSum(n1: Int,n2:Int):Int = {
return n1 + n2
}
// 如果返回值這裏什麼都沒有寫,即表示該函數沒有返回值
// 這時return 無效
def getSum2(n1: Int,n2:Int) {
return n1 + n2
}
6、如果函數明確聲明無返回值(聲明Unit),那麼函數體中即使使用return關鍵字也不會有返回值
println(GetSum(10,30))
// 如果函數明確聲明無返回值(聲明爲Unit),
// 那麼函數體中即使使用return關鍵字也不會有返回值
def GetSum(n1:Int,n2:Int):Unit={
return n1 + n2
}
7、如果明確函數無返回值或不確定返回值類型,那麼返回值類型可以省略(或聲明爲Any)
8、Scala語法中任何的語法結構都可以嵌套其他語法結構(靈活),即:函數/方法中可以再聲明/定義函數/方法,類中可以再聲明類
def main(args: Array[String]): Unit = {
def f1(): Unit = { // ok
println("f1")
}
println("ok~~")
def SayOk(): Unit = { // private final sayOk$1()
println("main sayok")
def SayOk(): Unit = { // private final sayOk$2()
println("main sayok")
}
}
}
def SayOk(): Unit = { // 成員
println("main sayok")
}
9、Scala函數的形參,在聲明函數時,直接賦初始值(默認值),這時調用函數時,如果沒有指定實參,則會使用默認值。如果指定了實參,則實參會覆蓋默認值
def main(args: Array[String]): Unit = {
println(sayOk("mary"))
}
// name形參的默認值爲jack
def sayOk(name: String = "jack"): String = {
return name +" "+ "ok!"
}
10、如果函數存在多個參數,每個參數都可以設定默認值,那麼這個時候,傳遞的參數到底是覆蓋默認值,還是賦值給沒有默認值的參數,就不確定了(默認按照聲明順序[從左到右])。在這種情況下,可以採用帶名參數[案列演示+練習]
def main(args: Array[String]): Unit = {
//mysqlCon()
//mysqlCon("127.0.0.1",7777) // 從左到右覆蓋
// 如果我們希望指定覆蓋某個默認值,則使用帶名參數即可,
// 比如修改用戶名和密碼
mysqlCon(user = "tom", pwd = "123")
// f6("v2") // x
f6(p2 = "v2") // √
}
def mysqlCon(add: String = "localhost", post: Int = 3306,
user: String = "root", pwd: String = "root"
): Unit = {
println("add=" + add)
println("post=" + post)
println("user=" + user)
println("pwd=" + pwd)
}
def f6(p1: String = "v1", p2: String) {
println(p1 + p2)
}
11、Scala 函數的形參默認是val的 ,因此不能再函數中修改
12、遞歸函數爲執行之前是無法推斷出來結果類型,在使用時必須有明確的返回值類型
def f8(n:Int)={ //? 錯誤,遞歸不能使用類型推斷,必須指定返回的數據類型
if(n <= 0)
1
else
n * f8(n - 1)
}
13、Scala 函數支持可變參數
// 支持0到多個參數
def sum(args: Int) : Int = {
}
// 支持1到多個參數
def sum(n1:Int,args: Int*) : Int = {
}
說明:
1、args是集合,通過for循環可以訪問到各個值
2、案列演示,編寫一個函數sum,可以求出1 到 多個Int的和
3、可變參數需要寫在形參列表的最後。
// 編寫一個函數sum,可以求出1 到 多個Int的和
println(sum(10, 30, 40, 3, 45, 7))
}
//
def sum(n1: Int, args: Int*): Int = {
println("args.length:" + args.length)
// 遍歷
var sum = n1
for (item <- args) {
sum += item
}
sum
}
/*// 可變參數需要放在最後
def sum2( args: Int*,n1: Int): Int =*/
5.5、函數練習題
object Hello01 {
def main(args: Array[String]): Unit = {
def f1 = "venassa"
println(f1) // venassa
}
}
6、過程
6.1、基本概念
將函數的返回類型爲Unit的函數稱之爲過程(procedure),如果明確沒有返回值,那麼等號可以省略
案例說明:
// f10 沒有返回值,可以使用Unit來說明
// 這時,這個函數我們也叫過程(procedure)
def f10(name:String) : Unit = {
println(name + "Hello")
}
6.2、注意事項
1)注意區分:如果函數聲明時沒有返回值類型,但是有=號,可以進行類型推斷最後一行代碼。這時這個函數實際是沒有返回值的,該函數並不是過程。(把=號去掉的)
2)開發工具的自動代碼補全功能,雖然會自動加上Unit,但是考慮到Scala語言的簡單,靈活,最好不加
7、惰性函數
7.1、看一個應用場景
惰性計算(儘可能延遲表達式求值)是許多函數式編程語言的特性。惰性集合在需要時提供其元素,無需預先計算它們,這帶來了一些好處。首先,您可以將耗時的計算推遲到絕對需要的時候。其次,您可以創造無限個集合,只要它們繼續收到請求,就會繼續提供元素。函數的惰性使用讓您能夠得到更高效的代碼。Java 並沒有爲惰性提供原生支持,Scala提供了。
7.2、畫圖說明[大數據推薦系統]
7.3、Scala中實現懶加載的代碼
當函數返回值被聲明爲lazy時,函數的執行將被推遲,直到我們首次對此取值,該函數纔會執行,這種函數我們稱之爲惰性函數,在Java 的某些框架代碼中稱爲懶加載(延遲加載)
def main(args: Array[String]): Unit = {
lazy val res = sum(10, 20)
println("··················")
println("res = " + res) // 在要使用res前,才執行
}
// sum函數,返回和
def sum(n1: Int, n2: Int) = {
println("sum()執行了") // 輸出一句話
n1 + n2
}
7.4、注意事項和細節說明:
1、lazy不能修飾var類型的變量
2、不但是 在調用函數時,加了 lazy ,會導致函數的執行被推遲,我們在聲明一個變量時,如果給聲明瞭 lazy ,那麼變量值得分配也會推遲。 比如 lazy val i = 10
8、異常處理
8.1、介紹
1、Scala提供try和catch塊來處理異常。try塊用於包含可能出錯的代碼。catch塊用於處理try塊中發生的異常。可以根據需要在程序中有任意數量的try…catch塊。
2、語法處理上和Java類似,但是又不盡相同
8.2、Java異常處理回顧
public class JavaExceptionDemo01 {
public static void main(String[] args) {
try {
// 可疑代碼
int i = 0;
int b = 10;
int c = b / i; //執行代碼時,會拋出ArithmeticException異常
} catch (Exception e) {
e.printStackTrace();
} finally {
// 最終執行的代碼
System.out.println("java Finally");
}
System.out.println("ok···繼續執行");
}
}
8.3、 Java異常處理的注意點
1、Java語言按照try-catch-catch…–finally的方式來處理異常
2、不管有沒有異常捕獲,都會執行finally,因此通常可以在finally代碼塊中釋放資源
3、可以有多個catch,分別捕獲對應的異常,這時需要把範圍小的異常類寫在前面,把範圍大的異常類寫在後面,否則編譯錯誤。會提示“Exception ‘java.lang.xxxxx’has already been caught "[案列演示]
8.4、Scala異常處理舉例
object ScalaExceptionDemo {
def main(args: Array[String]): Unit = {
try {
val r = 10 / 0
} catch {
// 說明
// 1.在Scala中只有一個catch
// 2.在catch中有多個case,每個case可以匹配一種異常
// 3. => 關鍵符號,表示後面是對該異常的處理代碼塊
// 4. finally 最終要執行的
case ex: ArithmeticException => {
println("捕獲了除數爲0的算術異常")
}
case ex: Exception => println("捕獲了異常")
} finally {
// 最終要執行的代碼
println("Scala finally")
}
println("ok···繼續執行")
}
}
8.5、Scala異常處理小結
1、我們將可疑代碼封裝在try塊中。 在try塊之後使用了一個catch處理程序來捕獲異常。如果發生任何異常,catch處理程序將處理它,程序將不會異常終止。
2、Scala的異常的工作機制和Java一樣,但是Scala沒有“checked(編譯期)”異常,即Scala沒有編譯異常這個概念,異常都是在運行的時候捕獲處理。
3、用throw關鍵字,拋出一個異常對象。所有異常都是Throwable的子類型。throw表達式是有類型的,就是Nothing,因爲Nothing是所有類型的子類型,所以throw表達式可以用在需要類型的地方
4、在Scala裏,借用了模式匹配的思想來做異常的匹配,因此,在catch的代碼裏,是一系列case子句來匹配異常。【前面案例可以看出這個特點, 模式匹配我們後面詳解】,當匹配上後 => 有多條語句可以換行寫,類似 java 的 switch case x: 代碼塊…
5、異常捕捉的機制與其他語言中一樣,如果有異常發生,catch子句是按次序捕捉的。因此,在catch子句中,越具體的異常越要靠前,越普遍的異常越靠後,如果把越普遍的異常寫在前,把具體的異常寫在後,在scala中也不會報錯,但這樣是非常不好的編程風格。
6、finally子句用於執行不管是正常處理還是有異常發生時都需要執行的步驟,一般用於對象的清理工作,這點和Java一樣。
7、Scala提供了throws關鍵字來聲明異常。可以使用方法定義聲明異常。 它向調用者函數提供了此方法可能引發此異常的信息。 它有助於調用函數處理並將該代碼包含在try-catch塊中,以避免程序異常終止。在scala中,可以使用throws註釋來聲明異常
9、函數的課堂練習題
1、編寫一個函數,從終端輸入一個整數(1—9),打印出對應的乘法表:
object Exercise01 {
def main(args: Array[String]): Unit = {
printf("請輸入一個數字(1~9)之間:")
val n = StdIn.readInt()
math(n)
}
// 編寫一個函數,輸出99乘法表
def math(n: Int) = {
for (i <- 1 to n) {
for (j <- 1 to i) {
printf("%d * %d = %d\t", j, i, i * j)
}
println()
}
}
}