一、Scala介紹
1、Scala語言特點
學習Scala是因為Spark框架就是有Scala編寫的,想學習Spark,首先需要對Scala有一定的了解。
Scala的語言特點:
Scala是一門以Java虛擬機(JVM)為運行環(huán)境并將面向對象和函數(shù)式編程的最佳特性結合在一起的靜態(tài)類型編程語言(靜態(tài)語言需要提前編譯的如:Java、c、c++等,動態(tài)語言如:js)。
(1)Scala是一門多范式的編程語言,Scala支持面向對象和函數(shù)式編程。(多范式,就是多種編程方法的意思。有面向過程、面向對象、泛型、函數(shù)式四種程序設計方法。)
(2)Scala源代碼(.scala)會被編譯成Java字節(jié)碼(.class),然后運行于JVM之上,并可以調用現(xiàn)有的Java類庫,實現(xiàn)兩種語言的無縫對接。
(3)Scala單作為一門語言來看,非常的簡潔高效。
2、Scala和Java的關系
可以說Scala就是在Java的基礎上,加上了自己函數(shù)式編程思想的語言。Scala的SDK中有Java的類庫,有Scala自己特有的類庫,也對Java的部分類庫做了特定的包裝。Scala同樣的經(jīng)過SDK編譯后,會生成.class文件,然后可以在各種環(huán)境(windows,linux,unix)的JVM上面運行。也能說Scala就是兼容Java的,所以Scala同時需要Java的JDK和自己的SDK的支持。
3、Scala的環(huán)境搭建
(1)首先需要按照對應版本的Java的JDK,并配置好環(huán)境變量
(2)下載對應的Scala安裝文件,并解壓到不帶中文的目錄下
(3)對應的配置Scala的環(huán)境變量,SCALA_HOME,開啟cmd,輸入scala測試即可
(4)IDEA默認不支持Scala的開發(fā),需要安裝插件;下載插件(scala-intellij-bin-2017.2.6.zip)并按照File->Setting->Plugins->Install plugin from disk的步驟找到并安裝插件
(5)Maven默認也不支持Scala的開發(fā),需要引入Scala框架,在項目上右擊,按照Add Framework Support->選擇 Scala->點擊 OK步驟引入Scala框架
4、簡單的Scala程序介紹
object Hello {
def main(args: Array[String]): Unit = {
println("HelloScala")
System.out.println("hello scala from java")
}
}
object:關鍵字,聲明一個單例對象(或者伴生對象)跟同名的類形成伴生關系;
Scala中無Static關鍵字,由Object實現(xiàn)類似靜態(tài)方法的功能。
class:關鍵字,聲音一個伴生類,伴生類和伴生對象的私有屬性是可以共有的。
def:關鍵字,聲明方法,形式如:
def 方法名(參數(shù)名:參數(shù)類型):返回值類型 = { 方法體 }
Unit:空返回值類型,Java中空返回類型用Void,Scala中空返回有Unit。
[String] 表示泛型,字符串類型,Scala用中括號表示泛型,Java中用尖括號<>表示泛型。
Scala 中不用添加分號作為每行的結束,當然添加了也是不會報錯,建議按照規(guī)范不必要添加。
scala 中可以直接調用Java中的類庫,當然使用類庫需要先導包,system.out.println() 這種常用的系統(tǒng)默認預先導入包了。
5、Scala的編譯命令
Java的編譯命令:
javac HelloScala.java
Java的運行命令:
java HelloScala
Scala的編譯命令:
scalac HelloScala.scala
Scala的運行命令:
scala HelloScala
二、變量和數(shù)據(jù)類型
1、注釋
Scala 注釋使用和 Java 完全一樣。
(1)單行注釋://
(2)多行注釋:/* */
(3)文檔注釋:
/***
*
*/
2、變量和常用(重點)
scala中有變量和常量之分,變量的值可以被修改,常量的值不能被修改;變量使用var 定義,常量使用val 定義。
java中變量和常量的定義如下,java中使用final關鍵字,來定義常量:
int a = 10
final int b = 20
scala中變量和常量的定義格式如下:
var 變量名[:變量類型] = 初始值
val 常量名[:常量類型] = 初始值
var i:Int = 10
val j:Int = 20
關于var 和val 定義的一些規(guī)則如下:
(1)聲明變量時,類型可以省略,編譯器會自動推導,即類型推導
var a = 10 //系統(tǒng)自動推導為Int類型
var name = "alice" //系統(tǒng)自動推導為String類型
(2)類型確定后,就不能修改,說明 Scala 是強數(shù)據(jù)類型語言
var a:Int = 123 //Int類型不能修改為String類型,或者String的值不能賦給Int類型的變量
a = "123" // error
(3)變量聲明時,必須要有初始值
var a1: Int // error 必須要有初始值,這兩種方式都是錯誤的
var a2 // error
(4)在聲明/定義一個變量時,可以使用 var 或者 val 來修飾,var 修飾的變量可變,val 修飾的變量不可改。
var a: Int = 15
val b: Int = 25
a = 18
b = 20 // error,val 修飾的值不能改變
(5)var 修飾對象時,對象和屬性均可變;val 修飾對象時,對象不可變,對象里面的屬性可變
// var 修飾的常規(guī)變量可以修改,修飾的引用變量也是可以修改的,如下:
var alice = new Student(name="alice",age=20)
alice = new Student(name="Alice",age=18)
alice = NULL
// val 修飾的引用變量不能修改,但是類里面的屬性可以修改,同時必須在創(chuàng)建類時在屬性上面有定義,如:
Student(name:String,var age:Int)
val bob = new Student(name="Bob",age=13)
// bob = new Student(name="bob",age=20) // error,對象不能重新賦值,不能修改
bob.age = 14 // 類里面的屬性可以修改,下面打印將會打印出年齡為14
bob.printInfo()
3、標識符命名
Scala的命名跟java的命名規(guī)則一樣,以字母或者下劃線開頭,后接字母、數(shù)字、下劃線。有以下兩個特例:
(1)以操作符開頭,且只包含操作符(+ - * / # !)等為合法命名,如下命名為合法:
var -+/#:String = "hello"
(2)用反引號
包括的任意字符串,即使是 Scala 關鍵字(39 個)也屬合法
var `for` = 123
var `def` = "123"
4、數(shù)據(jù)類型(重點)
Java數(shù)據(jù)類型
Java基本類型:
char、byte、short、int、long、float、double、boolean
Java引用類型:(對象類型)
由于Java有基本類型,而且基本類型不是真正意義的對象,即使后面產(chǎn)生了基本類型的包裝類,但是仍然存在基本數(shù)據(jù)類型,所以Java語言并不是真正意思的面向對象。
Java基本類型的包裝類:
Character、Byte、Short、Integer、Long、Float、Double、Boolean
注意:Java中基本類型和引用類型沒有共同的祖先類。但是scala是有的。
Scala數(shù)據(jù)類型
1、Scala數(shù)據(jù)類型
Scala的所有類型的祖先類是Any,下面分2大類,分別是AnyVal類(數(shù)值類型)和AnyRef類(引用類型),并且不管是數(shù)值類型還是引用類型都是對象。
- AnyVal類(數(shù)值類型):
- Unit: 空值類,相當于Java當中的void,常用于返回值為空的情況
- StringOps: 是對Java當中的String的增強類型
- Char: 字符類型
- Boolean: 布爾類型
- Double -> Float -> Long -> Int -> Short -> Byte: 數(shù)值類型,精度逐漸遞減
- AnyRef類(引用類型)
- Scala collections
- Other Scala classes
- all java classes
- Null: 只有一個對象就是null,它是所有引用類型的子類
Nothing: 是所有數(shù)據(jù)類型的子類,主要用在一個函數(shù)沒有明確返回值時使用,因為這樣我們可以把拋出的返回值,返回給任何的變量或者函數(shù)。
需要注意的是:
(1)Scala中所有類型,包括數(shù)據(jù)都是對象,且都是Any的子類
(2)Scala數(shù)據(jù)類型仍然遵守自動轉換,低精度的值類型向高精度值類型轉換時會自動轉換(隱式轉換)
(3)Null 是所有引用類型的子類
(4)Nothing 是所有數(shù)據(jù)類型的子類
2、整數(shù)類型
整數(shù)類型(Byte[1]、Short[2]、Int[4]、Long[8]),Scala默認的整數(shù)類型為Int型,聲明其他類型需要加上類型定義,且長整型需要在后面加L或者l;
val a3 = 10 // int型
val a4:Long = 32333333333L // 長整型定義
val a5:Byte = (10+20) // byte型
3、浮點類型
浮點類型(Float,Double),scala中默認是Double類型,聲明Float類型,需要在后面添加f或F。
val a6 = 3.6 //Double型
val a7:Float = 3.8f //Float型
4、字符類型
用單引號‘’扣起來的表示字符。
5、布爾類型
(1)布爾類型也叫Boolean 類型,Booolean 類型數(shù)據(jù)只允許取值 true 和 false
(2)boolean 類型占 1 個字節(jié)
6、Unit類型,Null類型和Nothing類型
Unit:表示無值,和Java語言中 void 等同。用作不返回任何結果的方法的結果的返回值類型。Unit 只有一個實例值,寫成(),可以查看里面的toString()方法。
Null:null , Null 類型只有一個實例值 null,null可以賦值給任意的引用類型AnyRef,不能賦值給數(shù)值類型AnyAvl
Nothing:Nothing 類型在 Scala 的類層級最低端;它是任何其他類型(AnyRef、AnyAvl)的子類型。當一個函數(shù),我們確定沒有正常的返回值時(也就是發(fā)生異常情況時),可以用 Nothing 來指定返回類型,這樣有一個好處,就是我們可以把返回的值(異常)賦給其它所有的函數(shù)或者變量(兼容性)
5、類型轉換
自動類型轉換
當 Scala 程序在進行賦值或者運算時,精度小的類型自動轉換為精度大的數(shù)值類型,這個就是自動類型轉換(隱式轉換)。按照順序可以有以下順序:
Byte => Short => Int => Long => Float => Double
一般自動轉換的規(guī)則如下:
(1)自動提升原則:有多種類型的數(shù)據(jù)混合運算時,系統(tǒng)首先自動將所有數(shù)據(jù)轉換成精度大的那種數(shù)據(jù)類型,然后再進行計算。
(2)把精度大的數(shù)值類型賦值給精度小的數(shù)值類型時,就會報錯,反之就會進行自動類型轉換。
(3)byte,short和 char 之間不會相互自動轉換,char在自動轉換時,會自動轉換成int類型
(4)byte,short,char 他們三者可以計算,在計算時首先會轉換為 int 類型,然后再計算
強制類型轉換
精度高的想轉成精度低的就需要用到強制轉換,但要注意會造成精度降低或者溢出問題,且強轉只針對于最近的操作數(shù)有效,常常會使用括號提升優(yōu)先級。
val a1:Int = 10*3.5.toInt // 結果為30,先將3.5轉為Int型的3
val a2:Int = (10*3.5).toInt // 結果為35,將整體結果35.0轉為35
var b1:Byte = 1
// 此處編譯報錯,因為b1+1時,系統(tǒng)默認會將b1轉化成int類型,再相加;
// 但加完需要再賦給b1的時候,變成了int類型賦給Byte類型,因為是精度大的類型賦給精度小的類型,所以報錯
// 此處需要加括號強轉 b1 = (b1+1).toByte
b1 = b1+1
// 此處不會報錯,因為直接將b1轉成了Int類型
b1 += 1
數(shù)值類型和String類型轉換
(1)基本類型轉 String 類型(語法:將基本類型的值+“” 即可)
var num: Int = 123
var strNum1 = num.toString // 或者
var strNum2 = num+""
(2)String 類型轉基本數(shù)值類型(語法:s1.toInt、s1.toFloat、s1.toDouble、s1.toByte、s1.toLong、s1.toShort)但要注意非數(shù)值的字符串轉成數(shù)值會報錯。
var str1 = "123"
val str2 = "a123"
val num1 = str1.toInt
val num2 = str2.toInt //報錯
6、輸出和輸入
輸出:
(1)字符串連接,通過+號連接,* 重復
val name: String = alice
val age: Int = 20
println(age + "年齡" + name + "姓名")
println(name * 3) // 多次打印字符串
(2)printf 用法:字符串,通過%占位符傳值,并跟上對應的類型
println("%d年齡 %s姓名",age,name)
(3)雙引號前面加上s""為字符串模板(插值字符串),通過 獲取變量值,后面的 {}獲取變量值,后面的 獲取變量值,后面的{}即可對應相應的值
println(s"${age}年齡 ${name}姓名")
(4)雙引號前面加上f"“為格式化輸出,%2.2f中%號為占位,小數(shù)點前面的2為前面保留2位,小數(shù)點后面為后面保留2位;雙引號前面加上raw”"為原格式輸出,不管后面帶的是什么
val num:Double = 2.5365
println(f"the number is ${num}%2.2f") //格式化模板字符串
# the number is 2.54
println(raw"the number is ${num}%2.2f") //raw 原樣輸出
# the number is 2.5365%2.2f
(5)多行字符串,在Scala中可以使用三個雙引號包圍多行字符串實現(xiàn),應用 scala 的 stripMargin 方法,并使用“|”作為連接符,可以保留多行字符串的格式,如需加上引用,可以在加上s即可。
val sql = """
|select
| name,
| age
|from user
|where name="zhangsan" """.stripMargin
println(sql)
// 加上引用時
val sql1 = s"""
|select
| name,
| age
|from user
|where name="$name" and age=${age+2} """.stripMargin
println(sql1)
標準輸入:
基本的輸入有StdIn.readLine()、StdIn.readShort()、StdIn.readDouble() 。
// 1 輸入姓名
println("input name:")
var name = StdIn.readLine()
// 2 輸入年齡
println("input age:")
var age = StdIn.readShort()
// 3 輸入薪水
println("input sal:")
var sal = StdIn.readDouble()
文件讀取,并在控制臺輸出:
Source.fromFile("D:/Tootls/abc.txt").foreach(print)
也可以直接調用Java的接口實現(xiàn)文件讀取的功能:
val writer = new PrintWriter(new File(pathname="D:/Tootls/abc.txt"))
weiter.write("hello scala from java writer")
三、運算符
1、基本語法
(1)對于除號“/”,它的整數(shù)除和小數(shù)除是有區(qū)別的:整數(shù)之間做除法時,只保留整數(shù)部分而舍棄小數(shù)部分。需要保留小數(shù)可以在前面將數(shù)值轉換成Double類型,或者數(shù)字后面加小數(shù)點。
(2)對一個數(shù)取模 a%b,和 Java 的取模規(guī)則一樣。
運算符 | 運算 | 范例 | 結果 |
---|---|---|---|
+ | 正號 | +3 | 3 |
- | 負號 | b=4; -b | -4 |
+ | 加 | 5+5 | 10 |
- | 減 | 6-4 | 2 |
* | 乘 | 3*4 | 12 |
/ | 除 | 5/5 | 1 |
% | 取模(取余) | 7%5 | 2 |
+ | 字符串相加 | “He”+”llo” | “Hello” |
2、關系運算法
運算符 | 運算 | 范例 | 結果 |
---|---|---|---|
== | 相等于 | 4==3 | false |
!= | 不等于 | 4!=3 | true |
< | 小于 | 4<3 | false |
> | 大于 | 4>3 | true |
<= | 小于等于 | 4<=3 | false |
>= | 大于等于 | 4>=3 | true |
Java 和Scala 中關于“==”的區(qū)別:
Java:
== 比較兩個變量本身的值,即兩個對象在內存中的首地址;
equals 比較字符串中所包含的內容是否相同。
Scala:
== 更加類似于 Java 中的 equals,參照 jd 工具
eq 而比較兩個對象之間在內存的地址使用的是eq函數(shù) str1.eq(str2)
3、邏輯運算符
其中&&和||是二元運算符,也就是需要前面后面兩個參數(shù),!為單元運算符,只需要后面接一個參數(shù)
(A && B)和(A || B) 也是惰性匹配,當A為false時,&&不會再判斷后面條件,因為一個為false結果只能為false,所以后面B不會執(zhí)行。
當A為true時,|| 同樣不會再判斷后面的B條件,因為只要有一個為true結果只能為true,這種不會再執(zhí)行后面條件成為惰性匹配。
-- 判斷字符串是否為空
isNotEmpty(String s){
//如果按位與,s 為空,會發(fā)生空指針
return s!=null && !"".equals(s.trim());
}
4、scala運算符的本質
在 Scala 中其實是沒有運算符的,所有運算符都是方法。前面講過可以使用運算法作為方法的命名,常用的運算符已經(jīng)給scala命名為方法了。
方法有以下的省略規(guī)則:
(1)當調用對象的方法時,點.可以省略
(2)如果函數(shù)參數(shù)只有一個,或者沒有參數(shù),()可以省略
// 標準的加法運算
val i:Int = 1.+(1)
// 當調用對象的方法時,.可以省略
val j:Int = 1 + (1)
// 如果函數(shù)參數(shù)只有一個,或者沒有參數(shù),()可以省略,這樣就跟平時的運算一樣的,也可以跟java或其他語言兼容
val k:Int = 1 + 1
5、賦值運算符和位運算符跟Java基本一致。
四、流程控制
1、if-else,使用跟Java基本一致
(1)單分支
if (表達式) {
執(zhí)行代碼塊
}
(2)雙分支
if (表達式) {
執(zhí)行代碼塊1
} else {
執(zhí)行代碼塊2
}
(3)多分支
if (表達式) {
執(zhí)行代碼塊1
} else if (表達式2){
執(zhí)行代碼塊2
} else {
執(zhí)行代碼塊3
}
(4)跟Java或其他語言不同的是,scala的if-else是有返回值的,具體返回值取決于滿足條件的代碼體的最后一行內容。
(4.1)在if-else的多分支里面,每個分支的返回值類型一樣的話,可以寫成下面形式:
// 返回值參數(shù)為res,類型為String
val res: String = if (age < 18) {
return "18"
} else {
return "19"
}
(4.2)Scala 中返回值類型不一致時,取它們共同的祖先類型,這里返回兩個類型Int和String,但整體的返回為Unit,即為空()
val res: Unit = if (age < 18) {
return 18
} else {
return "19"
}
(5)Java中的三元運算符可以使用if-else來實現(xiàn),scala中如果大括號{}內的邏輯代碼只有一行,大括號可以省略。如果省略大括號,if 只對最近的一行邏輯代碼起作用。
//java中:
int result = flag ? 1 : 0
scala中省略大括號后,用if-else實現(xiàn):
println("input age:")
var age = StdIn.readInt()
val res: String = if (age > 18) "成年" else "童年"
println(res)
2、for循環(huán)(重點)
基本語法如下:
for(i <- 1 to 3) // 將1到3的值依次賦給i
(1)i 表示循環(huán)的變量,<- to 都是規(guī)定的格式
(2)i 將會從 1-3 循環(huán),前后閉合
(3)當想后面不閉合時,使用range()或者使用until
(1) 范圍遍歷
for ( i <- 1 to 10) {} // 1-10 包含10
for ( i <- range(1,10) {} // 1-10 不包含10
for ( i <- 1 until 10) {} // 1-10 不包含10
(2) 集合遍歷,可以直接使用集合,數(shù)組,列表等
for ( i <- Array(2, 5, 8)){}
for ( i <- List(2, 5, 8)){}
for ( i <- Set(2, 5, 8)){}
(3) 循環(huán)守衛(wèi),當不需要循環(huán)里面的某個值時,使用循環(huán)守衛(wèi),相當于Java中的continue
for ( i <- 1 to 10 if i != 5){} // 1-10 包含10,不要5
(4) 循環(huán)步長,默認的步長都是1,當需要其他步長時,在后面的by關鍵字后面添加
for ( i <- 1 to 10 by 2){} // 取 1,3,5,7,9,步長間隔為2
(5) 反轉,當需要取值從大到小時,需將大的數(shù)放在前面,并且步長設置為-1,或者直接在后面添加反轉的關鍵字,reverse
for ( i <- 10 to 1 by -1){} // 從10到1
for ( i <- 1 to 10 reverse){} // 從10到1
(6) 小數(shù),當取的步長不為整數(shù),為小數(shù)時,因為后面的步長是跟前面數(shù)值的類型相關的,所以需將前面的類型轉成Double類型,才能設置小數(shù)步長
for ( i <- 1.0 to 10.0 by 0.5){}
(7) 循環(huán)嵌套,類似于java當中的嵌套
for ( i <- 1 to 3){
for (j <- 1 to 3){}
}
// scala當中的循環(huán)嵌套也可以寫在同一行,用分號隔開,兩個處在相同的地位
for (i <- 1 to 4; j <- 1 to 5) {} //輸出4*5的矩陣
(8) 引入變量,引入另一個變量j,j跟i是有相關的關系的
for ( i <- 1 to 10; j = i * 2){}
(9) 循環(huán)返回值
默認的返回值都是unit,當需要返回值的時候,需要加關鍵字 yield
// 返回的是一個Vector向量,可以直接對集合或者列表里面的值做操作。
val b: immutable.IndexedSeq[Int] = for ( i <- 1 to 10) yield i * 2
3、while和do.while循環(huán)控制
while基本語法
循環(huán)變量初始化
while (循環(huán)條件) {
循環(huán)體(語句)
循環(huán)變量迭代
}
while說明:
(1)循環(huán)條件是返回一個布爾值的表達式
(2)while 循環(huán)是先判斷再執(zhí)行語句
(3)與 for 語句不同,while 語句沒有返回值,即整個 while 語句的結果是Unit 類型()
(4)因為 while 中沒有返回值,所以當要用該語句來計算并返回結果時,就不可避免的使用變量,而變量需要聲明在 while 循環(huán)的外部,那么就等同于循環(huán)的內部對外部的變量造成了影響,所以不推薦使用,而是推薦使用 for 循環(huán)。
do.while基本語法:
循環(huán)變量初始化;
do{
循環(huán)體(語句)
循環(huán)變量迭代
} while(循環(huán)條件)
do.while說明:
(1)循環(huán)條件是返回一個布爾值的表達式
(2)do…while 循環(huán)是先執(zhí)行,再判斷
4、循環(huán)中斷
scala內置結構去掉了 break 和 continue 關鍵字,是為了更好的適應函數(shù)式編程,推薦使用函數(shù)式的風格解決 break 和 continue 的功能,而不是一個關鍵字。Scala 中使用 breakable 控制結構來實現(xiàn) break 和 continue 功能。break功能也就是相當于程序異常退出,卻沒有任何的異常信息,這里可以使用拋出異常,但不捕獲異常的方式實現(xiàn)退出功能。
def main(args: Array[String]): Unit = {
try {
for (elem <- 1 to 10) {
println(elem)
if (elem == 5) throw new RuntimeException // 隨便捕獲異常
}
}catch {
case e => // 捕獲異常后,不做任何處理,可以實現(xiàn)break的功能
}
println("正常結束循環(huán)")
}
使用scala自帶的函數(shù),來實現(xiàn)退出功能,將需要break的代碼塊包裹在Breaks.breakable()里面,需要時,調用Breaks.break()方法。
import scala.util.control.Breaks
def main(args: Array[String]): Unit = {
Breaks.breakable {
for (elem <- 1 to 10) {
println(elem)
if (elem == 5) Breaks.break()
}
}
println("正常結束循環(huán)")
}
導入包后,其實包名可以省略,當方法的參數(shù)為空時,括號也是可以省略,從而簡化成下面的格式:
import scala.util.control.Breaks._
object TestBreak {
def main(args: Array[String]): Unit = {
breakable {
for (elem <- 1 to 10) {
println(elem)
if (elem == 5) break
}
}
println("正常結束循環(huán)")
}
}
五、函數(shù)式編程
1、面向對象和面向函數(shù)編程
(1)面向對象編程
解決問題,分解對象,行為,屬性,然后通過對象的關系以及行為的調用來解決問題。對象的本質:就是對數(shù)據(jù)和行為的一個封裝。
Scala 語言是一個完全面向對象編程語言,萬物皆對象。
(2)函數(shù)式編程
解決問題時,將問題分解成一個一個的步驟,將每個步驟進行封裝(函數(shù)),通過調用這些封裝好的步驟,解決問題。函數(shù)的本質:函數(shù)可以當做一個值進行傳遞。
Scala 語言是一個完全函數(shù)式編程語言,萬物皆函數(shù)。
Scala就是將面向對象和函數(shù)編程完美融合在一起了。
2、函數(shù)基本語法
基本語法如下所示:
def sum( x: Int ,y: Int ): Int = {
x + y
}
其中def是定義函數(shù)的關鍵字,sum 是函數(shù)名,x,y 是函數(shù)的參數(shù)名,Int 是參數(shù)的類型,第三個Int是函數(shù)的返回值類型,x+y是函數(shù)體的具體實現(xiàn)。
函數(shù)和方法的區(qū)別:
(1)為完成某一功能的程序語句的集合,稱之為函數(shù),在類中的函數(shù)稱之方法。
(2)Scala 語言可以在任何的語法結構中聲明任何的語法,也就是與位置沒有關系,但要注意語法的生命周期
(3)函數(shù)沒有重載和重寫的概念;方法可以進行重載和重寫,Object(類)下面的方法可以重載,def 里面的函數(shù)不能重載,也就是不能重名
(4)Scala 中函數(shù)可以嵌套定義
函數(shù)定義
(1)函數(shù) 1:無參,無返回值
(2)函數(shù) 2:無參,有返回值
(3)函數(shù) 3:有參,無返回值
(4)函數(shù) 4:有參,有返回值
(5)函數(shù) 5:多參,無返回值
(6)函數(shù) 6:多參,有返回值
//(1)函數(shù) 1:無參,無返回值
def f1():Unit = {}
//(2)函數(shù) 2:無參,有返回值
def f2(): String = { return "name" }
//(3)函數(shù) 3:有參,無返回值
def f3(age: Int): Unit = {}
//(4)函數(shù) 4:有參,有返回值
def f4(age: Int): Int = { return age }
//(5)函數(shù) 5:多參,無返回值
def f5(name:String, age:Int): Unit = {}
//(6)函數(shù) 6:多參,有返回值
def f6(name:String, age:Int): Int = { return age}
函數(shù)參數(shù)
(1)可變參數(shù)(可以在使用前不確定參數(shù)的個數(shù),定義可變參數(shù),可變參數(shù)將以數(shù)組的形式傳入)
(2)如果參數(shù)列表中存在多個參數(shù),那么可變參數(shù)一般放置在最后
(3)參數(shù)默認值,一般將有默認值的參數(shù)放置在參數(shù)列表的后面
(4)帶名參數(shù)
//(1)可變參數(shù),可變參數(shù)在類型后面添加*號
def f1(s: Strring*): Unit = {}
//(2)如果參數(shù)列表中存在多個參數(shù),那么可變參數(shù)一般放置在最后,不放置在最后無法給后面的參數(shù)賦值
def f2(name:String, s:String*): Unit = {}
//(3)參數(shù)默認值,一般將有默認值的參數(shù)放置在參數(shù)列表的后面,如果調用的時候傳遞了age的值,就會覆蓋默認值
def f3(name:String, age:Int = 18): Unit = {}
//(4)帶名參數(shù)
def f4(name:String, age:Int = 18): Unit = {}
// 帶名參數(shù)調用是可以不按照順序調用,一般調用:
f4("bob",20)
// 帶名參數(shù)調用:
f4(age = 20,name = "bob")
// 或者
f4(name = "bob")
函數(shù)至簡原則
函數(shù)至簡原則,就是四個字,能省則省。
(1)return 可以省略,Scala 會使用函數(shù)體的最后一行代碼作為返回值,當不是最后一行的值需要返回時,return不能省
(2)如果函數(shù)體只有一行代碼,可以省略花括號
(3)返回值類型如果能夠推斷出來,那么可以省略(:冒號和返回值類型一起省略)
(4)如果有 return,則不能省略返回值類型,必須指定
(5)如果函數(shù)明確聲明unit,那么即使函數(shù)體中使用 return 關鍵字也不起作用
(6)Scala 如果期望是無返回值類型,可以省略等號
(7)如果函數(shù)無參,但是聲明了參數(shù)列表,那么調用時,小括號,可加可不加
(8)如果函數(shù)沒有參數(shù)列表,那么小括號可以省略,調用時小括號必須省略
(9)如果不關心名稱,只關心邏輯處理,那么函數(shù)名(def)可以省略
//函數(shù)標準寫法
def f( s : String ) : String = {
return s + " hello"
}
//(1)return可以省略,最后一行作為返回值
def f( s : String ) : String = {
s + " hello"
}
// (2)如果函數(shù)體只有一行代碼,可以省略花括號
def f( s : String ) : String = s + " hello"
// (3)返回值類型如果能夠推斷出來,那么可以省略(:冒號和返回值類型一起省略)
def f( s : String ) = s + " hello"
//(4)如果有 return,則不能省略返回值類型,必須指定
def f( s : String ) : String = {
return s
}
//(5)如果函數(shù)明確聲明unit,那么即使函數(shù)體中使用 return 關鍵字也不起作用
def f( s : String ) : Unit = {
return s //此處的return毫無意義,編譯器也會提醒,因為不會返回
}
//(6)Scala 如果期望是無返回值類型,可以省略等號,將無返回值的函數(shù)稱之為過程,可以跟其他語言做兼容
def f() {
println("hello") //無參數(shù),無返回值類型
}
//(7)如果函數(shù)無參,但是聲明了參數(shù)列表(就是函數(shù)名后面加了小括號()),那么調用時,小括號可加可不加
def f() = "hello" // 函數(shù)只有一行,省略花括號
println(f()) //兩種都可以調用該函數(shù),一個帶()
println(f) //兩種都可以調用該函數(shù),一個不帶()
//(8)如果函數(shù)沒有參數(shù)列表(就是函數(shù)名后面不加小括號()),那么小括號可以省略,定義時沒有小括號,則調用時也不能帶小括號
def f8 = "hello"
//println(f8()) // 該調用會報錯
println(f8) // 正確的調用方式
//(9)如果不關心名稱,只關心邏輯處理,那么函數(shù)名def 和函數(shù)名稱都可以省略
def f9(name:String): Unit = {
println(name)
}
// 可以寫成下面這樣:也就是匿名函數(shù),相當于lambda表達式
// 省略了函數(shù)定義,函數(shù)名,返回值類型
(name:String) => { println(name) }
函數(shù)高階用法
Scala的函數(shù)相對其他語言,有如下幾個高階用法,這也是Scala語言靈活的重要部分。
(1)函數(shù)可以作為值進行傳遞
(2)函數(shù)可以作為參數(shù)進行傳遞
(3)函數(shù)可以作為返回值進行傳遞
當函數(shù)作為值進行傳遞的時候,或者作為返回值返回的時候,其實傳遞的就是該函數(shù)的引用地址,其實就是將函數(shù)的引用地址進行賦值,從而可以將函數(shù)進行傳遞。另外函數(shù)式編程有個特點就是函數(shù)中有自變量和因變量,簡單理解就是x到y(tǒng)的過程,這個過程用符號來表示可以表示為 f(x) = y,而Scala的匿名函數(shù)或者說Scala的函數(shù)類型定義就是跟這個類似,只是將符號換成了 => 。例如上面的匿名函數(shù)(name:String) => { println(name) },符號左邊是輸入,右邊就是輸出,跟函數(shù)的定義很類似,而Scala簡化的原則就是將編程盡可能的寫成函數(shù)的形式。
//(1)函數(shù)可以作為值進行傳遞
def f1(n:Int):Int = { // 有參函數(shù)
println("f1被調用")
n + 1
}
def f2():Int = { // 無參函數(shù)
println("f2被調用")
1
}
// 正常的調用
println(f1(5))
println(f2())
// 函數(shù)作為值進行傳遞
val f3 = f1 _
val f4:Int=>Int = f1
val f5 = f2 _
val f6:()=>Int = f2
println(f3) // 直接打印函數(shù)的引用地址
println(f3(6)) // 傳參,調用函數(shù)f3,也就是調用f1
println(f6)
println(f6())
函數(shù)作為值進行傳遞,函數(shù)名接上小括號就是函數(shù)的調用,而函數(shù)接上**空格下劃線 _**代表的就是函數(shù)本身,也就是函數(shù)的引用地址。當我們想直接使用函數(shù)的名稱表示函數(shù)本身時,需要在定義函數(shù)時,加上函數(shù)的類型,也就是Int=>Int,表示參數(shù)為Int類型,返回值也為Int類型的參數(shù)。并且當參數(shù)為空時,小括號不能省略,也就是()=>Int。
//(2)函數(shù)可以作為參數(shù)進行傳遞
// 函數(shù)f里面的參數(shù)為:fun: (Int,Int) => Int
// 其中fun是參數(shù)名,表示參數(shù)是函數(shù)類型的意思
// (Int,Int) => Int 為參數(shù)類型,表示輸入?yún)?shù)為兩個Int型,返回值為Int型的函數(shù)作為參數(shù)
def f1(fun: (Int,Int) => Int):Int = {
fun(1, 2)
}
// 定義一個函數(shù),參數(shù)和返回值類型和fun要求的類型一致,兩個參數(shù)為Int型,返回值也為Int型
def add(a: Int, b: Int): Int = a + b
// 將 add 函數(shù)作為參數(shù)傳遞給 f1 函數(shù),因為傳遞的是f1函數(shù)本身,所以需要加空格下劃線 _
// 但如果能夠推斷出來不是函數(shù)調用,而是函數(shù)傳遞,空格下劃線可以省略
// 此處可以自動推斷,因為f1中已經(jīng)定義了函數(shù)參數(shù)的類型
println(f1(add _))
println(f1(add))
函數(shù)作為參數(shù)簡單理解就是之前的函數(shù),函數(shù)是固定的,也就是處理的流程固定,但數(shù)據(jù)是可變的。比如計算加法,可以傳遞1,2或者2,3實現(xiàn)加法效果,加法這個邏輯本身不變。函數(shù)作為參數(shù)進行傳遞時,可以看做數(shù)據(jù)不變,操作是可變的。比如也是計算1和2的加法,數(shù)據(jù)看成1和2不變,操作可以傳遞加法,也能傳遞減法。實際應用中函數(shù)和數(shù)據(jù)都會是可變的狀態(tài),這樣就更加靈活了。
//(3)函數(shù)可以作為函數(shù)返回值返回
def f1():Int=>Unit = {
def f2(a: Int): Unit = {
println("f2調用" + a)
}
f2 // 將函數(shù)直接返回
}
println(f1()) // 打印的是f2的引用地址,因為調用了f1,里面返回的函數(shù)f2
printfln(f1()()) // f1() = f2,所以f1()() = f2(),這才是調用了f2,且f2的返回值為空
其中 Int=>Unit 是返回值的類型,因為返回的是函數(shù)f2,所以Int=>Unit就是f2的函數(shù)類型,也就是f2的參數(shù)為Int型,返回值為空。
匿名函數(shù)
由上面的省略原則,我們可以得到匿名函數(shù)的基本定義和語法:
沒有名字的函數(shù)就是匿名函數(shù),其語法定義為:
(x:Int)=>{ 函數(shù)體 }
x:表示輸入?yún)?shù)名稱;Int:表示輸入?yún)?shù)類型;函數(shù)體:表示具體代碼邏輯。并且匿名函數(shù)還能再簡化,也就是匿名函數(shù)的簡化原則。
匿名函數(shù)簡化規(guī)則:
(1)參數(shù)的類型可以省略,會根據(jù)形參進行自動的推導
(2)類型省略之后,發(fā)現(xiàn)只有一個參數(shù)時,則圓括號可以省略;其他情況:沒有參數(shù)和參數(shù)超過 1 的則不能省略圓括號
(3)匿名函數(shù)如果只有一行,則大括號也可以省略
(4)如果參數(shù)在調用時只出現(xiàn)一次,則參數(shù)省略且后面參數(shù)可以用_代替
// 定義函數(shù)f1,里面的參數(shù)為op操作函數(shù)
def f1(op:Int => Int) = {
op(1)
}
// 定義op操作函數(shù)
def op(n:Int):Int = {
n + 1
}
// 直接傳入op調用f1函數(shù)
val result = f1(op)
// 采用不定義op操作函數(shù),直接匿名函數(shù)的方式
val result1 = f1((n:Int) => { n+1 })
//(1)參數(shù)的類型可以省略,會根據(jù)形參進行自動的推導,因為f1中已經(jīng)規(guī)定了op的參數(shù)為Int型
val result2 = f1((n) => { n+1 })
//(2)只有一個參數(shù)時,則參數(shù)圓括號可以省略;其他情況:沒有參數(shù)和參數(shù)超過 1 的則不能省略圓括號。
val result3 = f1(n => { n+1 })
//(3)匿名函數(shù)如果只有一行,則大括號也可以省略,就是相當于傳入?yún)?shù)的操作,中間用 => 連接
val result4 = f1(n => n+1)
//(4)如果參數(shù)在調用時只出現(xiàn)一次,則前面參數(shù)省略且后面參數(shù)可以用_代替,參數(shù)n在調用時只用一次
val result5 = f1(_+1)
//(5)如果可以推斷出,當前傳入的是一個函數(shù)體,而不是調用語句,可以直接省略下劃線
示例代碼,簡單實現(xiàn)1和2的相加和相減:
// 定義a和b相加和相減的匿名函數(shù),并將其返回值賦給addFun和defFun
var addFun = (a: Int, b: Int) => { a + b }
var defFun = (a: Int, b: Int) => { a - b }
// 定義函數(shù)f,里面的參數(shù)是函數(shù),返回值是Int類型
// 參數(shù)的函數(shù)類型為:兩個Int參數(shù),返回值為Int類型
def f(fun: (Int,Int) => Int):Int = {
fun(1, 2)
}
// 使用匿名函數(shù)addFun和defFun作為參數(shù),傳遞給f,并將f的返回值打印出來
println(f(addFun))
println(f(defFun))
// 其實匿名函數(shù)addFun和defFun等價于里面的實現(xiàn)語句,并省略了參數(shù)類型和花括號
// 因為f函數(shù)里面已經(jīng)規(guī)定了a,b只能會Int類型,所以可以省略參數(shù)類型,函數(shù)體在一行所以可以省略花括號
println(f((a, b) => a + b ))
println(f((a, b) => a - b ))
// 參數(shù)a和參數(shù)b只在后面調用的時候調用1次,所以可以用下劃線_來代替
println(f(_ + _))
println(f(_ - _))
函數(shù)柯里化和閉包
閉包:
如果一個函數(shù),訪問到了它的外部變量或者外部參數(shù),那么這個函數(shù)和它所處的環(huán)境(也就是外部變量的值)一起,稱之為閉包。
有如下函數(shù):
def func(i:Int):String=>(Char=>Boolean) = {
def f1(s:String):Char=>Boolean = {
def f2(c:Char):Boolean = {
if (i == 0 && s == "" and c == '0') false else true
}
f2
}
f1
}
// 正常調用如下:
func(0)("")('0')
// 如果將函數(shù)作為值傳遞,則有如下:
f3 = func(0)
println(f3("")('0')
在將 func(0) 的值賦值給 f3 之后,在調用 f3 的時候,這時候的 func 中的參數(shù) i 在內存中還存在嗎?
閉包就是為了解決這個問題,因為func已經(jīng)運行完了,在棧內存中,i 的值是不存在的,但是系統(tǒng)將func中的參數(shù) i 跟函數(shù) f1 一起打包了,并將其存放在堆內存中,這樣才能確保運行f1,f2時能正常用到外部的變量而不會導致出錯。這個就叫做閉包。
// 正常多個參數(shù)的寫法
def add(a:Int,b:Int):Int = {
a + b
}
// addByB 使用局部變量(外部變量)的閉包寫法
def addByFour():Int=>Int = {
val a = 4
def addByB(b:Int) => Int = {
a + b
}
addByB
}
// addByB 使用外部參數(shù)的閉包寫法
def addByA(a:Int):Int=>Int = {
def addByB(b:Int) => Int = {
a + b
}
addByB
}
// 調用
println(addByFour()(5))
println(addByA(4)(5))
柯里化:
把一個參數(shù)列表的多個參數(shù),變成多個參數(shù)列表的過程,稱之為函數(shù)柯里化。上面介紹了閉包就是為了引入柯里化,因為柯里化的底層就是用閉包來實現(xiàn)的。
柯里化其實就是為了更符合函數(shù)式編程的思想,因為在函數(shù)式編程里面,是沒有多個參數(shù)對應的,例如f(x1,x2) => y ??粗械氖菃蝹€值的對應關系,例如 f(x1)(x2) => y 的表達式,這里f(x1)返回的另一個函數(shù)然后參數(shù)是x2,就形成了一對一的嵌套關系。
// 上面使用外部參數(shù)的柯里化寫法
def addCurrying addByAB(a:Int)(b:Int):Int = {
a + b
}
// 調用
println(addCurrying(4)(5))
柯里化的寫法,我們發(fā)現(xiàn)簡介很多,并且跟調用的寫法如出一轍,而柯里化的內部寫法其實就是用了閉包的策略,但我們更推薦這種寫法,因為這種寫法更符合函數(shù)式編程的思想,也更容易理解。
遞歸
遞歸的含義其實很簡單:一個函數(shù)/方法在函數(shù)/方法體內又調用了本身,我們稱之為遞歸調用。
遞歸的一些注意事項:
(1) 方法調用自身
(2) 方法必須要有跳出的邏輯
(3) 方法調用自身時,傳遞的參數(shù)應該有規(guī)律
(4) scala 中的遞歸必須聲明函數(shù)返回值類型
// 用遞歸實現(xiàn)階乘
def test(i : Int) : Int = {
if (i == 1) {
1
} else {
i * test(i - 1)
}
}
println(test(5))
在 test 函數(shù)的內部,又調用了 test 本身,這樣的實現(xiàn)方式就叫做遞歸。但遞歸本身有一個缺點,就是每次調用內部 test 的時候,需要先將 i 的值保存下來,這樣每遞歸一次,就要保存外部的變量一次,當遞歸次數(shù)很多時,就會耗費很大的資源進行保存。
尾遞歸:
正常的遞歸,內層需要用到外層的參數(shù),導致在執(zhí)行內層的時候,需要先將外層參數(shù)的值保留下來。
尾遞歸,就是將外層的參數(shù)作為內層的參數(shù),傳遞進去給內層的參數(shù),進而內層函數(shù)在調用時,就可以保留函數(shù)和參數(shù),從而不需要外層函數(shù)的任何信息,從而不管遞歸多少次,都達到只保留一個棧信息的效果。
@tailrec
def tailFact(n:Int):Int = {
def loop(n:Int, curRes:Int):Int = {
if (n == 0) return curRes
loop(n - 1,curRes * n)
}
loop(n, curRes = 1)
}
在 loop 內部又調用了 loop,遞歸調用,但是每次調用 loop 時,會將上一次的結果 curRes * n 作為參數(shù)在傳遞給 loop,就能實現(xiàn)每次只保存一次參數(shù)的效果,更推薦使用。
上面的 @tailrec 是一個尾遞歸的注解,也是一個尾遞歸的判斷,當下面寫的函數(shù)不是尾遞歸的時候,編譯器就會報錯,是個很好的檢查工具。
控制抽象
(1)傳值調用:把值傳遞過去
// 傳值參數(shù)
def f0(a:Int):Unit = {
println("a = " + a)
println("a = " + a)
}
f0(23)
def f1():Int = {
println("f1被調用")
12
}
// f1 調用1次之后,將f1函數(shù)的值傳遞給f0,f0輸出兩次12
f0(f1())
(2)傳名調用:把代碼塊傳遞過去(不同于上面的函數(shù)作為參數(shù)傳遞,這里是代碼塊,不是函數(shù))
// 傳名參數(shù)
def f1():Int = {
println("f1被調用")
12
}
def f2(a: =>Int):Unit = {
println("a = " + a)
println("a = " + a)
}
// 打印2次 a = 23
f2(23)
// 打印2次f1 被調用,并打印2次a = 12
f2(f1())
這里相當于將整個f1的代碼塊,當成參數(shù)傳入f2中,所以a在f2中出現(xiàn)多少次,也就運行了f1的代碼塊多少次。=>Int 整個類型表示代碼塊,不像函數(shù),函數(shù)要求無參數(shù)的話,小括號不能省略()=>Int,這里只要求返回值為Int型即可,不要求有參數(shù)或者其他。
惰性加載
當函數(shù)返回值被聲明為 lazy 時,函數(shù)的執(zhí)行將被推遲,直到我們首次對此取值,該函數(shù)才會執(zhí)行。這種函數(shù)我們稱之為惰性函數(shù)。文章來源:http://www.zghlxwxcb.cn/news/detail-783826.html
def main(args: Array[String]) = {
lazy val res1 = sum1(10, 30)
val res2 = sum2(15, 20)
println("------------------------------")
println("res1 =" + res1)
println("------------------------------")
println("res2 =" + res2)
}
def sum1(a:Int,b:Int):Int = {
println("sum1 被執(zhí)行。。。")
a + b
}
def sum2(a:Int,b:Int):Int = {
println("sum2 被執(zhí)行。。。")
a + b
}
如上,res2在賦值的時候,sum2就已經(jīng)運行了,而res1需要在res1要使用時,在println(“res1 =” + res1)時,才會去執(zhí)行sum1的語句。
注意:lazy 不能修飾 var 類型的變量文章來源地址http://www.zghlxwxcb.cn/news/detail-783826.html
到了這里,關于大數(shù)據(jù)之Scala簡介的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!