2018年第42周-scala入門-基本語法

讓事情變得更加簡單方便, 注意是簡單方便, 而事情內在的複雜性並沒有降低.

變量定義

變量是一種使用方便的佔位符,用於引用計算機內存地址。
Scala有兩種變量,valvar。val類似於java的final變量。var則爲非final變量。
在scala程序中, 通常建議使用val, 也就是常量, 因爲類似於spark的大型複雜系統中, 需要大量的網絡傳輸數據, 如果使用var, 可能會擔心值被錯誤的更改.
在Java的大型複雜系統的設計和研發中, 也使用了類似的特性, 我們通常會將傳遞給其他模塊/組件/服務的對象, 設計成不可變類(Immutable Class). 在裏面也會使用java的常量定義, 比如final, 阻止變量的值被改變. 從而提高系統的健壯性(robust, 魯棒性), 和安全性.
簡單的說, 就是讓事情變得不可能發生, 那這錯誤就永遠不會發生.

聲明val變量

聲明val變量來存放表達式的計算結果.
例如, val result = 1 + 1
後續這些常量是可以繼續使用的, 例如, 2 * result
但是常量聲明後, 是無法改變它的值的, 例如, result=1, 會返回error: reassignment to val 的錯誤信息.

聲明var變量

如果要聲明值可以改變的引用, 可以使用val變量.
例如, val myresult = 1, myresult =2

類型推斷

無論聲明val變量, 還是聲明var變量. 都可以手動指定類型, 如果不指定的話, scala會自動根據值, 進行類型的推斷, 這種稱爲類型推斷(type inference)能力,它能讓Scala自動理解你省略了的類型。.
例如, var some = 2.0
例如, val name: String = null
例如, val name: Any = "jc"
第一個會自動判斷爲浮點, 而第二三個, 變量類型可以定義爲值的父類.

數據類型

數據類型,除了Unit、Nothing、Any、AnyRef,其他都是Java有的概念,值範圍也一樣。

數據類型 描述
Byte 8位有符號補碼整數。數值區間爲 -128 到 127
Short 16位有符號補碼整數。數值區間爲 -32768 到 32767
Int 32位有符號補碼整數。數值區間爲 -2147483648 到 2147483647
Long 4位有符號補碼整數。數值區間爲 -9223372036854775808 到 9223372036854775807
Float 32位IEEE754單精度浮點數
Double 64位IEEE754單精度浮點數
Char 16位無符號Unicode字符, 區間值爲 U+0000 到 U+FFFF
String 字符串
Boolean 布爾類型
Unit 表示無值,和其他語言中void等同。用作不返回任何結果的方法的結果類型。Unit只有一個實例值,寫成()。
Null null或空引用
Nothing Nothing類型在Scala的類層級的最低端;它是任何其他類型的子類型。
Any Any是所有其他類的超類
AnyRef AnyRef類是Scala裏所有引用類(reference class)的基類

類型的加強版類型

scala使用很多加強類給數據類型增加了上百種增強的功能或函數.
例如, String類通過StringOps類型增強了大量的函數, "Hello".intersect("World")
例如, Scala還提供了RichInt, RichDouble, RichChar等類型, RichInt就提供了to函數, 1.to(10), 此處Int先隱式轉換爲RichInt, 然後再調用其to函數.

基本操作符

scala的算術操作符與Java的算術操作符也沒有什麼區別, 比如+, -, *, /, %等, 以及&, |, ^, >>, <<等.
但是, 在scala中, 這些操作符其實是數據類型的函數, 比如 1 + 1, 可以寫做1.+(1)
例如, 1.to(10), 又可以寫做 1 to 10
scala中沒有提供++, --操作符,我們只能使用+=和-=, 比如counter=1, counter++ 是錯誤的, 必須寫做counter +=1

控制流語句

if表達式

在scala中, if表達式是有值的, 就是if或者else中最後一行語句返回的值. 簡單的理解就是scala不想參數傳來傳去, 約定由於配置, 所以就直接默認最後一句就是返回值, 在後面的函數也有所體現, 不用return, 直接最後一句就是返回值. 這樣確實對比Java來說, 敲鍵盤的次數少了很多, 臨時變量也不需要到處都是.
例如, val age = 30; if(age > 18) 1 else 0
可以將if表達式賦予一個變量, 例如, val isAdult = if(aget > 18) 1 else 0
另外一種寫法, var isAdult=-1; if(age>18) isAdult=1 else isAdult = 0, 但是通常使用上一種寫法.
還有一種多語句的寫法:

if(age>18){
  "adult"
}else if(age > 12) "teenage" else "children"  

if表達式的類型推斷

由於if表達式是有值的, 而if和else子句的值類型可能不同, 此時if表達式的值是什麼類型呢? scala會自動進行推斷, 取兩個類型的公共父類型.
例如, if(age > 18) 1 else 0, 表達式的類型是Int, 因爲1和0都是Int
例如, if(age > 18) "adult" else 0, 此時if和else的值分別是String和Int, 則表達式的值是Any, Any是String和Int的公共父類型.
如果if後面沒有跟else, 則默認else的值是Unit, 也用()表示, 類似於java中的void或者null. 例如, val age = 12; if(age > 18) "adult", 此時就相當於if(age > 18) "adult" else()

語句終結符, 塊表達式

默認情況下, scala不需要語句終結符, 默認將每一行作爲一個語句
一行放多條語句: 如果一行要放多條語句, 則必須使用語句終結符
例如, 使用分號作爲語句終結符, var a,b,c=0; if(a < 10){b = b+1; c=c+1}
通常來說, 對於多行語句, 還是會使用花括號的方式

if(a<\10){  
    b = b + 1  
    c = c + 1  
}  

塊表達式: 塊表達式, 指的就是{}中的值, 其中可以包含多條語句, 最後一條語句的值就是塊表達式的返回值.
例如, var d=if(a<10){b=b+1; c+1}

循環

while do循環

while do循環: scala有while do循環, 基本語義與java相同.

var n = 10
while(n>0){
  println(n);
  n-=1
}

scala沒有for循環

scala沒有for循環, 只能使用while替代for循環, 或者使用簡易版的for語句
簡易版for語句(包括n):

var n=10; for(i <- 1 to n) println(i)

或者使用until, 表達式不達到上限, for(i <- 1 until n) println(i), 沒執行一次pirntln(i), i會往上+1, 直至n停止(不包括n)
也可以對字符串進行變量, 類似於java的增強for循環, for(c <- "Hello World") print(c)

跳出循環語句

scala沒有提供類似於java的break語句
但是可以使用boolean類型變量, return或者Breaks的break函數來替代使用.

import scala.util.control.Breaks._
breakable{
  var n = 10
  for(c <- "Hello World"){
     if(n == 5) break;
     print(c)
     n -= 1
  }
}

高級for循環

多重for循環: 九九乘法

for(i <- 1 to 9; j <- 1 to 9){
  if(j==9){
   printf("%d * %d = %d", i,j,i*j)
   println()
  }else{
   printf("%d * %d = %d\t", i,j,i*j)
  }
}

if守衛: 取偶數

for(i <- 1 to 100 if i % 2 ==0 ) println(i)

for推導式: 構造集合  

for(i <- 1 to 10) yield i

函數

函數調用與apply()函數

先不看函數是如何定義的, 先使用起函數, 先體會, 後面再去理解函數.

函數調用方式

在scala中, 函數調用也很簡單, 例如使用數學的函數:

scala> import scala.math._
import scala.math._

scala> sqrt(2)
res0: Double = 1.4142135623730951 

scala> pow(2,4)
res2: Double = 16.0 

scala> min(3,Pi)
res4: Double = 3.0

不同的一點是, 如果調用函數時, 不需要傳遞參數,則scala允許調用函數時省略括號, 例如, "Hello World".distinct

apply函數

scala中的apply函數是非常特殊的一種函數, 在scala的object中, 可以聲明apply函數. 而使用"對象名()"的形式, 其實就是"對象名.apply()"的一種縮寫. 通常使用這種方式來構造類的對象, 而不是使用"new 類名()"的方式(注意, 這裏的對象名和類名我沒搞錯, 這個是伴生對象的特性, 後面會講解).

例如, "Hello World"(6), 因爲在StringOps類中有def apply(n: Int): Char的函數定義, 所以"Hello World"(6), 實際是"Hello World".apply(6)的縮寫.
例如, Array(1,2,3,4), 實際上是用Array object的apply()函數來創建Array類的實例, 也就是一個數組.

定義函數  

在scala中定義函數時, 需要定義函數的函數名, 參數, 函數體.
我們的第一個函數如下所示:

def sayHello(name:String, age:Int)={
  if(age>=18) {
     printf("hi %s, you are a big boy\n",name)
     age
  }
  else {
     printf("hi %s, you are a little boy\n",name)
     age
  }
}
sayHello("jevoncode",29)

scala要求必須給出所有參數的類型,但是不一定給出函數返回值的類型, 只要右側的函數體中不包含遞歸的語句, scala就可以自己根據右側的表達式推斷出返回類型.

單行函數

單行的函數: def sayHello(name: String)= print("Hello, "+name)

在代碼塊中定義函數體

如果函數體中有多行代碼, 則可以使用代碼塊的方式包裹多行代碼, 代碼塊中最後一行的方繪製就是整個函數的返回值. 與Java不同, 不是使用return返回值的.
比如下面的函數, 實現累加的功能:

def sum(n: Int)={
  var sum=0;
  for(i <-1 to n) sum+=i
  sum
}

遞歸函數

如果在函數體內遞歸調用函自身, 則必須手動給出函數的返回類型.
例如, 實現經典的斐波那契數列:
1 1 2 3 5 8 13
簡單的說斐波那契數列就是一個數是前面兩個數值之和的數列.
此函數求第n個(從0開始)斐波那契數列的值

def fab(n:Int): Int={
  if(n<=1) 1
  else fab(n-2)+fab(n-1)
}

默認參數  

在scala中, 有時我們調用某些函數時, 不希望給出參數的具體值, 而希望使用參數自身默認的值, 此時就在定義函數時使用默認參數.

def sayHello(firstName: String, middleName: String = "William", lastName: String = "Croft") = firstName + " " + middleName + " " + lastName

\\調用方式
scala> sayHello("a")
res1: String = a William Croft

scala> sayHello("a","b")
res2: String = a b Croft 

scala> sayHello("a","b","c")
res3: String = a b c

如果給出的參數不夠, 則會從左往右依次應用參數.

Java與scala實現默認參數的區別

public void sayHello(String firstName, String middleName, String lastName){
    if(middleName == null)
        middleName = "William";
    if(lastName == null)
        lastName = "Croft";

    System.out.println(firstName + " " + middleName + " " + lastName);
}
 

對比上面的scala的代碼,

  1. 從代碼上對比, 代碼量少很多.
  2. 調用Java的sayHello函數需全部字段傳入, 如sayHelle(a,null,null), 這就顯得有點麻煩

雖然Java有這樣的缺點, 但是Java提供了代理模式, 可以通過註解+proxy的方式動態的給參數注入值, 代理模式提供很大的靈活性. 但寫代碼的便利性還不如scala, 因爲無聊使不使用代理, 傳參時都得寫全.

帶名參數

在調用函數時, 也可以不按照函數定義的參數順序來傳遞參數, 而是使用帶名參數的方式來傳遞.

sayHello(firstName = "Mick", lastName="Nina", middleName="Jack")

還可以混合使用未命名參數和帶名參數, 但是未命名參數必須排在帶名參數的前面.

sayHello("Mick", lastName="Nina", middleName="Jack")

變長參數

在scala中, 有時我們需要將函數定義爲參數個數可變的形式, 則可以使用變長參數來定義函數.

def sum(nums: Int*)={
  var res = 0
  for(num <-nums) res+=num
  res
}
\\調用方式
scala> sum(1,2,3,4,5,6)
res6: Int = 21

使用序列調用變長參數

在如果想要將一個已有的序列直接調用變長參數函數, 則不對的. 比如val s=sum(1 to 5). 此時需要使用scala特殊的語法將參數定義爲序列, 讓scala解析器能夠識別. 這種語法非常有用! 一定要好好注意, 在spark的源碼中大量地使用到了.

val s = sum(1 to 5:_*)

案例: 使用遞歸函數實現累加

def sum2(nums: Int*): Int={
  if(nums.length == 0) 0
  else nums.head + sum2(nums.tail:_*)
}

過程

在scala中, 定義函數時, 如果函數體直接包含在花括號裏面, 而沒有使用=鏈接, 則函數返回值類型就是Unit. 這樣的函數就被稱爲過程. 過程通常用於不需要返回值的函數.
過程還有一種寫法, 就是將函數的返回值類型定義爲Unit.

def sayHello(name: String) = "Hello, " + name     //非過程
def sayHello(name: String) {print("Hello, "+ name); "Hello, " + name}    //過程
def sayHello(name: String): Unit = "Hello, " + name            //過程    

就是概念的定義,暫時還沒看到這概念帶來思想的昇華.  

lazy值

在scala中, 提供lazy值的特性, 也就是說, 如果將一個變量聲明爲lazy, 則有在第一次調用該變量時, 變量對於的表達式纔會發生計算.這種特性對於特別耗時的計算操作特別有用, 比如打開文件進行IO, 進行網絡IO等.

import scala.io.Source._
lazy val lines = fromFile("/home/gucci/Desktop/helloworld.txt").mkString

即使文件不存在, 也不會報錯, 只有第一個使用變量時會報錯, 證明了表達式計算的lazy特性.

scala> val lines2 = fromFile("/home/gucci/Desktop/helloworld2.txt").mkString
java.io.FileNotFoundException: /home/gucci/Desktop/helloworld2.txt (No such file or directory)
  at java.io.FileInputStream.open0(Native Method)
  at java.io.FileInputStream.open(FileInputStream.java:195)
  at java.io.FileInputStream.<init>(FileInputStream.java:138)
  at scala.io.Source$.fromFile(Source.scala:91)
  at scala.io.Source$.fromFile(Source.scala:76)
  at scala.io.Source$.fromFile(Source.scala:54)
  ... 36 elided

scala> lazy val lines = fromFile("/home/gucci/Desktop/helloworld2.txt").mkString
lines: String = <lazy>
 

scala> lines
java.io.FileNotFoundException: /home/gucci/Desktop/helloworld2.txt (No such file or directory)
  at java.io.FileInputStream.open0(Native Method)
  at java.io.FileInputStream.open(FileInputStream.java:195)
  at java.io.FileInputStream.<init>(FileInputStream.java:138)
  at scala.io.Source$.fromFile(Source.scala:91)
  at scala.io.Source$.fromFile(Source.scala:76)
  at scala.io.Source$.fromFile(Source.scala:54)
  at .lines$lzycompute(<console>:14)
  at .lines(<console>:14)
  ... 36 elided

異常

在scala中, 異常處理和捕獲機制與Jav是非常相似的.

try{
  throw new IllegalArgumentException("x should not be negative") 
}catch{
  case _:IllegalArgumentException => println("Illegal Argument!")
}finally{
  print("release resource!")
}

除了異常捕獲, 這裏還有模式匹配和匿名函數知識點, 後面高級語法會講到.

數據結構

Array

在scala中, Array代表的含義與Java中類似, 也是長度不可改變的數據. 此外, 由於scala與Java都是運行在JVM中, 雙方可以互相調用, 因此scala數組的底層實際上是Java數組. 例如字符串數組的底層就是Java的String[], 整數數組底層就是Java的Integer[]
數組初始化後, 長度就固定下來了, 而且元素全部根據其類型初始化. Int就是0, String就是null

val a = new Array[Int](10)
val a = new Array[String](10)

可以直接使用Array()創建數組, 元素類型自動推斷

val a = Array("hello", "world")
a(0) = "hi"

ArrayBuffer

在Scala中, 如果需要類似於Java的ArrayList這種長度可變的集合類, 則可以使用ArrayBuffer

// 如果不想每次都是用全限定名, 則可以預先導入ArrayBuffer類
import scala.collection.mutable.ArrayBuffer
//使用ArrayBuffer()的方式可以創建一個空的ArrayBuffer
val b = ArrayBuffer[Int]()
//使用+=操作符, 可以添加一個元素, 或者多個元素
//這個語法需謹記在心! 因爲spark源碼裏大量使用了這種集合操作語法!
b+=1
b+=(2,3,4,5)
//使用++=操作符, 可以添加其他集合中的所有元素  
b++=Array(6,7,8,9,10)
//使用trimEnd()函數, 可以從尾部截斷指定個數的元素
b.trimEnd(5)
//使用insert()函數可以在指定位置插入元素
//但這種操作效率很低, 因爲需要移動指定位置後的所有元素  
b.insert(5,6)
b.insert(6,7,8,9,10)
//使用remove()函數可以移除指定位置的元素
b.remove(1)
b.remove(1,3)
//Array與ArrayBuffer可以互相進行轉換
b.toArray
a.toBuffer

遍歷Array和ArrayBuffer

//使用for循環和until遍歷Array/ArrayBuffer
//使用until是RichInt提供的函數
for(i <- 0 until b.length)
  println(b(i))

//跳躍遍歷Array/ArrayBuffer
for(i <- 0 until (b.length,2))
  println(b(i))

//從尾部遍歷Array/ArrayBuffer
for(i <-(0 until b.length).reverse)
  println(b(i))

//使用"增強for循環"遍歷Array/ArrayBuffer
for(e <- b)
  println(e)

數組常見操作

//元素求和
val a = Array(1,2,3,4,5)
val sum = a.sum

//獲取數組最大值
val max = a.max

//對數組進行排序
scala.util.Sorting.quckSort(a)

//獲取數組中所有元素內容
a.mkString
a.mkString(",")
a.mkString("<",",",">")

//toString函數
a.toString
b.toString

使用yield和函數式編程(初體驗, 暫不需要理解)轉換數組

//對Array進行轉換, 獲取的還是Array
val a = Array(1,2,3,4,5)
val a2 = for(ele <- a) yield ele * ele

//對ArrayBuffer進行轉換, 獲取的還是ArrayBuffer 
val b = ArrayBuffer[Int]()
b+=(1,2,3,4,5)
val b2 = for(ele<-b) yield ele* ele

//結合if守衛, 僅轉換需要的元素
val a3 = for(ele <-b if ele % 2 == 0) yield ele * ele

//使用函數式編程轉換數組(通常使用是一種方式)
a.filter(_%2==0).map(2*_)
a.filter{_%2==0}map{2*_}

算法案例: 移除第一個負數之後的所有負數

// 構建數組
val a = ArrayBuffer[Int]()
a += (1,2,3,4,5,-1,-3,-5,-9)

//每發現一個負數(不包括第一個負數), 進行移除, 但這個性能比較差, 需多次移動數組
var isFoundFirstNegative = false
var arrayLength = a.length
var index = 0
while(index < arrayLength){
  if(a(index)>0){
    index+=1
   }else{
     if(!isFoundFirstNegative){isFoundFirstNegative = true; index+=1}
     else{ a.remove(index); arrayLength-=1}
   }
}

算法案例: 移除第一個負數之後的所有負數(改良版)

// 構建數組
val a = ArrayBuffer[Int]()
a += (1,2,3,4,5,-1,-3,-5,-9)

//記錄所有不需要移除的元素的所有, 稍後一次性移除所有需要移動的元素
//性能比較高, 數組內的元素遷移只需要執行一次即可
var isFoundFirstNegative = false
val keepIndexes = for(i<-0 until a.length if !isFoundFirstNegative || a(i) >=0) yield{
  if(a(i) < 0) isFoundFirstNegative = true
  i
}
for(i <-0 until keepIndexes.length) {a(i) = a(keepIndexes(i))}
a.trimEnd(a.length - keepIndexes.length)

創建Map

//創建一個不可變的Map
val ages = Map("Jevoncode"->29, "Jen"->25, "Jack"->23)
ages("Jevoncode") = 30    //出錯value update is not a member of scala.collection.immutable.Map[String,Int]


//創建一個可變的Map
val ages = scala.collection.mutable.Map("Jevoncode"->29, "Jen"->25, "Jack"->23)
ages("Jevoncode") = 30

//使用另外一個種方式定義Map元素
val ages = Map(("Jevoncode",29),("Jen",25),("Jack",23))

//創建一個空的HashMap
val ages = new scala.collection.mutable.HashMap[String,Int]
//添加元素
scala> ages += "jevoncode" ->30
res6: ages.type = Map(jevoncode -> 30)

scala> ages
res7: scala.collection.mutable.HashMap[String,Int] = Map(jevoncode -> 30)

scala> ages += "jevoncode2" ->29
res8: ages.type = Map(jevoncode2 -> 29, jevoncode -> 30)

訪問Map的元素

//獲取指定key對應的value, 如果key不存在, 會報錯
scala> val jcAge = ages("jc")
java.util.NoSuchElementException: key not found: jc
  at scala.collection.MapLike.default(MapLike.scala:232)
  at scala.collection.MapLike.default$(MapLike.scala:231)
  at scala.collection.AbstractMap.default(Map.scala:59)
  at scala.collection.mutable.HashMap.apply(HashMap.scala:65)
  ... 36 elided

scala> val jcAge = ages("jevoncode")
jcAge: Int = 30


//使用container函數檢查key是否存在
val jcAge = if(ages.contains("jc")) ages("jc") else 0

//getOrElse函數
val jcAge = ages.getOrElse("jc",0)

修改Map的元素

//更新Map的元素
ages("jevoncode") = 29 

//增加多一個元素
ages += ("Mike"->35, "Tom"->40)

//移除元素
ages -="Mike"

//更新不可變的Map
val ages2 = ages + ("Mike"->36, "Tom"->41)

//移除不可變Map的元素
val ages3 = ages-"Tom"

遍歷Map

// 遍歷map的entrySet
for((key,value) <- ages) println(key + " " +value)

// 遍歷map的key
for(key <- ages.keySet) println(key)

// 遍歷map的value
for(value <- ages.values) println(value)

//生成新map, 反轉key和value
for((key,value) <- ages) yield(value,key)

SortedMap和LinkedHashMap

//SortedMap可以自動對Map的key的排序
val ages = scala.collection.immutable.SortedMap("jevoncode"->29, "alice"->15, "jen"->25)

//LinkedHashMap可以記住插入entry的順序
val ages = new scala.collection.mutable.LinkedHashMap[String,Int]
ages("jevoncode")=30
ages("alice")=15    
ages("jen")=25

元組Tuple

//簡單Tuple
val t=("jevoncode",29)

//訪問Tuple
t._1

//zip操作, zip有拉鍊的意思
val names = Array("jevoncode","jack","mike")
val ages = Array(29,24,26)
val nameAges = names.zip(ages)
for((name,age) <- nameAges) println(name + ": "+age)

以上

點下最先開頭的那句話:

讓事情變得更加簡單方便, 注意是簡單方便, 而事情內在的複雜性並沒有降低.

我個人體會就是scala把java一些繁瑣的東西給簡化, 還有常用的功能也寫進去. 如 1 to 10數列, 元組Tuple, 還有後續的可直接定義object, extends"接口"等等.
還有就是讓語言更加語義化, 這或許對熟悉英語的人才更加有體會吧, 如:

var n=10; for(i <- 1 to n) println(i)

如多個"接口", 用with鏈接.

最後引用知乎上《Scala 是一門怎樣的語言,具有哪些優缺點?》那幾段話

Java的模塊化,給企業、大公司帶來了第一道曙光,模塊化之後,這些公司不再給程序員一整個任務,而是一大塊任務的一小塊。接口一定義,虛擬類一定義,換誰上都可以,管你是保羅·格雷厄姆這樣的明星程序員,還是一個新來的大學生,程序員不聽話就直接開除,反正模塊化之後,開除程序員的成本大大降低,這也是爲什麼谷歌、甲骨文(這貨最後收購了Java)一類的公司大規模的推崇Java,還一度提出了模塊化人事管理的理念(把人當模塊化的積木一樣隨時移進移出)。
過度企業化後,這延展出了Java的第二個特性,束縛手腳。保羅·格雷厄姆在《黑客與畫家》中寫道,Java屬於B&D(捆綁與束縛)類型的語言。爲何束縛手腳?因爲要讓新手和明星程序員寫出類似質量的代碼,儘可能的抹消人的才華對程序的影響。不同於C/C++,老手和新手寫出的Java代碼不會有上百倍的耗時差距。但同樣也導致了Java的一個弱點——不容易優化。很多優化Java代碼的程序員必須要對JVM(虛擬機)進行優化,實際上增大了很多任務難度。
Scala不把程序員當傻子。

在這就不評判這幾段話觀點是否政治正確, 因爲不同的立場, 問題的答案就有不同. 但我想表達是, 這幾段話給出了學習scala的思路, 它是一門靠經驗積累的語言, 直白的說就是語法少了很多條條框框, 讓程序員更自由.

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