一、函數式編程
1.1 概述
函數式編程(FP,即Functional Programming)也是近幾年才逐漸為人們所知,但它并不是新的概念。函數式編程語言的鼻主Lisp語言的產生時間甚至比C語言還早,它擁有和面向對象編程(OOP)幾乎等長的歷史。
函數式編程,其實就是以 純函數 的方式編寫代碼。
純函數:一個函數在程序的執行過程中除了根據輸入參數給出運算結果之外沒用其他影響,就可以說是沒有 副作用 的,我們就可以將這一類函數稱之為純函數。
純函數最核心的目的是為了編寫無副作用的代碼,它的很多特性,包括不變量,惰性求值等等都是為了這個目標。
函數式編程的優點
- 可以以更少的代碼實現同樣的功能,可極大的提升生產效率
- 更容易編寫多并發或多線程的應用,更易于編寫利用多核的應用程序
- 可以幫助寫出健壯的代碼
- 更容易寫出易于閱讀、理解的優雅代碼
函數式編程的基本特征
- 函數是一等公民:函數也有數據類型,函數與其他數據類型的變量或值一樣,處于平等地位,可以賦值給其它變量,也可以作為函數參數,傳入另一個函數,或者作為別的函數的返回值。
-
不可變數據:所有的狀態(或變量)都是不可變的。你可以聲明一個狀態,但是不能改變這個狀態。如果要變化,只能復制一個。
純函數式編程語言不使用任何可變數據結構或變量。但在Scala等編程語言中,即支持不可變的數據結構或變量,也支持可變的。 -
函數沒有 “副作用”:函數要保持獨立,一旦函數的輸入確定,輸出就是確定的,函數的執行不會影響系統的狀態,不會修改外部狀態。
如果函數沒有副作用,那函數的執行就可以緩存起來了,一旦函數執行過一次,如果再次執行,當輸入和前面一樣的情況下,就直接可以用前面執行的輸出結果,就不用再次運算了,可大大提高程序運行的效率。 - 一切皆是表達式:在函數式編程語言中,每一個語句都是一個表達式,都會有返回值。
1.2 基本語法
函數和方法的區別
object TestFunction {
// (2)方法可以進行重載和重寫,程序可以執行
def main(): Unit = {
}
def main(args: Array[String]): Unit = {
// (1)Scala語言的語法非常靈活,可以在任何的語法結構中聲明任何的語法
import java.util.Date
new Date()
// (2)函數沒有重載和重寫的概念,程序報錯
def test(): Unit ={
println("無參,無返回值")
}
test()
def test(name:String):Unit={
println()
}
//(3)scala中函數可以嵌套定義
def test2(): Unit ={
def test3(name:String):Unit={
println("函數可以嵌套定義")
}
}
}
}
1.3 函數的參數
// (1) 可變參數
def test(s: String*): Unit = {
println(s)
}
// 有輸入參數:輸出Array
test("Hello", "Scala")
// 無輸入參數:輸出List()
test()
// (2) 如果參數列表中存在多個參數,name可變參數一般放置在后面
def test2(name: String, s: String*): Unit = {
println(name + "," + s)
}
// (3) 默認參數,一般情況下,將有默認值的參數放置在參數列表的后面
def test3(name: String, age: Int = 30): Unit = {
println(s"$name, $age")
}
// 如果要使用默認,在調用的時候,可以省略這個參數
test3("jerry")
// 如果參數傳遞了值,那么會覆蓋默認值
test3("tom", 20)
// (4) 命名參數
def test4(name: String, age: Int = 30, gender: String): Unit = {
println(s"$name, $age, $gender")
}
// 可以指明參數名稱,進行傳參
test4("alex", gender = "male")
1.4 函數的至簡原則
- 函數體內可以省略 return,Scala 會自動把最后一行代碼作為返回值
若函數使用 return 關鍵字,那么函數就不能自行推斷了,需要聲明返回值類型 - 返回值如果能夠推斷出來,那么可以省略
- 函數體只有一行代碼,括號可以省略
- 若函數無參數,聲明函數時可以省略小括號;若聲明函數時省略小括號,則調用該函數時,也需要省略小括號
- 若函數明確聲明 Unit,那么即使函數體中使用 return 關鍵字也不起作用
- Scala 如果想要自動推斷無返回值,可以省略等號
// 將無返回值的函數稱之為過程
def f7() {
println("dalang")
}
- 如果不關心名稱,只關心邏輯處理,那么函數名(def)可以省略,省略名字的函數稱為匿名函數
- 純函數:純函數天然的支持高并發!
純函數的特點:1. 不產生副作用(常見的副作用:打印到控制臺,修改了外部變量的值,向磁盤寫入文件...)2. 引用透明(函數的返回值,只和形參有關,和其它任何的值沒有關系) - 惰性求值:類似于懶加載
val a = { 10 } // 立即賦值
lazy val b = { 20 } // 惰性求值,調用時賦值一次,以后不用賦值
def c = 30 // 每次調用都會賦值一次
1.5 高階函數
參數或返回值為函數的函數稱為高階函數(高階算子)
//高階函數 ———— 函數作為參數
def calculator(a: Int, b: Int, operater: (Int, Int) => Int): Int = {
operater(a, b)
}
//函數(求和)
def plus(x: Int, y: Int): Int = {
x + y
}
// 函數(求積)
def multiply(x: Int, y: Int): Int = {
x * y
}
//函數作為參數
println(calculator(2, 3, plus))
println(calculator(2, 3, multiply))
1.6 匿名函數
沒有名字的函數就是匿名函數,直接通過函數字面量(lambda表達式:( ) => x + y)來表示匿名函數
匿名函數的簡寫:當傳入的參數只被使用了一次時,可以使用 _ 指代,多個 _ 代表多個參數。
object TestFunction {
//高階函數 ———— 函數作為參數
def calculator(a: Int, b: Int, operator: (Int, Int) => Int): Int = {
operator(a, b)
}
//函數————求和
def plus(x: Int, y: Int): Int = {
x + y
}
def main(args: Array[String]): Unit = {
//函數作為參數
println(calculator(2, 3, plus))
//匿名函數作為參數
println(calculator(2, 3, (x: Int, y: Int) => x + y))
//匿名函數簡寫形式
println(calculator(2, 3, _ + _))
}
}
1.7 函數閉包&柯里化
閉包:如果一個函數, 訪問到了它的外部(局部)變量的值, 那么這個函數和他所處的環境, 稱為閉包
//外部變量
var x: Int = 10
//閉包
def f(x: Int, y: Int): Int = {
x + y
}
函數柯里化:將接收多個參數的函數轉化成接受一個參數的函數過程,可以理解為把函數的參數列表的多個參數, 變成多個參數列表一個參數的過程。
// 1.原始函數,一次傳入兩個函數
def add1(a:Int, b:Int): Int = a + b
// 2.將原函數分解成兩層函數,外函數傳入第一個參數,內函數使用外函數的參數,同時還需要在傳入一個函數,
// 這樣其實是將一次傳入兩個參數的函數解耦,變成內外兩個每次接收一個參數的函數。
def add2(a: Int): Int => Int = {
(b: Int) => a + b
}
// 3.第二步中add2(a:Int)返回一個函數,那么直接將(b:Int)作為其參數列表就可以簡寫成如下:
def add3(a: Int)(b: Int) = a + b
// 同時,add3可以看做一個函數,而 (a:Int)(b:Int) 是它的兩個函數列表,但是每次只接收一個參數,這就是函數最終柯里化的狀態。
// 理解柯里化:將接收多個參數的函數轉化成接受一個參數的函數過程
1.8 遞歸函數
一個函數/方法在函數/方法體內又調用了本身,我們稱之為遞歸調用
遞歸函數四要素:
- 函數調用自身
- 函數必須要有跳出的邏輯
- 函數的遞歸過程要逼近跳出邏輯
- 遞歸函數的返回值要手動聲明
// 求 10 的階乘
def factorial(n: Long): Long = {
if (n == 1) 1
else n * factorial(n - 1)
}
1.9 惰性函數
當函數返回值被定義為 lazy 時,函數的執行將被推遲,直到我們首次調用此值,該函數才會被執行,這種函數稱之為惰性函數。
def main(args: Array[String]): Unit = {
lazy val res = sum(10, 30) // 注意:lazy 不能修飾 var 類型的變量
println("----------------")
println("res=" + res)
}
def sum(n1: Int, n2: Int): Int = {
println("sum被執行。。。")
return n1 + n2
}
// 輸出結果
----------------
sum被執行。。。
res=40
1.10 值調用&名調用
- 值調用:把計算后的值傳入
println(3 + 4)
- 名調用:把一段代碼傳入,調用時,運行代碼,調用幾次運行幾次
def main(args: Array[String]): Unit = {
// 注意:這里調用時,要使用 a() 作為參數才可以代表a接收到的匿名函數,而 a 函數 本身內部什么都沒有定義
foo(a())
}
// 定義 a 返回一個匿名函數 () => {}
def a:() => Int = () => {
println("f...")
10
}
// 將 a 以名調用的方式傳入foo中,實際是把匿名函數 () => {} 傳入 foo 中
def foo(a: => Int): Unit = {
println(a)
println(a)
println(a)
}
// 輸出結果
f...
10
f...
10
f...
10
1.11 控制抽象
Scala 中可以自己定義類似于 if-else,while 的流程控制語句,即所謂的控制抽象。
提示:scala 中 { code...... } 結構稱為代碼塊(block),可視為無參函數,作為 =>Unit 類型的參數值。
def main(args: Array[String]): Unit = {
var i =2
// 調用自己寫的循環,這里傳入兩段代碼
// myWhile({i <= 100})({println(i += 1);i += 1}) 可行
// 最終寫法:
myWhile(i <= 100) {
println(i += 1)
i += 1
}
}
// 使用名調用、柯里化傳入兩個段代碼,這兩段代碼都在調用時執行
def myWhile (condition: => Boolean)(op: => Unit): Unit = {
if (condition) {
op
myWhile(condition)(op)
}
}
以上就是一個自定義循環函數。
二、面向對象
2.1 Scala 的包
- 包的聲明方式
// 1.直接在文件首行聲明,和java一樣
package com.alibaba.scalamaben.obj
// 2. 在代碼中嵌套聲明
package com {
package alibaba {
.....
}
}
// 這種方式可以使一個源文件中聲明多個 package; 子包的類可以直接訪問父包中的內容,而無需導包
// 但是很少使用
- 包對象
可以為包定義同名的包對象,定義在包對象中的成員,作為其對應包下所有 class 和 object 的共享變量,可以被直接訪問。
package object com{
val shareValue="share"
def shareMethod()={}
}
- 導包
普通導入、通配符導入、起別名導入、屏蔽類、指定多個類導入、局部導入
// 1.通配符導入util下的所有類
import java.util._
// 2.給類起別名,給 ArrayList 起別為JAL,防止類名沖突
import java.util.{ArrayList => JAL}
// 3.屏蔽類:導入util下的所有類,但不包含ArrayList
import java.util.{ArrayList => _, _}
// 4.導入多個類,指定導入某幾個類
import java.util.{HashSet, ArrayList}
object PckDemo2 {
// 5.局部導入,在局部免去寫 List
import java.util.List
def foo() {
}
}
- Scala 中默認導入的包
scala._
scala.Predef._
java.lang._
- 權限修飾符
Scala 中屬性和方法的默認訪問權限為 'public',Scala 中不加修飾符即為 'public'
private 為私有權限,只在類的內部和伴生對象中可用
protected 為受保護權限,Scala 中受保護權限更為嚴格,在同類、子類中可以訪問,同包內無法訪問
private [ 包名 ] 增加包訪問權限,包名下的其他類也可以使用
2.2 類和對象
- 類的屬性
class User(var name: String, val age: Int, sex: String)
// var 修飾的 name 會有 scala 形式的 get/set 方法
// val 修飾的 age,由于不可變,只有 get 方法
// sex 則只是一個私有屬性,沒有對外使用的方法
實際生產中,很多 java 框架會利用反射調用 get/set 方法,有時候為了兼容這些框架,會為 scala 的屬性設置 java 形式的 get和set 方法(通過@BeanProperty注解實現)。
class User(@BeanProperty var name: String, @BeanProperty val age: Int, sex: String)
// var 類型的屬性會額外的生成 java 形式的 get/set 方法
// val 類型的屬性只會額外生成 java 形式的 get 方法
// 當類中的屬性不加 var/val 時,該屬性默認為私有屬性,沒有公共的 get/set 方法,也無法在外部訪問。
- 類的方法
語法:def 方法名 (參數列表) [: 返回值類型] = { 方法體 }
// 類中的方法
class Person {
def sum(n1:Int, n2:Int) : Int = {
n1 + n2
}
}
// 對象中的方法
object Person {
def main(args: Array[String]): Unit = {
val person = new Person()
println(person.sum(10, 20))
}
}
- 創建對象
語法:var / val 對象名 [: 類型] = new 類型 ( )
val 修飾的對象,不能改變對象的引用地址,可以改變對象的屬性的值。
var 修飾的對象,可以修改對象的引用和屬性值。
val person = new Person()
person.name = 'aa'
person.name = 'bb'
person = new Person() // person 不可變,故報錯
- 構造器
Scala 類的構造器包含:主構造器 和 輔助構造器
主構造器:
1.主構造器只能有一個,當沒有形參時可以省略 ( )
2.主構造器的 形參自動成為類的屬性
輔助構造器:
1.輔助構造器的函數名為,多個輔助構造器之間構成重載
2.輔助構造器 首行必須直接或間接的調用主構造器
3.輔助構造器調用其他輔助構造器的收后面的調用前面的
object ObjDemo1 {
def main(args: Array[String]): Unit = {
val person2 = new Person(18)
}
}
class Person (val name: String, var age: Int, sex: String) {
var name: String = "lisi"
var age: Int = _ // 默認值 0
def this(age: Int) {
this()
this.age = age
println("輔助構造器")
}
def this(age: Int, sex: String) {
this(age)
// this.name = name 柱構造器中 val 修飾,無法修改
this.sex = sex
}
println("主構造器") // 主構造中的語句先被執行
}
2.3 三大特征
- 封裝,同 java
- 繼承,Scala 是單繼承,函數和屬性都是動態綁定,同時支持函數和屬性的覆寫
函數的覆寫:必須添加 override 關鍵字(java 中可以省略)
屬性的覆寫:只支持覆寫 val 類型,而不支持 var 類型
繼承的構造器調用:只有子類的主構造器才有權利去調用父類的構造器
object Test {
def main(args: Array[String]): Unit = {
new Emp("z3", 11, 1001)
}
}
class Person(nameParam: String) {
var name = nameParam
var age: Int = _
def this(nameParam: String, ageParam: Int) {
this(nameParam)
this.age = ageParam
println("父類輔助構造器")
}
println("父類主構造器")
}
class Emp(nameParam: String, ageParam: Int) extends Person(nameParam, ageParam) {
var empNo: Int = _
def this(nameParam: String, ageParam: Int, empNoParam: Int) {
this(nameParam, ageParam)
this.empNo = empNoParam
println("子類的輔助構造器")
}
println("子類主構造器")
}
----------------輸出
父類主構造器
父類輔助構造器
子類主構造器
子類的輔助構造器
觀察以上例子在創建子類對象時,構造器的調用順序。
- 多態
父類(編譯時類型) = 子類(運行時類型)
子類的對象賦值給父類的引用
val person: Person = new Emp("李四", "23")
2.4 抽象屬性和抽象方法
- 抽象屬性和抽象方法
抽象類: abstract class Person {} // 通過 abstract 關鍵字標記抽象類
抽象方法: val/var name: String // 一個屬性沒有初始化,就是抽象屬性
抽象方法: def hello(): String // 只聲明,沒有函數體
覆寫抽象方法時可以不加 override
abstract class Person {
val name: String
var age: Int
def hello(): Unit
}
class Teacher extends Person {
// 覆寫 val 類型需要加 override
override val name: String = "lzc"
var age = 1
// 覆寫抽象方法可以不加 override
def hello(): Unit = {
println(s"hello $name")
}
}
- 抽象內部類(匿名子類)
和 Java 一樣,Scala 可以通過包含帶有定義或重寫的代碼的方式創建一個匿名的子類
abstract class Person {
val name: String
def hello(): Unit
}
object Test {
def main(args: Array[String]): Unit = {
// 創建匿名內部類的對象,這個類是 Person 的實現類
val person = new Person {
override val name: String = "teacher"
override def hello(): Unit = println("hello teacher")
}
}
}
2.5 單例對象(伴生對象)
- 伴生對象與伴生類
Scala語言是完全面向對象的語言,所以并沒有靜態的操作(即在Scala中沒有靜態的概念)。而是產生了一種特殊的對象來模擬靜態類對象,該對象為單例對象。若在一個文件中單例對象名與類名一致,則該單例對象與這個類與互為 伴生對象和伴生類,這個類的所有“靜態”內容都可以放置在它的伴生對象中聲明。
因此,在 Scala 中, 單例模式已經在語言層面完成了解決。創建一個單例要使用關鍵字 object,因為不能實例化一個單例對象,所以不能傳遞參數給它的構造器。
伴生對象和伴生類必須位于同一文件中,伴生對象和伴生類可以相互訪問私有成員 - apply 方法
使用伴生對象 ( 參數 )的形式,其實是在調用伴生對象的 apply(參數) 的方法
通過伴生對象的 apply 方法,可以實現不使用 new 方法來創建對象。
apply方法 可以重載
object SingleDemo1 {
def main(args: Array[String]): Unit = {
// 默認調用 aplly 方法,不使用new創建對象
Car("red", "JAPAN")
Car("blue")
}
}
// 伴生對象
object Car {
def apply(color: String, country: String):Car = {
val car = new Car(color, country) // 調用伴生類的私有化構造器
car // 返回對象
}
// 重載的 apply 方法
def apply(color: String): Car = new Car(color) // 調用伴生類的私有化構造器
}
// 伴生類,參數列表前加 private 私有化主構造器
class Car private (val color: String, country: String = "CHNIA"){
println(s"a $color Car made in $country")
}
---------------- 輸出
a red Car made in JAPAN
a blue Car made in CHNIA
- 小結
1.Scala 中伴生對象采用 object 關鍵字聲明,所謂的伴生對象其實就是類的靜態方法和靜態屬性的集合
2.伴生對象對應的類稱之為伴生類,伴生對象的名稱應該和伴生類名一致,且在同一個源碼文件中
3.伴生對象中的屬性和方法都可以通過伴生對象名(類名)直接調用訪問
4.類和伴生對象之間可以互相訪問對方的私有成員
5.apply 方法,可以實現不使用 new 方法來創建對象
2.6 特質(trait)
- 特質的基本使用
Scala 是純面向對象的語言,在 Scala 中,沒有接口,采用特質trait
(特征, 特質)來代替接口的概念,當多個類具有相同的特質時,就可以將這個特質獨立出來,采用關鍵字trait
聲明。
特質可以有抽象方法, 也可以有實體方法, 相比抽象類最大的優點是特質可以實現多繼承, 抽象類是只能實現單繼承。
特質可以有非抽象方法
一個類已經繼承了一個類,或已經混入了一個特質,要再混入一個特質時應該使用 with
覆寫特質的方法時可以不加 override
object TraitDemo {
def main(args: Array[String]): Unit = {
val dog = new Dog
dog.move("lag")
}
}
trait Animal {
// 1.特質也可以有非抽象的方法
def run(useWhat: String): Unit = {
println(s"an Animal run in $useWhat")
}
}
trait Creature {
def move(useWhat: String)
}
// 2.已經繼承了一個類,或已經混入了一個特質,要再混入一個特質時應該使用 with
class Dog extends Animal with Creature {
// 3.覆寫特質的方法時可以不加 override
def move(useWhat: String): Unit = {
println(s"a dog move in $useWhat")
}
}
- 特質的動態混入
除了可以在類聲明時繼承特質以外,還可以在構建對象時混入特質
,擴展目標類的功能。
動態混入是 Scala 特有的方式,動態混入可以在不影響原有的繼承關系的基礎上,給指定的類擴展功能
object TraitDemo2 {
def main(args: Array[String]): Unit = {
val mysql = new Mysql with BetterConnectDB // 創建 Mysql 對象的時候, 指定一個新的特質
mysql.connectToMysql()
// 如果再創建新的對象, 也可以換另外的特質 ———— 這就叫動態混入
}
}
trait ConnectToDB { // 這個特質用來連接數據庫
def getConn() {} // 什么都不做的實現方法
}
class Mysql extends ConnectToDB { // 連接到Mysql
def connectToMysql(): Unit = {
// 獲取連接
getConn()
//其他代碼
}
}
// 上面的 getConn其實什么都沒有做, 感覺沒有什么用
// 但是我們創建 Mysql對象的時候, 可以給他指定一個更好的特質
trait BetterConnectDB extends ConnectToDB { // 一個更好的特質
override def getConn(): Unit = { // 注意這個時候需要添加override
println("更好的獲取連接....")
}
}
- 疊加特質
構建對象的同時如果混入多個特質,稱之為疊加特質
object OverflowTrait {
def main(args: Array[String]): Unit = {
val test = new Test with A with B with C // 疊加特質
test.foo() // 最先調用 C 中的 foo 方法(C 又會掉 B中的 foo 一次類推,直到調用到 Fater 中的 foo)
}
}
class Test { }
trait Father{
def foo(): Unit ={
println("Father...")
}}
trait A extends Father{
override def foo(): Unit = { // 以下省略每個 trait 中的 foo 方法(自行腦補)
println("A...")
super.foo() // 不想按照順序向上找, 可以直接指定該特質的直接父特質 super[Father].foo()
}}
trait B extends Father{
override def foo(): Unit = {
println("B...")
super.foo()
}}
trait C extends Father{
override def foo(): Unit = {
println("C...")
super.foo()
}}
---------------輸出
C...
B...
A...
Fater...
一個對象混入多個特質,且特質的構建順序(從上到下)和混入的順序一致(從左到右),對象中的 foo 方法一定是最后一個特質的方法。
如果不想按照順序向上找, 可以直接指定該特質的直接父特質super[Father].foo()
- 特質中的具體字段
特質中的字段可以是抽象的也可以是具體的
混入該特質(或者繼承該特質)的類就具有了該字段,字段不是繼承,而是直接加入類,成為自己的字段。 - 特質繼承類
Scala 中還有一種不太常用的手法: 特質繼承類
將來這個被繼承的類會自動成為所有混入了該特質的類的直接超類。
注:如果混入該特質的類,已經繼承了另一個類(A類),則要求A類是特質超類的子類,否則就會出現了多繼承現象,發生錯誤 - 自身類型
當特質A 繼承類B 的時候, 編譯器能夠確保的是所有混入該特質A 的類都認類B 作為自己的超類。
Scala 還有另外一套機制也可以保證這一點:自身類型(self type)
trait Logger {
// 聲明該特質就是 Exception,后面的 getMessage 才可以調用
this: Exception => // 表示混入該特質的類必須繼承 Exception 或 Exception 的子類
def log(): Unit = {
println(getMessage)
}
}
class Console extends Exception with Logger {
}
2.7 補充
- 類型檢測和轉換
obj.isInstanceOf [ T ] 判斷 obj 是不是 T 類型
obj.asInstanceOf [ T ] 將 obj 強轉成 T 類型
classOf 獲取對象的類名 - 枚舉類和應用類
枚舉類:需要繼承 Enumeration
應用類:需要繼承 App - Type 定義新類型
使用 type 關鍵字可以定義新的數據數據類型名稱,本質上就是類型的一個別名
object Test {
def main(args: Array[String]): Unit = {
type S = String // 將 String 類型定義為 S 類型
var v: S = "abc"
def test(): S = "xyz"
}
}