Scala詳解

1       快速入門... 4html

1.1             分號... 4前端

1.2             常變量聲明... 4java

1.2.1         val常量... 4程序員

1.2.2         var變量... 4es6

1.2.3         類型推導... 5正則表達式

1.2.4         函數編程風格... 5算法

1.3             Range. 5sql

1.4             定義函數... 6編程

1.5             whileif 6設計模式

1.6             foreachfor. 7

1.7             讀取文件... 7

1.8             類、字段和方法... 7

1.9             Singleton單例對象... 9

1.10          Scala入口程序... 10

1.11          基本類型... 11

1.12          字面量... 12

1.12.1       整數字面量... 12

1.12.2       浮點數字面量... 12

1.12.3       字符字面量... 13

1.12.4       字符串字面量... 13

1.12.5       Symbol符號字面量... 14

1.12.6       布爾字面量... 15

1.13          操做符和方法... 15

1.14          數學運算... 16

1.15          關係和邏輯操做... 17

1.16          位操做符... 17

1.17          對象相等性... 18

1.18          操做符優先級... 19

1.19          基本類型富包裝類型... 19

1.20          Scala標識符... 20

2       示例:分數(有理數)運算... 20

3       內置控制結構... 24

3.1             If表達式... 24

3.2             While循環... 24

3.3             For表達式... 25

3.3.1         集合枚舉... 25

3.3.2         If守衛... 25

3.3.3         嵌套枚舉... 26

3.3.4         變量中間綁定... 26

3.3.5         yield生成新的集合... 26

3.4             try. 26

3.5             match. 27

3.6             breakcontinue. 28

3.7             變量做用域... 29

4       Scala方法和函數的區別... 29

5       函數和閉包... 31

5.1             方法... 31

5.2             本地(內嵌、局部)函數... 31

5.3             函數字面量... 32

5.4             簡化函數字面量... 33

5.5             佔位符_. 33

5.6             偏應用函數... 33

5.7             閉包... 34

5.8             可變參數... 35

5.9             命名參數... 36

5.10          缺省參數值... 36

5.11          尾(僞)遞歸... 36

6       控制抽象... 37

6.1             函數封裝變化... 37

6.2             柯里化currying. 38

6.3             編寫新的控制結構... 40

6.4             傳名參數by-name parameter. 40

7       組合與繼承... 43

7.1             抽象類... 43

7.2             無參方法... 43

7.3             extends. 44

7.4             字段重寫無參方法(或實現無參抽象方法)... 44

7.5             字段重寫字段... 45

7.6             參數化成員字段... 46

7.6.1         var成員變量、gettersetter方法... 46

7.7             調用父類構造器... 47

7.8             多態... 48

7.9             final 49

7.10          組合與繼承... 49

7.11          工廠對象... 50

8       Scala層級... 51

8.1             Scala類的層級... 51

8.2             基本類型實現... 52

8.3             NullNothing. 53

8.4             Any... 54

8.5             底層類型... 54

9       特質trait. 55

9.1             trait基本概念... 55

9.2             接口的胖與瘦... 56

9.3             特質做爲可堆疊改變... 57

9.4             爲何不是傳統的多繼承... 58

9.5             特質研究... 59

10              包、訪問修飾符... 66

10.1          ... 66

10.2          包的導入... 67

10.3          隱式導入... 68

10.4          訪問修飾符... 69

10.4.1       私有成員... 69

10.4.2       保護成員... 69

10.4.3       公開成員... 69

10.4.4       細化保護範圍... 70

10.4.5       伴生對象可見性... 70

11              斷言... 71

12              樣本類與模式匹配... 72

12.1          簡單示例... 72

12.1.1       樣本類... 72

12.1.2       模式匹配... 72

12.2          模式種類... 73

12.2.1       通配模式... 73

12.2.2       常量模式... 74

12.2.3       變量模式... 74

12.2.4       構造器模式... 75

12.2.5       序列模式... 75

12.2.6       元組模式... 75

12.2.7       類型模式... 76

12.2.8       給模式綁定變量... 76

12.3          模式守衛... 77

12.4          模式重疊... 77

12.5          封閉類... 77

12.6          Option... 78

12.7          模式無處不在... 78

12.7.1       模式在變量定義中... 78

12.7.2       PartialFunction(偏函數)... 79

12.7.2.1            Case語句塊用做偏函數... 80

12.7.3       for表達式裏的模式... 81

13              列表List. 82

13.1          列表字面量... 82

13.2          列表類型... 82

13.3          建立列表 :: 82

13.4          列表基本操做... 83

13.5          列表模式... 83

13.6          List經常使用方法... 84

13.6.1       鏈接列表:::: 84

13.6.2       列表長度:length方法... 84

13.6.3       列表尾部:lastinit方法... 84

13.6.4       反轉列表:reverse. 85

13.6.5       指定長度:takedropsplitAt. 85

13.6.6       索引方法:applyindices. 86

13.6.7       zipzipWithIndex. 86

13.6.8       toStringmkString. 86

13.6.9       列表轉換:toArray, copyToArray. 87

13.6.10              列表迭代:iterator. 87

13.6.11              示例:歸併排序... 88

13.7          List高階函數... 89

13.7.1       映射與處理:mapflatMapflattenforeach. 89

13.7.2       過濾:filterpartitionfindtakeWhiledropWhilespan. 89

13.7.3       判斷條件:forall exists. 90

13.7.4       摺疊/::\. 91

13.7.5       列表排序:sortWith. 92

13.8          List伴生對象的方法... 92

13.8.1       工廠方法:List.apply. 92

13.8.2       範圍列表:List.range. 92

13.8.3       鏈接多個列表:List.concat. 93

13.9          Scala中的類型推斷... 93

14              集合類型... 94

14.1          集合框架... 94

14.2          序列Seq. 94

14.2.1       列表List. 94

14.2.2       數組Array. 94

14.2.3       列表緩存ListBuffer. 95

14.2.4       數組緩存ArrayBuffer. 95

14.2.5       隊列Queue. 96

14.2.6       Stack. 96

14.2.7       字符串... 97

14.3          集(Set)、映射(Map... 97

14.3.1       Set. 97

14.3.2       映射Map. 98

14.3.3       默認SetMap. 99

14.3.4       排序的SetMap. 100

14.3.5       同步的SetMap. 100

14.4          可變(mutable)集合Vs.不可變(immutable)集合... 101

14.4.1       Map可變與不可變... 102

14.5          初始化集合... 103

14.5.1       集合轉化爲數組(Array)、列表(List... 104

14.5.2       SetMap的可變和不可變互轉... 104

14.6          元組Tuples. 104

15              類型參數化(泛型)... 105

15.1          //父構造器、工廠方法... 105

15.2          暴露接口與隱藏類... 107

15.3          Scala中的泛型... 108

15.4          Java中的泛型... 110

15.5          類型成員... 111

15.6          枚舉... 112

16              隱式轉換... 112

 

 

 

1       快速入門

1.1               分號

分號表示語句的結束;

若是一行只有一條語句時,能夠省略,多條時,須要分隔

通常一行結束時,表示表達式結束,除非推斷該表達式未結束:

// 末尾的等號代表下一行還有未結束的代碼.

def equalsign(s: String) =

  println("equalsign: " + s)

 

// 末尾的花括號代表下一行還有未結束的代碼.

def equalsign2(s: String) = {

  println("equalsign2: " + s)

}

 

//末尾的逗號、句號操做符均可以代表,下一行還有未結束的代碼.

def commas(s1: String,

           s2: String) = Console.

  println("comma: " + s1 +

          ", " + s2)

 

多個表達式在同一行時,須要使用分號分隔

 

1.2              常變量聲明

1.2.1      val常量

定義的引用不可變,不能再指向別的對象,至關於Java中的final

 

Scala中一切皆對象,因此,定義一切都是引用(包括定義的基本類型變量,實質上是對象)

val定義的引用不可變,指不能再指向其餘變量,但指向的內容是能夠變的:

 

val定義的常量必需要初始化

 

val的特性能併發或分佈式編程頗有好處

1.2.2      var變量

定義的引用能夠再次改變(內容就更能夠修改了),但定義時也須要初始化

 

Java中有原生類型(基礎類型),即charbyteshortintlongfloatdoubleboolean,這些都有相應的Scala類型(沒有基本類型,但比如Java中相應的包裝類型),Scala編譯成字節碼時將這些類型儘量地轉爲Java中的原生類型,使你能夠獲得原生類型的運行效率

 

valvar聲明變量時必須初始化,但這兩個關鍵字都可以用在構造函數的參數中,這時變量是該類的一個屬性所以顯然沒必要在聲明時進行初始化。此時若是用val聲明,該屬性是不可變的;若是用var聲明,則該屬性是可變的:

class Person(val name: String, var age: Int)

即姓名不可變,但年齡是變化的

val p = new Person("Dean Wampler", 29)

 

varval關鍵字只標識引用自己是否能夠指向另外一個不一樣的對象,它們並未代表其所引用的對象內容是否可變

1.2.3      類型推導

定義時能夠省略類型,會根據值來推導出類型

scala> var str = "hello"

str: String = hello

scala> var int = 1

int: Int = 1

定義時也可明確指定類型:

scala> var str2:String = "2"

str2: String = 2

1.2.4      函數編程風格

之前傳統Java都是指令式編程風格,若是代碼根本就沒有var,即僅含有val,那它或許是函數式編程風格,所以向函數式風格轉變的方式之一,多使用val,嘗試不用任何var編程

 

指令式編程風格:

  def printArgs(args: Array[String]): Unit = {

    var i = 0

    while (i < args.length) {

      println(args(i))

      i += 1

    }

  }

函數式編程風格:

  def printArgs(args: Array[String]): Unit = {

    for (arg <- args)

      println(arg)

  }

或者:

  def printArgs(args: Array[String]): Unit = {

    //若是函數字面量只有一行語句而且只帶一個參數,

    //則麼甚至連指代參數都不須要

    args.foreach(println)

  }

 

1.3               Range

數據範圍、序列

支持Range的類型包括IntLongFloatDoubleCharBigIntBigDecimal

 

Range能夠包含區間上限,也能夠不包含區間上限;步長默認爲1,也能夠指定一個非1的步長:

 

1.4               定義函數

函數是一種具備返回值(包括空Unit類型)的方法

 

函數體中最後一條語句即爲返回值。若是函數會根據不一樣狀況返回不一樣類型值時,函數的返回類型將是不一樣值的通用(父)類型,或者是能夠相互轉換的類型(如Char->Int

若是函數體只一條語句,能夠省略花括號:

def max2(x:Int,y:Int)=if(x>y)x else y

 

scala> max2(3,5)

res2: Int = 5

 

Unit:返回類型爲空,即Java中的void類型。若是函數返回爲空,則能夠省略

scala> def greet()=println("Hello")

greet: ()Unit

 

1.5               whileif

打印入口程序的外部傳入的參數:

object Test {

  def main(args: Array[String]): Unit = {

    var i = 0

    while (i < args.length) {

      if (i != 0) print(" ")

      print(args(i))

      i += 1

    }

  }

}

注:Java++ii++,但Scala中沒有。

 

Java同樣,whileif後面的布爾表達式必須放在括號裏,不能寫成諸如 if i < 10 的形式

1.6               foreachfor

object Test {

  def main(args: Array[String]): Unit = {

    var i = 0;

    args.foreach(arg => { if (i != 0) print(" "); print(arg); i += 1 })

  }

}

foreach方法參數要求傳的是函數字面量(匿名函數),arg爲函數字面量的參數,而且值爲遍歷出來的集合中的每一個元素,類型爲String,已省略,如不省略,則應爲:

    args.foreach((arg: String) => { if (i != 0) print(" "); print(arg); i += 1 })

 

若是函數字面量只有一行語句而且只帶一個參數,則麼甚至連指代參數都不須要:

    args.foreach(println)

 

也能夠使用for循環來代替:

    for (arg <- args) println(arg)

1.7               讀取文件

object Test {

  def main(args: Array[String]): Unit = {

    import scala.io.Source

    //將文件中全部行讀取到List列表中

    val lines = Source.fromFile(args(0)).getLines().toList

    //找到最長的行:相似冒泡排序,每次拿兩個元素進行比較

    val longestLine = lines.reduceLeft((a, b) => if (a.length() > b.length()) a else b)

    //最長行的長度自己的寬度

    val maxWidth = widthOfLength(longestLine)

    for (line <- lines) {

      val numSpaces = maxWidth - widthOfLength(line)

      //讓輸出的每行寬度右對齊

      val padding = " " * numSpaces

      println(padding + line.length() + " | " + line)

    }

  }

 

  def widthOfLength(s: String) = s.length().toString().length()

}

1.8               類、字段和方法

類的方法以 def 定義開始,要注意的 Scala 的方法的參數都是 val 類型,而不是 var 類型,所以在函數體內不能夠修改參數的值,好比若是你修改 add 方法以下:

 

 

使用class定義類:

class ChecksumAccumulator {}

而後就能夠使用 new 來實例化:

val cal = new ChecksumAccumulator

類裏面能夠放置字段和方法,這都稱爲成員(member)。

字段,無論是使用val仍是var,都是指向對象的變量(即Java中的引用)

方法,使用def進行定義

 

class ChecksumAccumulator {

  var sum = 0

}

 

object Test {

  def main(args: Array[String]): Unit = {

    val acc = new ChecksumAccumulator

    val csa = new ChecksumAccumulator

  }

}

剛實例化時內存的狀態以下:

注:在Scala中,因爲數字類型(整型與小數)都是final類型的,即不可變,因此在內存中若是是同一數據,則是共享的

 

因爲上面是使用var進行定義的字段,而不是val,因此能夠從新賦值:

    acc.sum = 3

如今內存狀態以下:

因爲修改了accsum的內容,因此acc.sum指向了3所在的內存。

 

對象的穩定型就是要保證對象狀態的穩定型,即對象中字段值在對象整個生命週期中持續有效。這須要將字段設爲private以阻止外界直接對它進行訪問與修改,由於私有字段只能被同一類裏的方法訪問,因此更新字段的代碼將被鎖定在類裏:

class ChecksumAccumulator {

  private var sum = 0

  def add(b: Byte): Unit = {

    sum += b

  }

}

Java 不一樣的,Scala 的缺省修飾符爲 public,也就是若是不帶有訪問範圍的修飾符 public,protected,privateScala 缺省定義爲 public

 

類的方法以 def 定義開始,要注意的 Scala 的方法的參數都是 val 類型,而不是 var 類型,所以在函數體內不能夠修改參數的值,好比若是你修改 add 方法以下

  def add(b: Byte): Unit = {

    b = 1 // 編譯出錯,由於bval error: reassignment to val

    sum += b

  }

 

若是某個方法的方法體只有一條語句,則能夠去掉花括號

def add(b: Byte): Unit = sum += b

 

類的方法分兩種,一種是有返回值的,一種是不含返回值

 

若是方法沒有返回值(或爲Unit),則定義方法時能夠去掉結果類型和等號 =,並把方法體放在花括號裏:

def add(b: Byte) { sum += b }

 

定義方法時,若是去掉方法體前面的等號 =,則方法的結果類型就必定是Unit,這無論方法體最後語句是啥,由於編譯器能夠把任何類型轉換爲Unit,如結果是String,但返回結果類型聲明爲Unit,那麼String將被轉換爲Unit並丟棄原值。下面是明肯定義返回類型爲Unit

scala> def f(): Unit = "this String gets lost"

f: ()Unit

去掉等號的方法返回值類型也必定是Unit

scala> def g() { "this String gets lost too" }

g: ()Unit

 

加上等號時,若是沒有肯定定義返回類型,則會根據方法體最後語句來推導:

scala> def h() = { "this String gets returned!" }

h: ()java.lang.String

scala> h

res0: java.lang.String = this String gets returned!

 

Scala 代碼無需使用「;」結尾,也不須要使用 return返回值,函數的最後一行的值就做爲函數的返回值

1.9               Singleton單例對象

Scala中不能定義靜態成員,而是以定義成單例對象(singleton object)來代替,即定義類時,使用的object關鍵字,而非class關鍵字,但看上去就像定義class同樣:

class ChecksumAccumulator {

  private var sum = 0

  def add(b: Byte): Unit = {

    sum += b

  }

  def checksum(): Int = {

    return ~(sum & 0xFF) + 1

  }

}

 

import scala.collection.mutable.Map

object ChecksumAccumulator {

  private val cache = Map[String, Int]()

  def calculate(s: String): Int =

    if (cache.contains(s))

      cache(s)

    else {

      val acc = new ChecksumAccumulator

      for (c <- s)

        acc.add(c.toByte)

      val cs = acc.checksum()

      cache += (s -> cs)

      cs

    }

}

當單例對象(object)與某個類(class)的名稱相同時(上面都爲ChecksumAccumulator),它就被稱爲是這個類的伴生對象類和它的伴生對象必須定義在一個源文件裏。類被稱爲這個單例對象的伴生類類和它的伴生對象能夠互相訪問其私有成員

 

能夠將單例對象看成Java中的靜態方法工具類來使用,能夠直接經過單例對象的名稱來調用:

ChecksumAccumulator.calculate("Every value is an object.")

其實,單例對象就是一個對象,不須要實例化就能夠直接經過單例對象名來訪問其成員,即單例對象名就至關於變量名,已指向了某個類的實例,只不過該類不是由你來實例化,而是在訪問它時由Scala實例化出來的,且在JVM只有一個這個的實例。在編譯伴生對象時,會生成一個相應名爲ChecksumAccumulator$(在單例對象名後加上美圓符號)的類:

 

類和單例對象的差異:單例對象定義時不帶參數(Object關鍵字後面),而類能夠(Class關鍵字後面能夠帶括號將類參數包圍起來),由於單例對象不是使用new關鍵字實例化出來的(這也是 Singleton 名字的由來)

 

單例對象在第一次被訪問的時候纔會被始化

 

沒有伴生類的單例對象被稱爲獨立對象,通常做爲相關功能方法的工具類,或者用做Scala應用的入口程序

 

1.10         Scala入口程序

Java中,只要類中有以下簽名的main方法,便可做爲程序入口程序:

class T {

   public static void main(String[] args) {}

}

Scala中,入口程序不是定義在類class中的,而是定義在單例對象中的

object T {

  def main(args: Array[String]): Unit = {}

}

Java 相似,Scala 中任何 Singleton對象(使用Object關鍵字定義),若是包含 main 方法,均可以做爲應用的入口

 

Scala的每一個源文件都會自動引入包java.langscala中的成員,和scala包中名爲Predef的單例對象的成員,該單例對象中包含了許多有用的方法,例如,當在Scala源文件中寫pringln的時候,實際調用了Predef.println,另外當你寫assert,實質上是調用Predef.assert

 

Java的源文件擴展名爲.java,而Scala的源文件擴展名爲.scala

 

Java中,若是源文件中有publicclass,則該public類的類名必須與Java源文件名一致,但在Scala中沒有這種限制,但通常會將源文件名與類名設爲一致

 

Java同樣,也有對應的編譯與運行命令,它們分別是scalac(編譯)與scala(運行),ava中的爲javacjava,無論是Java仍是Scala程序,都會編譯成.class的字節碼文件

 

Scala Singleton 對象的 main 定義了一個 App trait 類型

Scala的入口程序還能夠繼承scala.App特質(TraitScala中的TraitJava中的Interface,但不一樣的是能夠有方法的實現),這樣就不用寫main方法(由於scala.App特質裏實現了main方法),而直接將代碼寫在花括號裏,花括號裏的代碼會被收集進單例對象的主構造器中,並在類被初始化時執行:

object T extends scala.App {

  println("T")

}

缺點:命令參數行args不能再被訪問;某些JVM線程會要求main方法不能經過繼承獲得,必須本身行編寫;

 

1.11         基本類型

Java 支持的基本數據類型,Scala 都有對應的支持,不過 Scala 的數據類型都是對象,並且這些基本類型均可以經過隱式自動轉換的形式支持比 Java 基本數據類型更多的方法:好比調用 -1.abs() Scala 發現基本類型 Int 沒有提供 abs()方法,但能夠發現系統提供了從 Int 類型轉換爲 RichInt 的隱式自動轉換,而 RichInt 具備 abs 方法,那麼 Scala 就自動將 1 轉換爲 RichInt 類型,而後調用 RichInt abs 方法。

 

Scala 的基本數據類型有: Byte,ShortIntLong Char (這些成爲整數類型)。整數類型加上 Float Double 成爲數值類型。此外還有 String 類型,除 String 類型在 java.lang 包中定義,其它的類型都定義在包 scala 中。好比 Int 的全名爲 scala.Int。實際上 Scala 運行環境自動會載入包 scala java.lang 中定義的數據類型,你能夠使用直接使用 IntShortString 而無需再引入包或是使用全稱(如scala.xxjava.lang.xx)。

 

Scala的基本類型與Java對應類型範圍徹底同樣,這樣可讓Scala編譯器直接把這些類型編譯成Java中的原始類型

 

scala> var hex=0xa  //十六進制,整數默認就是Int類型

hex: Int = 10

scala> var hex:Short=0x00ff //若要Short類型,則要明確指定變量類型

hex: Short = 255

scala> var hex=0xaL //賦值時明確指定數據爲Long型,不然默認爲Int類型

hex: Long = 10

scala> val prog=2147483648L //若超過了Int範圍,則後面必定要加上 L ,置換爲Long類型

prog: Long = 2147483648

scala> val bt:Byte= 38 //若要Byte類型,則要在定義時明確指定變量的類型爲Byte類型

bt: Byte = 38

 

scala> val big=1.23232 //小數默認就是Double類型

big: Double = 1.23232

 

scala> val big=1.23232f //若是要定義成Float,則可直接在小數後面加上F

big: Float = 1.23232

scala> val big=1.23232D //雖然默認就是Double,但也可在小數後面加上D

big: Double = 1.23232

 

scala> val a='A' //類型推導成Char類型

a: Char = A

scala> val f ='\u0041' //也能夠使用Unicode編碼表示, \u 開頭,u必定要小寫,且\u後面接4位十六進制

f: Char = A

 

scala> val hello="hello" //類型推導成String類型

hello: String = hello

scala> val longString=""" Welcome to Ultamix 3000. Type "Help" for help.""" //以使用三個引號(""")開頭和結尾,這樣之間的字符都將看做是最原始的字符,不會被轉義

longString: String = " Welcome to Ultamix 3000. Type "Help" for help." //注:開頭與結尾的雙引號不屬於上面字符串的一部分,而是表示控制檯上輸出的是String

 

scala> val bool=true

bool: Boolean = true

1.12         字面量

字面量就是直接寫在代碼裏的常量值

1.12.1              整數字面量

十六進制以 0x0X開頭:

scala> val hex = 0x5

hex: Int = 5

scala> val hex2 = 0x00FF

hex2: Int = 255

scala> val magic = 0xcafebabe

magic: Int = -889275714

注:不區分大小寫

 

八進制以0開頭

scala> val oct = 035 // (八進制35是十進制29

oct: Int = 29

scala> val nov = 0777

nov: Int = 511

scala> val dec = 0321

dec: Int = 209

 

若是是非0開頭,即十進制:

scala> val dec2 = 255

dec2: Int = 255

 

注:無論字面量是幾進制,輸出時都會轉換爲十進制

 

若是整數以Ll結尾,就是Long類型,不然默認就是Int類型:

scala> val prog = 0XCAFEBABEL

prog: Long = 3405691582

scala> val tower = 35L

tower: Long = 35

scala> val of = 31l

of: Long = 31

 

從上面能夠看出,定義Int型時能夠省去類型便可,若是是Long類型,定義時也可省略Long類型,此時在數字後面加上Ll便可,但也能夠直接定義成Long也可:

scala> var lg:Long = 2

lg: Long = 2

若是要獲得ByteShort類型的變量時,需在定義時指定變量的相應類型:

scala> val little: Short = 367

little: Short = 367

scala> val littler: Byte = 38

littler: Byte = 38

1.12.2              浮點數字面量

scala> val big = 1.2345

big: Double = 1.2345

scala> val bigger = 1.2345e1

bigger: Double = 12.345

scala> val biggerStill = 123E45

biggerStill: Double = 1.23E47

 

小數默認就是Double類型,若是要是Float,則要以F結尾:

scala> val little = 1.2345F

little: Float = 1.2345

scala> val littleBigger = 3e5f

littleBigger: Float = 300000.0

 

固然Double類型也能夠D結尾,不過是可選的

scala> val anotherDouble = 3e5

anotherDouble: Double = 300000.0

scala> val yetAnother = 3e5D

yetAnother: Double = 300000.0

 

固然,也要以在定義變量時明確指定類型也可:

scala> var f2 = 1.0

f2: Double = 1.0

scala> var f2:Float = 1

f2: Float = 1.0

1.12.3              字符字面量

使用單引號引發的單個字符

scala> val a = 'A'

a: Char = A

 

單引號之間除了直接是字符外,也能夠是對應編碼,編碼是八進制或十六進制來表示

 

若是以八進制表示,則以 \ 開頭,且爲 '\0 '\377' 0377=255):

scala> val c = '\101'

c: Char = A

注:若是以八進制表示,則只能表示一個字節大小的字符,即0~255之間的ASCII碼單字節字符,若是要表示大於255Unicode字符,則只能使用十六進制來表示:

scala> val d = '\u0041'

d: Char = A scala>

val f = '\u0044'

f: Char = D

scala> val c = '\u6c5f'

c: Char =

注:以十六進制表示時,需以 \u(小寫)開頭,即後面跟4位十六進制的編碼(兩個字節)

 

實際上,十六進制能夠出如今Scala程序的任何地方,如能夠用在變量名裏:

scala> val B\u0041\u0044 = 1

BAD: Int = 1

 

轉義字符:

scala> val backslash = '\\'

backslash: Char = \

1.12.4              字符串字面量

使用雙引號引發來的0個或多個字符

scala> val hello = "hello"

hello: java.lang.String = hello

 

特殊字符也需轉義:

scala> val escapes = "\\\"\'"

escapes: java.lang.String = \"'

 

若是字符串中須要轉義的字符不少時,能夠使用三個引號(""")開頭和結尾,這樣之間的字符都將看做是最原始的字符,不會被轉義(固然三個連續的引號除外):

發現第二行前面的空格也會原樣輸出來,因此第二行前面看起來縮進了,若是要去掉每行前面的空白字符(ASCII編碼小於等於32的都會去掉),則把管道符號(|)放在每行前面,而後對字符串調用stripMargin

1.12.5              Symbol符號字面量

以單引號打頭,後面跟一個或多個數字、字母或下劃線,但第一個字符不能是數字,這種字面量會轉換成預約義類scala.Symbol的實例,如 'cymbal編譯器將會調用工廠方法Symbol("cymbal")轉化成Symbol實例。

scala> val s = 'aSymbol

s: Symbol = 'aSymbol

scala> s.name

res20: String = aSymbol

 

符號字面量 'x 是表達式 scala.Symbol("x") 的簡寫

 

JavaStringintern()方法:String類內部維護一個字符串池(strings pool),當調用Stringintern()方法時,若是字符串池中已經存在該字符串,則直接返回池中字符串引用,若是不存在,則將該字符串添加到池中,並返回該字符串對象的引用。執行過intern()方法的字符串,咱們就說這個字符串被拘禁了(interned),即放入了池子。默認狀況下,代碼中的字符串字面量和字符串常量值都是被拘禁的,例如:

      String s1 = "abc";

      String s2 = new String("abc");

      System.out.println(s1 == s2);//false

      System.out.println(s1 == s2.intern());//true

同值字符串的intern()方法返回的引用都相同,例如:

      String s2 = new String("abc");

      String s3 = new String("abc");

      System.out.println(s2 == s3);// false

      System.out.println(s2.intern() == s3.intern());// true

 

      String str1 = "abc";

      String str2 = "abc";

      System.out.println(str1 == str2);//true

      String str3 = new String("abc");

      System.out.println(str1 == str3);//false

 

Sysmbol實質上也是一種字符串,其好好處:

1. 節省內存

Scala中,Symbol類型的對象是被拘禁的(interned,即會被放入池中),任意的同名symbols都指向同一個Symbol對象,避免了因冗餘而形成的內存開銷:

    val s1 = "aSymbol"

    val s2 = "aSymbol"

    println(s1.eq(s2)) //true :代表s1s2指向同一對象

    val s3 = new String("aSymbol") //因爲在編譯時就肯定,因此仍是會放入常量池

    println(s1.eq(s3)) //false : 代表s1s3不是同一對象

    println(s1 == s3) //true:雖然不是同一對象,可是它們的內容相同

 

    val s = 'aSymbol

    println(s.eq('aSymbol)) //true

println(s.eq(Symbol("aSymbol"))) //true:只要是同名的Symbol,則都是指向同一對象

    //即便ss3的內容相同,但eq比較的是對象地址,因此不等

    println(s.name.eq(s3)) //false

    println(s.name == s3) //true:但內容相同

println(s.name.eq(s1)) //true : ss1的的內容都會放入池中,因此指向的是同一對象

注:在Scala中,若是要基於引用地址進行比較,則要使用eq方法,而不是==,這與Java是不同的

2. 快速比較

因爲Symbol類型的對象會自動拘禁的(interned),任意的同名symbols(準確的說是值)都指向同一個Symbol對象,而相同值的字符串並不必定是同一個instance,因此symbols對象之間的比較操做符==速度會很快:由於它只基於地址進行比較,若是發現不是同一Symbols對象,則就認爲不相同,不會在對內容進行比較(由於不一樣名的Symbols的值確定也不相同)

Symbol類型的應用

Symbol類型通常用於快速比較,例如用於Map類型:Map<Symbol, Data>,根據一個Symbol對象,能夠快速查詢相應的Data, Map<String, Data>的查詢效率則低不少。

雖然說利用Stringintern方法也能夠實現Map<String, Data>的鍵值快速比較,可是因爲須要顯式地調用intern()方法,在編碼時會形成不少的麻煩,並且若是忘了調用intern()方法,還會形成難以尋找的bug。從這個角度看,ScalaSymbol類型會自動進行intern操做(加入到池中),因此簡化了編碼的複雜度;而Java中除了字符串常量,是不會自動進行intern的,須要對相應對象手動調用interned方法

1.12.6              布爾字面量

scala> val bool = true

bool: Boolean = true

scala> val fool = false

fool: Boolean = false

1.13         操做符和方法

操做符如+加號,實質上是類型中有名爲 + 的方法:

scala> val sum = 1 + 2 // Scala調用了(1).+(2)

sum: Int = 3

scala> val sumMore = (1).+(2)

sumMore: Int = 3

實際上Int類型包含了名爲 + 的各類不一樣參數的重載方法,如Int+Long

scala> val longSum = 1 + 2L // Scala調用了(1).+(2L)

longSum: Long = 3

 

既然+是名爲加號的方法,能夠以 1+2 這種操做模式來調用,那麼其餘方法也是能夠以下方式來調用:

scala> val s = "Hello, world!"

s: java.lang.String = Hello, world!

scala> s indexOf 'o' // 調用了s.indexOf(’o’)

res0: Int = 4

若是有多個參數,則要使用括號:

scala> s indexOf ('o', 5) // Scala調用了s.indexOf(’o’, 5)

res1: Int = 8

 

Scala 中任何方法均可以是操做符:Scala裏的操做符不是特殊的語法,任何方法均可以是操做符,究竟是方法仍是操做符取決於你如何使用它。若是寫成s.indexOf('o')indexOf就不是操做符。不過若是寫成,s indexOf 'o',那麼indexOf就是操做符了

 

上面看到的是中綴操做符,仍是前綴操做符,如 -7裏的「-」,後綴操做符如 7 toLong裏的「toLong」( 實爲 7.toLong )。

前綴操做符與後綴操做都只有一個操做數,是一元(unary)操做符,如-2.0!found~0xFF,這些操做符對應的方法是在操做符前加上前綴「unary_」:

scala> -2.0 // Scala調用了(2.0).unary_-

res2: Double = -2.0

scala> (2.0).unary_-

res3: Double = -2.0

 

能夠看成前綴操做符用的標識符只有+-!~。所以,若是你定義了名爲unary_!的方法,就能夠對值或變量用 !P 這樣的前綴操做符方式調用方法。可是若是你定義了名爲unary_*的方法,就沒辦法將其用成前綴操做符了,由於*不是四種能夠看成前綴操做符用的標識符之一。

 

後綴操做符是不用點括號調用的不帶任何參數的方法:

scala> val s = "Hello, world!"

s: java.lang.String = Hello, world!

scala> s.toLowerCase()

res4: java.lang.String = hello, world!

因爲不帶參數,則可能省略括號

scala> s.toLowerCase

res5: java.lang.String = hello, world!

還能夠省去點:

scala> import scala.language.postfixOps//須要導一下這個來激活後綴操做符使用方式,不然會警告

import scala.language.postfixOps

scala> s toLowerCase

res6: java.lang.String = hello, world!

1.14         數學運算

scala> 1.2 + 2.3

res6: Double = 3.5

scala> 3 - 1

res7: Int = 2

scala> 'b' - 'a'

res8: Int = 1

scala> 2L * 3L

res9: Long = 6

scala> 11 / 4

res10: Int = 2

scala> 11 % 4

res11: Int = 3

scala> 11.0f / 4.0f

res12: Float = 2.75

scala> 11.0 % 4.0

res13: Double = 3.0

 

數字類型還提供了一元的前綴 + - 操做符(方法 unary_+ unary_-

scala> val neg = 1 + -3

neg: Int = -2

scala> val y = +3

y: Int = 3

scala> -neg

res15: Int = 2

1.15         關係和邏輯操做

scala> 1 > 2

res16: Boolean = false

scala> 1 < 2

res17: Boolean = true

scala> 1.0 <= 1.0

res18: Boolean = true

scala> 3.5f >= 3.6f

res19: Boolean = false

scala> 'a' >= 'A'

res20: Boolean = true

 

能夠使用一元操做符!unary_!方法)改變Boolean值:

scala> val thisIsBoring = !true

thisIsBoring: Boolean = false

scala> !thisIsBoring

res21: Boolean = true

 

邏輯與(&&)和邏輯或(||):

scala> val toBe = true

toBe: Boolean = true

scala> val question = toBe || !toBe

question: Boolean = true

scala> val paradox = toBe && !toBe

paradox: Boolean = false

 

Java裏同樣,邏輯與和邏輯或有短路:

scala> def salt() = { println("salt"); false }

salt: ()Boolean

scala> def pepper() = { println("pepper"); true }

pepper: ()Boolean

scala> pepper() && salt()

pepper

salt

res22: Boolean = false

scala> salt() && pepper()

salt

res23: Boolean = false

 

scala> pepper() || salt()

pepper

res24: Boolean = true

scala> salt() || pepper()

salt

pepper

res25: Boolean = true

1.16         位操做符

按位與運算(&):都爲1時才爲1,不然爲0

按位或運算(|):只要有一個爲1 就爲1,不然爲0

按位異或運算(^):相同位產生0,不一樣產生1,所以0011 ^ 0101產生0110

scala> 1 & 2  // 0001 & 0010 = 0000 = 0

res24: Int = 0

scala> 1 | 2 // 0001 | 0010 = 0011 = 3

res25: Int = 3

scala> 1 ˆ 3 // 0001 ^ 0011 = 0010 = 2

res26: Int = 2

scala> ~1  // ~0001 = 1110(負數原碼:從最末一位向前除符號位各位取反便可) = 1010 = -2

res27: Int = -2

負數補碼:反碼+1

 

左移(<<),右移(>>)和無符號右移(>>>)。左移和無符號右移在移動的時候填入零。右移則在移動時填入左側整數的最高位(符號位)。

scala> -1 >> 31 //1111 1111 1111 1111 1111 1111 1111 1111 向右移動後,仍是 1111 1111 1111 1111 1111 1111 1111 1111,左側用符號位1填充

res38: Int = -1

scala> -1 >>> 31 //1111 1111 1111 1111 1111 1111 1111 1111 向右無符號移動後,爲0000 0000 0000 0000 0000 0000 0000 0001,左側用0填充

es39: Int = 1

scala> 1 << 2 //0000 0000 0000 0000 0000 0000 0000 0001 向左位移後,爲0000 0000 0000 0000 0000 0000 0000 0100,右側用0填充

res40: Int = 4

1.17         對象相等性

基本類型比較:

scala> 1 == 2
res31: Boolean = false
scala> 1 != 2
res32: Boolean = true
scala> 2 == 2
res33: Boolean = true

 

這些操做對全部對象都起做用,而不只僅是基本類型,如列表的比較:

scala> List(1, 2, 3) == List(1, 2, 3)
res34: Boolean = true
scala> List(1, 2, 3) == List(1, 3, 2)
res35: Boolean = false

 

還能夠對不一樣類型進行比較,如:

scala> 1 == 1.0
res36: Boolean = true
scala> List(1, 2, 3) == "hello"
res37: Boolean = false

 

甚至能夠與null進行比較:

scala> List(1, 2, 3) == null
res38: Boolean = false

 

== 操做符在比較以前,會先判斷左側的操做符是否爲null,不爲null時再調用左操做數的equals方法進行比較,比較的結果主要是看這個左操做數的equals方法是怎麼實現的。只要比較的二者內容相同且而且equals方法是基於內容編寫的,不一樣對象之間比較也可能爲truex == that判斷表達式判斷的過程實質以下:

    if (x.eq(null))

      that eq null

    else

      x.equals(that)

equals方法是檢查值是否相等,而eq方法檢查的是引用是否相等。因此若是使用 == 操做符比較兩個對象時,若是左操做數是null,那麼調用eq判斷右操做數也是否爲null;若是左操做數不是null的狀況則調用equals基於對象內容進行比較

 

!= 就是 == 計算結果取反

Java中的==便可以比較原始類型,也能夠比較引用類型。對於原始類型,Java==比較值的相等性,與Scala一致,而對於引用類型,Java==是比較這兩個引用是否都指向了同一個對象,不過Scala比較對象地址不是 == 而是使用eq方法,因此Scala中的 == 操做符做用於對象時,會轉換調用equals方法來比較對象的內容(在左操做數非null狀況下)

 

AnyRefequals方法默認調用eq方法實現,也就是說,默認狀況下,判斷兩個變量相等,要求必須指向同一個對象實例

 

注:在Scala中,若是要基於引用地址進行比較,則要使用eq方法,而不是==,這與Java是不同的

1.18         操做符優先級

Scala沒有操做符,操做符只是方法的一種表達方式,其優先級是根據做用符的第一個字符來判斷的(也有例外,請看後面以等號 = 字符結束的一些操做符),如規定第一個字符*就比+的優先級高。以操做符的第一個字符爲依據,優先級以下:

(全部其餘的特殊字符)
* / %
+ -
:
= !
< >
&

^

|
(全部字母)

(全部賦值操做)

上面同一行的字符具備一樣的優先級

 

scala> 2 << 2 + 2 // 2 << (2 + 2)
res41: Int = 32

<<操做符第一個字符爲<,根據上表 << 要比 + 優先級低

 

若是操做符以等號字符( =)結束 且操做符並不是比較操做符<= >= ==,或=,那麼這個操做符的優先級與賦值符( =)相同。也就是說,它比任何其餘操做符的優先級都低。例如:

x *= y + 1

與下面的相同:

x *= (y + 1)

操做符 *= = 結束,被看成賦值操做符,它的優先級低於+,儘管操做符的第一個字符是*看起來高於+

 

任何以「:」字符結尾的方法由它的右操做數調用,並傳入左操做數;其餘結尾的方法與之相反,它們被左操做數調用,並傳入右操做:a * b 變成 a.*(b) a:::b 變成 b.:::(a)

 

多個同優先級操做符出現時,若是方法以:結尾,它們就被從右往左進行分組;反之,就從左往右進行分組,如:a ::: b ::: c 會被看成 a :::

(b ::: c),而 a * b * c 被看成(a * b) * c

1.19         基本類型富包裝類型

基本類型除了一些常見的算術操做外,還有一些更爲豐富的操做,這些操做能夠直接使用,更多須要參考API中對應的富包裝類:

 

上述相應的富操做都是由下面相應的富包裝類提供的,使用前會先自動進行隱式轉換:

1.20         Scala標識符

變量(本地變量、方法參數、成員字段)、或方法的定義名都是標識符

標識符由字母數字操做符組成

 

Scala中有4種標識符:

字母數字標識符:以字母或下劃線開始,後面能夠跟字母、數字或下劃線。注:$ 字符自己也是看成字母的,但被Scala編譯器保留做爲特殊標識符使用,因此用戶定義的標識符中最好不要包含 $ 字符,儘管可以編譯經過。另外,雖然下劃線能夠用來作爲標識符,但一樣也有不少其餘非標識符用法,因此也最好避免在標識符中含有下劃線

Java中常量習慣全大寫,且單詞之間使用下劃線鏈接,但Scala裏習慣第一個字母必須大寫,其餘仍是駝峯形式

操做符標識符:由一個或多個操做符組成,操做符是一些如 +:?~ # 的可打印ASCII字符(精確的說,應該是字母、數字、括號、方括號、花括號、單引號、雙引號、下劃線、句號、分號、冒號、回退字符\b),如下是一些操做符標識符:

+++:::<?>:->

Scala編譯器內部會將操做符標識符轉換成含有 $ 字符的Java標識符,如操做符標識符 :-> 將被編譯器轉換成相應Java標識符 $colon$minus$greater colon:冒號,minus:負號,greater:大於),若是要從Java代碼訪問這個標識符,則應該使用這個轉換後的標識符,而不是原始的

Java x<-y 會被拆分紅4個詞彙符號,因此與 x < - y 同樣,但在Scala裏,<- 將被做爲一個標識符,因此會被拆分紅3個詞彙,從而獲得 x <- y,若是想要拆分紅 < 的話,須要在 < 之間加上一個空格

混合標識符:由字母、數字組成,後面跟下劃線和一個操做符標識符,如 unary_+ 被用作定義一元操做符 + 的方法名,myvar_= 被用作定義賦值操做符 = 的方法名(myvar_=是由編譯器用來支持屬性property的)

字面量標識符:是用反引  `...` 包括的任意字符串,如 `x` `<clinit>` `yield`,由於yieldScala中是保留字,因此在Scala中不能直接調用java.lang.Thread.yield()(使當前線程從運行狀態變爲就緒狀態),而是這樣調用java.lang.Thread.`yield`()

2       示例:分數(有理數)運算

有理數是一個整數a和一個非零整數b的比,例如3/8,通則爲a/b,又稱做分數。

0也是有理數。有理數是整數和分數的集合,整數也可看作是分母爲1的分數。

有理數的小數部分是有限或爲無限循環的數。無理數的小數部分是無限不循環的數。

 

與浮點數相比較,有理數的優點是小數部分獲得了徹底表達,沒有舍入或估算

 

下面設計分數這樣的類:

class Rational(n: Int, d: Int) // 分子:n、分母:d

若是類沒有主體,則能夠省略掉花括號。括號裏的nd參數,而且編譯器會建立帶這兩個參數的主構造器(有主就有從)

 

Scala編譯器將把類的內部任何即不是字段也不是方法的定義代碼編譯到主構造器中,如:

class Rational(n: Int, d: Int) {

  println("Created " + n + "/" + d)

}

scala> new Rational(1, 2)
Created 1/2
res0: Rational =
Rational@90110a

 

Rational 類繼承了定義在 java.lang.Object 類上的 toString方法,因此打印輸出了「Rational@90110a」。重寫toString方法:

class Rational(n: Int, d: Int) {

  override def toString = n + "/" + d // 重寫時override關鍵字不能省略,這與Java不同

}

scala> val x = new Rational(1, 3)
x: Rational = 1/3

 

構造時,分母非0檢查:

class Rational(n: Int, d: Int) {

  require(d != 0) //此句會放入主構造器中

  override def toString = n + "/" + d

}

require方法爲scala包中Predef對象中定義的方法,編譯器會自動引入到源文件中,因此可直接使用不需導入。傳入的若是是false時,會拋java.lang.IllegalArgumentException異常,對象構造失敗

 

實現add方法:

class Rational(n: Int, d: Int) {

  require(d != 0)

  override def toString = n + "/" + d

  def add(that: Rational): Rational =

    new Rational(n * that.d + d * that.n, d * that.d)

}

因爲nd只是類參數,在整個類範圍內是可見(但由於只是類的參數,做用域比較廣而已——至關於方法的參數來講,編譯器是不會爲這些類的參數自動建立出相應的成員字段的),但只能被調用它的對象訪問,其餘對象不能訪問。代碼中的that對象並非調用add方法的對象,因此編譯時出錯:

因此須要將nd轉存到字段成員中才可訪問:

class Rational(n: Int, d: Int) {

  require(d != 0)

  private val numer: Int = n //Java同樣,私有的也能夠在同一類中的全部對象中訪問

  private val denom: Int = d

  override def toString = n + "/" + d

  // 1/2 + 2/3 = (1*3)/(2 *3) + (2*2)/(3*2)

  def add(that: Rational): Rational =

    new Rational(n * that.denom + d * that.numer, d * that.denom)

}

 

scala> val oneHalf = new Rational(1, 2)
oneHalf: Rational = 1/2
scala> val twoThirds = new Rational(2, 3)
twoThirds: Rational = 2/3
scala> oneHalf add twoThirds
res0: Rational = 7/6

 

比大小:

def lessThan(that: Rational) = numer * that.denom < that.numer * denom

返回較大的:

def max(that: Rational) = if (lessThan(that)) that else this //Scala 也使用 this 來引用當前對象自己

 

在定義類時,不少時候須要定義多個構造函數,在 Scala 中,除主構造函數以外的構造函數都稱爲輔助構造函數(或是從構造函數)Scala 定義輔助構造函數使用 this()的語法,全部輔助構造函數名稱爲 this。如當分母爲1時,只需傳入分子,分母固定爲1,下面增長一個這樣的從構造器:

def this(n: Int) = this(n, 1)

Scala 的從構造器以 def this(...) 定義形式開頭。每一個從構造器的第一個語句都是調用同類裏的其餘構造器,但最終都會以調用主構造器而結束(注:在類裏面調用自身主構造器也是使用this(...)形式來調用的),這樣使得每一個構造函數最終都會調用主構造函數,所以主構造器是類的惟一入口點。 Scala 中也只有主構造函數才能(會)去調用基類的構造函數

而在Java中,構造器的第一個語句只有兩個選擇:要麼調用同類的其餘構造器,要麼直接調用父類的構造器,若是省略,則默認爲super(),即默認會調用父類的無參默認構造器

 

加入分數最簡形式,形如66/42能夠簡化爲11/7,分子分母同時除以最大公約數6

class Rational(n: Int, d: Int) {

  //此句會放入主構造器中

  require(d != 0)

  //分子與分母的最大公約數。Scala也會根據成員變量出現的順序依次初始化它們,因此通常在使用前定義並初始化它,雖然在Scala中能夠將g定義在numerdenom後面,但這樣易出錯,這與Java不太同樣

  private val g = gcd(n.abs, d.abs)

  private val numer: Int = n / g //Java同樣,私有的也能夠在同一類中的全部對象中訪問

  private val denom: Int = d / g

 

  //從構造器

  def this(n: Int) = this(n, 1)//調用主構造器

  override def toString = numer + "/" + denom

  // 1/2 + 2/3 = (1*3)/(2 *3) + (2*2)/(3*2)

  def add(that: Rational): Rational =

    new Rational(numer * that.denom + denom * that.numer, denom * that.denom)

  //求兩個數的最大公約數

  private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

}

 

object T {

  def main(args: Array[String]): Unit = {

    println(new Rational(66, 42)) //11/7

    val oneHalf = new Rational(1, 2)

    val twoThirds = new Rational(2, 3)

    println(oneHalf add twoThirds) //7/6

  }

}

 

 

定義操做符:到目前爲止,已實現了分數相加的方法add,但不能像Scala庫裏面數字類型那樣使用 + 操做符來完成兩個分數的相加,其實將add方法名改成 + 便可:

class Rational(n: Int, d: Int) {

  //此句會放入主構造器中

  require(d != 0)

  //分子與分母的最大公約數

  private val g = gcd(n.abs, d.abs)

  private val numer: Int = n / g //Java同樣,私有的也能夠在同一類中的全部對象中訪問

  private val denom: Int = d / g

 

  //從構造器

  def this(n: Int) = this(n, 1)

  override def toString = numer + "/" + denom

  // 1/2 + 2/3 = (1*3)/(2 *3) + (2*2)/(3*2)

  def +(that: Rational): Rational =

    new Rational(numer * that.denom + denom * that.numer, denom * that.denom)

  //實現乘法

  def *(that: Rational): Rational =

    new Rational(numer * that.numer, denom * that.denom)

  //求兩個數的最大公約數

  private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

}

 

object T {

  def main(args: Array[String]): Unit = {

    val x = new Rational(1, 2)

    val y = new Rational(2, 3)

    //會自動根據優先級來進行運算

    println(x + x * y) //5/6

    println((x + x) * y) //2/3

  }

}

 

上面只是針對分數Rational進行加、乘運行,不能與Int進行運算,下面對這些方法進行重載,並加上減、除運算:

class Rational(n: Int, d: Int) {

  //此句會放入主構造器中

  require(d != 0)

  //分子與分母的最大公約數

  private val g = gcd(n.abs, d.abs)

  private val numer: Int = n / g //Java同樣,私有的也能夠在同一類中的全部對象中訪問

  private val denom: Int = d / g

 

  //從構造器

  def this(n: Int) = this(n, 1)

  override def toString = numer + "/" + denom

  // 1/2 + 2/3 = (1*3)/(2 *3) + (2*2)/(3*2)

  def +(that: Rational): Rational =

    new Rational(numer * that.denom + denom * that.numer, denom * that.denom)

  def +(i: Int): Rational = new Rational(numer + i * denom, denom)//方法重載

  // 1/2 - 2/3 = (1*3)/(2 *3) - (2*2)/(3*2)

  def -(that: Rational): Rational = new Rational(numer * that.denom - that.numer * denom, denom * that.denom)

  def -(i: Int): Rational = new Rational(numer - i * denom, denom) //方法重載

  //1/2 * 2/3 =(1*2)/(2*3)

  def *(that: Rational): Rational =

    new Rational(numer * that.numer, denom * that.denom)

  def *(i: Int): Rational = new Rational(numer * i, denom) //方法重載

  //1/2 / 2/3 =(1*3)/(2*2)

  def /(that: Rational): Rational = new Rational(numer * that.denom, denom * that.numer)

  def /(i: Int): Rational = new Rational(numer, denom * i) //方法重載

  //求兩個數的最大公約數

  private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

}

 

object T {

  def main(args: Array[String]): Unit = {

    println(new Rational(2, 3) * 2)// 4/3

  }

}

 

上面只能使用 new Rational(2, 3) * 2 ,而不能倒過來 2 * new Rational(2, 3),由於Int類就沒有針對 Rational類型進行運算的方法,Rational不是Scala的標準類型,若是想使用2 * new Rational(2, 3) 這種形式,則須要在使用前進行隱式轉換,則Int類型轉換爲Rational類型:

//須要在使用以前定義從IntRational的隱式轉換方法

implicit def int2Rational(x: Int) = new Rational(x)

println(2 * new Rational(2, 3)) // 4/3

增長這個隱式置換後,其實此時 Rational 的一些Int重載方法是多餘的,由於第一個整型操做符已轉換爲Rational類型,且第二個操做符也是Rational類型,因此Int類型的重載方法就多餘了

3       內置控制結構

和其它語言(好比 JavaC#)相比,Scala 只內置了爲數很少的幾種程序控制語句: ifwhilefor trymatch 以及函數調用,Scala 沒有內置不少控制結構,這是由於 Scala 賦予了程序員本身經過函數來擴展控制結構的能力

 

Scala的控制結構特色都是有返回值的,若是沒有這種特色,程序員經常須要建立一個臨時變量用來保存結果

3.1               If表達式

Java傳統方式:

var filename = "default.txt"

if (!args.isEmpty)

filename = args(0)

 

Scala中寫法:

val filename = if (!args.isEmpty) args(0) else "default.txt"

這段代碼使用 val 而無需使用 var 類型的變量。使用 val 爲函數式編程風格

3.2               While循環

  //計算最大公約數:(69= 3

  def gcdLoop(x: Long, y: Long): Long = {

    var a = x

    var b = y

    while (a != 0) {

      val temp = a

      a = b % a

      b = temp

    }

    b

  }

 

  //從控制檯循環讀取輸入行

    var line = ""

    do {

      line = readLine()

      println("Read: " + line)

    } while (line != "")

whiledo-while結構之因此稱爲「循環」,而不是表達式,是由於它們不能產生有意義的結果,循環結果返回結果的類型是Unit,寫作()()的存在是ScalaUnit不一樣於Javavoid的地方:

  //返回值爲空

  def greet() { println("hi") }

 

  def main(args: Array[String]): Unit = {

    println(greet() == ())//hi true

  }

另外,在Java等編程語言中,賦值語句自己會返回被賦予的那值:

              String line = "";

              System.out.println(line = "hello");// hello

Scala中,賦值語句自己不會再返回被賦予的那值,而是Unit

    var line = ""

    println(line = "ho") // ()

因此下面從控制檯讀取將永遠不能結束:

    var line = ""

    while ((line = readLine()) != "") // 不起做用,由於賦值語句固定返回爲Unit()

      println("Read: " + line)

 

因爲while循環不產生值,所以在純函數式語言中不推薦使用,它適合於傳統指令式編程,而使用遞歸的函數式風格能夠替代while。下面使用這種遞歸的函數式風格來代替上面使用while指令式風格求最大公約數:

  //計算最大公約數,使用遞歸函數實現

  def gcd(x: Long, y: Long): Long =

    if (y == 0) x else gcd(y, x % y)

3.3               For表達式

3.3.1      集合枚舉

枚舉當前目錄下全部文件,Java傳統作法:

              File[] filesHere = new java.io.File(".").listFiles();

              for (int i = 0; i < filesHere.length; i++) {

                     System.out.println(filesHere[i]);

              }

Scala枚舉當前目錄下的全部文件(包括目錄):

val filesHere = new java.io.File(".").listFiles

    for (file <- filesHere)

      println(file)

<–爲提取符,提取集合中的元素

 

Java1.5之後也有相似的語法:

              File[] filesHere = new java.io.File(".").listFiles();

              for (File file : filesHere) {

                     System.out.println(file);

              }

 

Scala for 表達式支持全部類型的集合類型,而不只僅是數組,好比下面使用 for 表達式來枚舉一個 Range 類型:

    for (i <- 1 to 4)

      println("Iteration " + i)

 

    for (i <- 1 until 4)//不包括邊界4

      println("Iteration " + i)

 

    val filesHere = new java.io.File(".").listFiles

    //也可按傳統方式經過索引遍歷數組元數,但不推薦這樣使用

    for (i <- 0 to filesHere.length - 1)

      println(filesHere(i))

3.3.2      If守衛

for語句中添加if過濾器:

    val filesHere = (new java.io.File(".")).listFiles

    //只打印出後綴名爲 .project 的文件

    for (file <- filesHere if file.getName.endsWith(".project"))

      println(file)

也能夠將if語句拿出來寫在循環體中,但不推薦:

    for (file <- filesHere)

      if (file.getName.endsWith(".project"))

        println(file)

 

能夠添加多個過濾器:

    for (file <- filesHere if file.isFile if file.getName.endsWith(".project"))

      println(file)

3.3.3      嵌套枚舉

for語句中能夠使用多個 <- 提取符造成嵌套循環。

下面外層循環是針對擴展名爲.classpath的文件進行循環,而後讀取每一個文件中含有con字符的文本行:

    val filesHere = (new java.io.File(".")).listFiles

 

    def fileLines(file: java.io.File) =

      scala.io.Source.fromFile(file).getLines.toList

    def grep(pattern: String) =

      for (

        file <- filesHere if file.getName.endsWith(".classpath"); //循環文件

        line <- fileLines(file) if line.trim.matches(pattern) //循環文本行

      ) println(file + ": " + line.trim)

    grep(".*con.*")

注:嵌套之間要使用分號分隔,不過能夠使用花括號來代替小括號,此時嵌套之間的分號就能夠省略了:

    def grep(pattern: String) =

      for {

        file <- filesHere if file.getName.endsWith(".classpath")

        line <- fileLines(file) if line.trim.matches(pattern)

      } println(file + ": " + line.trim)

3.3.4      變量中間綁定

若是某個方法屢次用,能夠將其先賦給某個val變量,這樣只需計算一次,如上面兩處調用line.trim

    def grep(pattern: String) =

      for {

        file <- filesHere if file.getName.endsWith(".classpath")

        line <- fileLines(file)

        trimmed = line.trim //結果臨時保存在 trimmed 變量,能夠定義val中間變量,val關鍵字能夠省略

        if trimmed.matches(pattern)

      } println(file + ": " + trimmed)

3.3.5      yield生成新的集合

for clauses yield body

關鍵字 yield 放在 body(這裏的body指單條語句)的前面,for 每迭代一次,產生一個 bodyyield 收集全部的 body 結果,返回一個 body 類型的集合(通常狀況下,當返回的元素類型與源集合中的元素類型相同時,則返回的集合類型與源集合類型相同;若是返回的元素類型與源集合元素類型不一樣時,則兩集合類型則可能不一樣,請看下面代碼)。如列出全部名爲 .scala 結尾的文件,返回這些文件的集合(scalaFiles: Array[java.io.File]):

val filesHere = (new java.io.File(".")).listFiles                                 // filesHere: Array[java.io.File]

def scalaFiles = for {file <- filesHere if file.getName.endsWith(".scala")} yield file  // scalaFiles: Array[java.io.File]

yieldfor同樣是關鍵字,它應該放在循環體最前面,若是循環體有多個語句時,能夠使用花括號包起來,yield放在花括號外:

for {子句} yield {循環體}

 

scala> val arr = Array("a", "b")

arr: Array[String] = Array(a, b)

scala> val arr2 = for (e <- arr) yield e.length

arr2: Array[Int] = Array(1, 1) // 注:返回的類型仍是Array,但裏面元素的類型變了

 

scala> val map = Map(1 -> "a", 2 -> "b")

map: scala.collection.immutable.Map[Int,String] = Map(1 -> a, 2 -> b)

scala> val map2 = for ((k, v) <- map) yield v.length

map2: scala.collection.immutable.Iterable[Int] = List(1, 1) // 注:這裏返回的是List類型,不是Array,也不是Map,由於Map變長,因此返回的List

scala> val map3 = for ((k, v) <- map) yield (k, v)

map3: scala.collection.immutable.Map[Int,String] = Map(1 -> a, 2 -> b)

3.4               try

    val half =

      if (n % 2 == 0) n / 2

      else

        throw new RuntimeException("n must be even")

儘管throw不實際產生任何值,你仍是能夠把它看成表達式,throw語句返回的類型爲Nothing

scala> def t = throw new RuntimeException("n must be even")

t: Nothing

 

if 的一個分支計算值,另外一個拋出異常並得出 Nothing,整個 if 表達式的類型就是那個實際計算值的分支的類型,因此上面的half類型爲Int,由於Nothing是任何類型的子類型,因此整個if表達式的類型爲父類型Int

 

    import java.io.FileReader

    import java.io.FileNotFoundException

    import java.io.IOException

    var f: FileReader = null

    try {

      f = new FileReader("input.txt")

      // Use and close file

 

    } catch {//若是打開文件出現異常,將先檢查是不是 FileNotFoundException 異常,若是不是,再檢查是不是 IOException,若是還不是,在終止 try-catch 塊的運行,而向上傳遞這個異常

      case ex: FileNotFoundException =>

      // 文件不存在捕獲後在此處理異常

      // ...

      case ex: IOException           =>

      // 其它 I/O 錯誤捕獲後在此處理異常

      // ...

    } finally { // Java同樣,無論 try 塊是否拋出異常,finally塊都會執行

      f.close() // 文件必定會被關閉

    }

注:與 Java 異常處理不一樣的一點是,Scala 不須要你捕獲 checked 的異常,因此下面語句雖然要拋出檢測異常FileNotFoundException,但不須要使用try-catch 塊來包圍,這在Java中是不行的:

    var f = new FileReader("input.txt")

 

 

和其它大多數Scala控制結構同樣,try-catch-finally也產生值Scala的行爲與Java的差異僅源於Javatry-finally不產生值),好比下面的例子嘗試分析一個 URL,若是輸入的 URL 無效,則使用缺省的 URL 連接地址:

  import java.net.URL

  import java.net.MalformedURLException

  def urlFor(path: String) =

    try {

      new URL(path)

    } catch {

      case e: MalformedURLException =>

        new URL("http://www.scalalang.org")//缺省的 URL

    }

若是拋異常但未捕獲異常,則表達式沒有值。

 

finally子句當使用return顯示的返回時,這個值將覆蓋 try-catch 產生的結果:

  def f(): Int = try { return 1 } finally { return 2 }

  println(f()) //2             結果會被返回

不然即便finally塊最後一句有值,也會被拋棄:

  def g(): Int = try { 1 } finally { 2 }

  println(g()) //1              結果會被拋棄

正是由於這樣容易弄混,因此finally子句不要返回值,而只做如關閉資源、清理之類的工做

3.5               match

相似Java中的switch,從多個選擇中選取其一。match 表達式支持任意的匹配模式

 

    val firstArg = if (args.length > 0) args(0) else ""

    firstArg match {

      case "salt"  => println("pepper")

      case "chips" => println("salsa")

      case "eggs"  => println("bacon")

      case _       => println("huh?")

    }

_下劃線表示其它,相似Java中的default

不像Java那樣,firstArg能夠是任何類型,而不僅是整型或枚舉,示例中是字符串。另外,每一個可選項最後並無break,隱含就有

 

match表達式還能夠產生值:

    val firstArg = if (!args.isEmpty) args(0) else ""

    val friend =

      firstArg match {

        case "salt"  => "pepper"

        case "chips" => "salsa"

        case "eggs"  => "bacon"

        case _       => "huh?"

      }

    println(friend)

3.6               breakcontinue

Scala 內置控制結構特意去掉了 break continue

 

如從一組字符串中尋找以「 .scala 」結尾的字符串,但跳過以「-」開頭的字符串,Java中能夠這樣實現:

int i = 0;

boolean foundIt = false;

while (i < args.length) {

   if (args[i].startsWith("-")) {

      i = i + 1;

      continue;

   }

   if (args[i].endsWith(".scala")) {

      foundIt = true;

      break;

   }

   i = i + 1;

}

完成能夠這樣,經過調代碼結構能夠去掉它們:

var i = 0

var foundIt = false

while (i < args.length && !foundIt) {

  if (!args(i).startsWith("-")) {

    if (args(i).endsWith(".scala"))

      foundIt = true

  }

  i = i + 1

}

 

另外,在Scala中完使用能夠遞歸函數來代替循環,下面使用遞歸實現上面一樣功能:

def searchFrom(i: Int): Int =

  if (i >= args.length) -1

  else if (args(i).startsWith("-")) searchFrom(i + 1)

  else if (args(i).endsWith(".scala")) i

  else searchFrom(i + 1)

val i = searchFrom(0)

在函數化編程中使用遞歸函數來實現循環是很是常見的一種方法,咱們應用熟悉使用遞歸函數的用法

 

若是你實在仍是但願使用 breakScala scala.util.control 包中定義了 break 控制結構,它的實現是經過拋出異常給上級調用函數。下面給出使用 break 的一個例子,不停的從屏幕讀取一個非空行,若是用戶輸入一個空行,則退出循環:

import scala.util.control.Breaks._

import java.io._

val in = new BufferedReader(new InputStreamReader(System.in))

breakable {//breakable是帶了一個傳名參數的方法

  while (true) {

    println("? ")

    if (in.readLine() == "") break //break也是一個方法,會拋異常BreakControl

  }

}

 

3.7               變量做用域

Scala容許你在嵌套範圍內定義同名變量

 

大括號一般引入了一個新的範圍,因此任何定義在花括號裏的東西在括號以後就脫離了範圍(但有個例外,由於嵌套for語句能夠使用花括號來代替小括號,因此此種除外,可參見這裏的中間變量trimmed):

def printMultiTable() {

  var i = 1

  // 這裏只有i在範圍內

  while (i <= 10) {

    var j = 1

    // 這裏ij在範圍內

    while (j <= 10) {

      val prod = (i * j).toString

      // 這裏ijprod在範圍內

      var k = prod.length

      // 這裏ijprodk在範圍內

      while (k < 4) {

        print(" ")

        k += 1

      }

      print(prod)

      j += 1

    }

    // ij仍在範圍內;prodk脫離範圍

    println()

    i += 1

  }

  // i仍在範圍內;jprodk脫離範圍

}

 

 

然而,你能夠在一個內部範圍內定義與外部範圍里名稱相同的變量:

  val a = 1; //在這裏要加上分號,由於此處不能推導

  {

    val a = 2

    println(a) //2

  }

  println(a) //1

Scala不一樣,Java不容許你在內部範圍內建立與外部範圍變量同名的變量。在Scala程序裏,內部變量隱藏掉同名的外部變量,所以在內部範圍內外部變量變得不可見

4       Scala方法和函數的區別

使用val(或var)語句能夠定義函數,def語句定義方法:

class T{

  def m(x: Int) = x + 3     //定義方法,m將是類T的成員方法

  val f = (x: Int) => x + 3 //定義函數, f將是類T的成員字段

}

函數類型形如(T1, ..., Tn) => T(注意與傳名參數類型區別:p: => Tp是參數名,當冒號後面無參,將無參空括號去掉就是傳名參數了;若是參數定義成p:() => T,則是參數是函數類型,而非傳名參數了),函數都實現了FuctionN(N[0..22])特質trait的對象,因此函數具備一些方法如equalstoString等,而方法不具備這些特性:

  def m(x: Int) = x + 3

  var f = (x: Int) => x + 3

  //  m.toString //編譯失敗

  f.toString //編譯經過

若是想把方法轉換成一個函數,能夠用方法名跟上下劃線的方式:

  def m(x: Int) = x + 3

  (m _).toString//編譯經過

 

一般在使用一個函數時是將賦值給函數變量或者是經過函數類型的參數傳遞給方法,函數的調用跟方法同樣,也是在函數對象(值)後面接小括號進行函數的調用,在Java是否是容許在對象後面接小括號的(只能在方法名後面接小括號),這正由於applyscala中的語法糖:能夠在一個對象obj後面帶上括號與參數(也可無參數),如obj(x,y)scala編譯器會將obj(x,y)轉換爲obj.apply(x,y)方法的調用;而在一個類clazz上調用clazz(),scala編譯器會轉換爲「類的伴生對象.apply()(通常是工廠方法)

 

函數的調用必須經過後面接上括號,不然表示函數對象自己;而方法的調用能夠就是方法名,而不須要接空括號

 

有兩種方法能夠將方法轉換成函數:

val f1 = m _

在方法名稱m後面緊跟一個空格和下劃線告訴編譯器將方法m轉換成函數。也能夠顯示地告訴編譯器須要將方法轉換成函數:

val f1: (Int) => Int = m

一般狀況下編譯器會自動將方法轉換成函數,例如在一個應該傳入函數參數(即參數類型爲函數)的地方傳入了一個方法,編譯器會自動將傳入的方法轉換成函數

 

將方法轉換爲函數的時候,若是方法有重載的狀況,必須指定參數和返回值的類型:

scala> object Tool{

     | def increment(n: Int): Int = n + 1

     | def increment(n: Int, step: Int): Int = n + step

     | }

scala> val fun = Tool.increment _

<console>:12: error: ambiguous reference to overloaded definition,

scala> val fun1 = Tool.increment _ : (Int => Int)

fun1: Int => Int = <function1>

scala> val fun2 = Tool.increment _ : ((Int, Int) => Int)

fun2: (Int, Int) => Int = <function2>

 

對於一個無參數的方法能夠省略掉空括號,而無參函數是帶空括號的:

scala> def x = println("Hi scala")//無參方法可省略掉空括號

x: Unit

scala> def x() = println("Hi scala")

x: ()Unit

scala> val y = x _

y: () => Unit = <function0> //無參函數類型是帶空括號的

scala> y()

Hi scala

scala> x

Hi scala

 

方法是支持參數默認值的用法,可是函數會忽略參數默認值的,因此函數不能省略參數:

scala> def exec(s: String, n: Int = 2) = s * n

exec: (s: String, n: Int)String

scala> exec("Hi") //第二個參數使用了默認值

res0: String = HiHi

scala> val fun = exec _

fun: (String, Int) => String = <function2>

scala> fun("Hi") //沒法使用默認值,不能省略參數

<console>:14: error: not enough arguments for method apply

scala> fun("Hi", 2) //必須設置全部參數

res2: String = HiHi

 

柯里化Currying函數能夠只傳入部分參數返回一個偏應用函數,而柯里化方法在轉換成偏應用函數時須要加上顯式說明,讓編譯器完成轉換:

object TestCurrying {

  def invoke(f: Int => Int => Int): Int = {//f是柯里化函數

    f(1)(2)

  }

  def multiply(x: Int)(y: Int): Int = x * y // multiply是柯里化方法

  def main(args: Array[String]) {

    invoke(multiply) //編譯器會自動將multiply方法轉換成函數

    // val partial1 = multiply(1) //multiply(1)至關於第二個方法的方法名,因此不能將方法賦值給變量

    val partial2 = multiply(1):(Int => Int) //編譯經過,且等效下面兩個

    val partial4 = multiply(1)_  // partial4的類型爲 Int=>Int

    val partial5: Int => Int = multiply(1)

    val f = multiply _ //multiply方法轉換成柯里化函數ff的類型爲 Int=>(Int=>Int)

    val partial3 = f(1) //只應用第1個參數返回函數,編譯經過, partial3的類型爲 Int=>Int

  }

}

5       函數和閉包

5.1               方法

定義函數最通用的方法是做爲某個對象的成員。這種函數被稱爲方法 method

  //公有方法

  def processFile(filename: String, width: Int) {

    val source = Source.fromFile(filename)

    for (line <- source.getLines)

      processLine(filename, width, line)

  }

  //私有方法

  private def processLine(filename: String, width: Int, line: String) {

    if (line.length > width) //打印輸長度超過給定寬度的行

      println(filename + ": " + line.trim)

  }

上面使用的是一般面向對象的編程方式

5.2               本地(內嵌、局部)函數

  def processFile(filename: String, width: Int) {

    def processLine(filename: String, width: Int, line: String) {//局部函數,只能在processFile方法(函數)中使用

      if (line.length > width) print(filename + ": " + line)

    }

    val source = Source.fromFile(filename)

    for (line <- source.getLines) {

      processLine(filename, width, line)

    }

  }

 

本地函數能夠直接訪問所在外層函數的參數,因此上面能夠改爲:

  def processFile(filename: String, width: Int) {

    def processLine(line: String) {

      if (line.length > width) print(filename + ": " + line)

    }

    val source = Source.fromFile(filename)

    for (line <- source.getLines)

      processLine(line)

  }

5.3               函數字面量

你能夠把函數寫在一個沒有名字函數字面量(匿名字面量函數),而且能夠把它當成一個值傳遞到其它函數或賦值給其它變量

 

下面的例子爲一個簡單的函數字面量

scala> (x: Int) => x + 1

res0: Int => Int = <function1> //res0函數變量

=>表示這個函數將符號左邊的東西(本例爲一個整數),轉換成符號右邊的東西(加 1),=>符號左邊是函數的參數,右邊是函數體

函數字面量會被編譯成類(實現了AbstractFunctionN抽象類的類),並在運行期實例化成函數值(即函數對象)。所以函數字面量和函數值的區別在於函數字面量存在於源代碼中,而函數值則做爲對象存在於運行期,這個區別很像類(源代碼)與對象(運行期)之間的關係

 

任何函數值都是scala包中的FunctionN特質(trait)的一個實例,如不帶參數的函數值是Function0特質的實例,帶一個參數的函數值是Function1特質的實例等等:

scala> var inc = (x: Int) => x + 1

inc: Int => Int = <function1>

scala> inc.isInstanceOf[Function1[Int,Int]]

res8: Boolean = true

 

每一個FunctionN特質都有一個apply方法,運行時實質上是由該方法來調用函數的

 

能夠將函數字面量賦給一個函數類型的變量,而且能夠參數變量調用:

scala> var increase = (x: Int) => x + 1
increase: (Int) => Int = <function1>     // 函數返回值爲Int函數體最後一條語句即返回值

scala> increase(10)
res0: Int = 11

因爲函數字面量在編譯時會被編譯成相應的類以及實例化成相應的函數值對象,下面經過類的方式來實現上面函數字面量「(x: Int) => x + 1」所能自動化實現的過程:

//自定義類名爲Increase函數類

class Increase extends Function1[Int, Int] {

  def apply(x: Int): Int = x + 2 //這裏試着加2以示區別,加幾並不重要

}

object T {

  //建立匿名函數實例對象,匿名函數能夠直接從Function1繼承,並實現apply方法

  var increase: Function1[Int, Int] = new Function1[Int, Int] {

    def apply(x: Int): Int = x + 1

  }

  def main(args: Array[String]): Unit = {

    println(increase(10)) //11變量後面可帶括號參數,是由於該對象定義了相應的apply方法,increase(10) 等價於 increase.apply(10)

    increase = new Increase()

    println(increase(10)) //12 

  }

}

 

函數體有多條語句時,使用大括號:

scala> increase = (x: Int) => {

println("We")
println(
"are")
println(
"here!")
x +
1 // 函數返回值爲Int函數體最後一條語句即返回值

}

increase: (Int) => Int = <function1>
scala> increase(10)
We
are
here!
res4: Int = 11

 

Iterable特質是 List Set Array,還有 Map 的共同超類,foreach 方法就定義在其中,它能夠用來針對集合中的每一個元素應用某個函數,即foreach方法參數容許傳入函數:

scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)
scala> someNumbers.foreach((x: Int) => println(x)) // 只是簡單的打印每一個元素
-11
-10
-5
0

5
10

 

集合中還有filter函數也是能夠傳遞一個函數的,用來過濾元素,傳遞進去的函數要求返回Boolean

scala> someNumbers.filter((x: Int) => x > 0)
res6: List[Int] = List(5, 10)

5.4               簡化函數字面量

去除參數類型,以及外面的括號:

someNumbers.filter((x: Int) => x > 0)
因數函數是應用於集合中元素,因此會根據集合元素類型來推導參數類型。

5.5               佔位符_

下劃線「_」來替代一個或多個參數,只要某個參數只在函數體裏出現一次,則能夠使用下劃線 _ 來替換這個參數:

scala> someNumbers.filter(_ > 0)

res9: List[Int] = List(5, 10)

_ > 0 至關於x => x > 0,遍歷時會使用當前相應元素來替換下劃線(你能夠這樣來理解,就像咱們之前作過的填空題,「_」爲要填的空,Scala 來完成這個填空題,你來定義填空題)

 

有多少個下劃線,則就表示有多少個不一樣的參數。多個佔位符時,第一個下劃線表示第一個參數,第二個下劃線表示第二個參數,以此類推;因此同一參數多處出現時是沒法使用這種佔位符來表示的。

 

使用佔位符時,有時沒法推導出類型,如:

scala> val f = _ + _

此時需明確寫出類型:

scala> val f = (_: Int) + (_: Int)

f: (Int, Int) => Int = <function2>

 

scala> def sum = (_:Int) + (_:Int) + (_:Int) //注:這裏的下劃線不是偏應用,它是函數字面量的佔位符簡化,該函數字面量爲sum方法體最後一個語句,因此該方法返回值類型爲函數

sum: (Int, Int, Int) => Int  // 方法由三部分組成方法名(這裏爲sum+ 參數列表(這裏沒有,也不帶空括號)+ 返回類型(這裏爲函數值類型(Int, Int, Int) => Int

scala> sum //調用無參無空括號方法。因爲參數爲空,定義時去掉了空括號,因此調用時不能帶空括號

res0: (Int, Int, Int) => Int =<function3> //返回的是函數字面量

scala> sum (1,2,3) //因爲sum方法定義成了無參無空括號的方法,因此單獨的語句  sum 就表示對sum方法進行了一次調用,而sum後面的(1,2,3)則是對函數值進行再一次調用

res1: Int = 6

5.6               偏應用函數

偏應用函數(Partial Applied Function)是指在調用函數時,有意缺乏部分參數的函數。

 

前面的例子下劃線 _ 代替的只是一個參數,實際上你還能夠用「_」來代替整個參數列表(有多少個參數,則表明多少個參數),如println(_) ,或更簡潔println _ 或乾脆println

someNumbers.foreach(println _)

Scala 編譯器自動將上面代碼解釋成:

someNumbers.foreach(x => println(x))

(注:下面的 println _ 卻又是返回的是無參數的偏應用函數,Why?由於println函數自己就有不帶參數的形式,又因爲這裏沒有信息指引f函數變量要帶參數,因此 _」就優先表明了0參數列表,因此f函數變量最終是一個無參數的函數變量。而上面的List.foreach(println _)中,因爲foreach方法要求是帶一個參數的函數,因此此時的「_」就去匹配一個參數列表的println函數

scala> val f = println _

f: () => Unit = <function0>

若是要帶參數,這樣才能夠帶一個參數:

scala>  val f = (x:String)=>println(x)

f: String => Unit = <function1>

 

這個例子的下劃線不是單個參數的佔位符,而是整個參數列表的佔位符(雖然示例中是隻帶有一個參數的println函數)

因爲someNumbers.foreach方法要求傳入的就是函數,因此此時下劃線也能夠直接省略,更簡潔的寫法:

someNumbers.foreach(println)

注:只能在須要傳入函數的地方去掉下劃線,其餘地方不能,如後面的sum函數:

scala> val c = sum
<console>:12: error: missing argument list for method sum

Unapplied methods are only converted to functions when a function type is expected.

You can make this conversion explicit by writing `sum _` or `sum(_,_,_)` instead of `sum`.

       val c = sum

               ^

 

以上在調用方法時,使用下劃線「_」來代替方法參數列表(而不是傳入具體參數值),這時你就是正在寫一個偏應用函數Partially applied functions

 

Scala 中,當你調用函數傳入所需參數時,你就把函數「應用」到參數,好比一個加法函數:

scala> def sum(a: Int, b: Int, c: Int) = a + b + c
sum: (Int,Int,Int)Int
你就能夠把函數 sum 應用到參數 1 2 3 上,以下:
scala> sum(1, 2, 3)
res12: Int = 6

 

一個偏應用函數指的是你在調用函數時,不指定函數所需的全部參數(或只提供部分,或不提供任何參數),這樣你就建立了一個新的函數,這個新的函數就稱爲原始函數的偏應用函數,如:

scala> val a = sum _ // sum方法轉換爲偏應用函數後賦值給名爲a的函數變量
a: (Int, Int, Int) => Int = <function3>

scala> a(1, 2, 3)
res13: Int = 6

scala> var b = sum(1,2,3); //若是在定義時就傳入了具體值,則返回的就是具體的值了,此時b變量是Int變量,而非函數變量

b: Int = 6

上面的過程是這樣的:名爲a的變量指向了一個函數值對象,這個函數值是由Scala編譯器依照偏應用函數表達式sum _ 自動產生的類的一個實例。且這個類有一個帶3個參數的apply方法,編譯器會將a(1,2,3) 表達式翻譯成對函數值的apply方法的調用。所以a(1, 2, 3)實質爲:

scala> a.apply(1, 2, 3)
res14: Int = 6

 

這種由下劃線代替整個參數列表的一個重要的用途就是:能夠將def定義的方法轉換爲偏應用函數儘管不能直接將def定義的方法或嵌套函數賦值給函數變量,或當作參數傳遞給其它的方法,可是若是把方法或嵌套函數經過在名稱後面加一個下劃線的方式轉換爲函數後,就能夠作到了

偏應用函數還能夠部分指定參數,如:

scala> val b = sum(1, _: Int, 3) //變量 b 的類型爲函數,是由 sum方法應用了第一個和第三個參數後構成的
b: (Int) => Int = <function1>

只缺乏中間一個參數,因此編譯器會產生一個新的函數類,其 apply 方法帶一個參數,因此調用b函數變量時只能傳入一個:

scala> b(2)
res15: Int = 6

此時,b(2)實質上是對函數值的apply方法調用,即b.apply(2),而b.apply(2)再去調用sum(1,2,3)

5.7               閉包

函數字面量在運行時建立的函數值(對象)稱爲閉包

 

scala> var more = 1
more: Int = 1
scala> val addMore = (x: Int) => x + more // 函數值賦值給addMorey,該函數值就是一個閉包
addMore: (Int) => Int = <function1>
scala> addMore(10)
res19: Int = 11

 

在閉包建立以後,閉包以外的變量more修改後,閉包中的引用也會根着變化,所以 Scala 的閉包捕獲的是變量自己而不是當時變量的值:

scala> more = 9999
more: Int = 9999
scala> addMore(10)
res21: Int = 10009

 

上面是閉包以外的變量修改會影響閉包中相應變量,一樣,在閉包中修改閉包外的變量,則閉包外的變量也會跟着變化

scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)
scala> var sum = 0
sum: Int = 0
scala> someNumbers.foreach(sum += _) //在閉包中修改了閉包外的變量,外部變量也會跟着變化
scala> sum
res23: Int = -11

示例中的someNumbers.foreach(sum += _)語句中的 sum += _ 就是一個函數字面量,至關於 x => sum += x,具體參考前面的函數字面量佔位符

 

 

scala> var increase = (x: Int) => x + 1
increase: (Int) => Int = <function1>// 變量由兩部分組成變量名(這裏爲increase+ 類型(這裏爲函數值類型 (Int) => Int = <function1>

 

scala> def makeIncreaser(more: Int) = (x: Int) => x + more  //這裏的more 至關於閉包參數,要在調用時才能肯定

makeIncreaser: (more: Int) Int => Int //方法由三部分組成方法名(這裏爲makeIncreaser+ 參數列表(這裏爲(more: Int)+ 返回類型(這裏爲函數值類型 Int => Int

scala> val inc1 = makeIncreaser(1) //調用時肯定閉包參數more1且返回函數值,並賦給inc1函數變量
inc1: Int => Int = <function1>
scala> val inc9999 = makeIncreaser(9999)
inc9999: (Int) => Int = <function1>

上面每次makeIncreaser函數調用時都會產生一個閉包,且每一個閉包都會有本身的more變量(即調用時傳入的值)。

下面纔開始真正調用函數字面量,且各自有本身的閉包參數more

scala> inc1(10)   //閉包參數more值爲1
res24: Int = 11
scala> inc9999(10) //閉包參數more值爲9999
res25: Int = 10009

5.8               可變參數

若是參數列表後面的參數類型都同樣,能夠使用*來表明參數列表,下面表明0個或多個String類型的參數,參數會存放到String類型的args數組中,即args類型爲Array[String]

scala> def echo(args: String*) =

for (arg <- args) println(arg)

echo: (String*)Unit

 

scala> echo()
scala> echo(
"one")
one
scala> echo("hello", "world!")
hello
world!

在函數內部,變長參數的類型,實際爲一數組,好比上例的 String * 類型實際爲 Array[String],然而,現在你試圖直接傳入一個數組類型的參數給這個參數,編譯器會報錯:

scala> val arr = Array("What's", "up", "doc?")
arr: Array[java.lang.String] = Array(What's, up, doc?)
scala> echo(arr)

<console>:7: error: type mismatch;

found : Array[java.lang.String]
required: String
echo(arr)

ˆ

但你能夠經過在變量後面添加一個冒號 : 和一個 _* 符號,這個符號告訴 Scala 編譯器在傳遞參數時逐個傳入數組的每一個元素,而不是數組總體:

scala> echo(arr: _*)
What's
up
doc?

 

注:可變參數只能是參數列表中的最後一個

5.9               命名參數

一般狀況下,調用函數時,參數傳入和函數定義時參數列表一一對應。

scala> def speed(distance: Float, time:Float) :Float = distance/time

speed: (distance: Float, time: Float)Float

scala> speed(100,10)

res0: Float = 10.0

使用命名參數容許你使用任意順序傳入參數,好比下面的調用:

scala> speed( time=10,distance=100)

res1: Float = 10.0

scala> speed(distance=100,time=10)

res2: Float = 10.0

5.10         缺省參數值

Scala 在定義函數時,容許指定參數的缺省值,從而容許在調用函數時不傳該參數,此時該參數使用缺省值。缺省參數一般配合命名參數使用,例如:

scala> def printTime(out:java.io.PrintStream = Console.out, divisor:Int =1 ) =

       out.println("time = " + System.currentTimeMillis()/divisor)

printTime: (out: java.io.PrintStream, divisor: Int)Unit

scala> printTime()

time = 1383220409463

scala> printTime(divisor=1000)

time = 1383220422

5.11         尾(僞)遞歸

能夠使用遞歸函數來消除須要使用 var 變量的 while 循環

 

def approximate(guess: Double): Double =

if (isGoodEnough(guess)) guess //該數已經足夠好了,直接返回結果
else approximate(improve(guess)) //還不是最好,需繼續改進

像上面,結尾是調用本身,這樣的遞歸爲尾遞歸

因爲遞歸會產生堆棧調用而影響性能,因此你可能將遞歸修改成傳遞的While結構,如將上面的代碼改進以下:

  def approximateLoop(initialGuess: Double): Double = {

    var guess = initialGuess

    while (!isGoodEnough(guess))

      guess = improve(guess)

    guess

  }

那麼這兩種實現哪種更可取呢? 從簡潔度和避免使用 var 變量上看,使用遞歸比較好。但依照之前經驗遞歸比while循環慢,但實際上,經測試這兩種方法所需時間幾乎相同,Why

其實,對於 approximate 的遞歸實現,Scala 編譯器會作些優化,由於這裏 approximate 的實現,最後一行是調用 approximate 自己,咱們把這種遞歸叫作尾遞歸Scala 編譯器檢測到尾遞歸時會自動使用循環來代替,所以,你應該仍是多使用遞歸函數來解決問題,若是是尾遞歸,那麼在效率上是不會有什麼損失的

 

尾遞歸函數在每次調用不會構造一個新的調用棧。全部遞歸其實都在同一個執行棧中運行,而是Scala會使用While結構來優化這種遞歸

 

以下面的調用不是尾遞歸調用,由於最後一句雖然調用了本身,但在調用本身後,還進了增1操做:

scala> def boom(x: Int): Int ={

if (x == 0) throw new Exception("boom!")
else boom(x - 1) + 1}

scala> boom(3)
java.lang.Exception: boom!
at .
boom(<console>:5)
at .
boom(<console>:6)
at .
boom(<console>:6)
at .
boom(<console>:6)
at .<init>(<console>:6)
...

從上面調用堆棧來看,boom函數是真正遞歸調用了屢次(boom函數被調用了屢次),因此不是尾遞歸。將上面的加一去掉後,纔是尾遞歸調用,測試以下:

scala> def bang(x: Int): Int ={

if (x == 0) throw new Exception("bang!")
else bang(x - 1)}

scala> bang(5)
java.lang.Exception: bang!
at .
bang(<console>:5)
at .<init>(<console>:6)
...

從上面能夠看出,函數bang只被調用了一次,即沒有被遞歸調用,因此是尾遞歸

 

注:尾遞歸只在函數體最後一句直接調用函數自己,才能造成尾遞歸,其它任何狀況下的間接調用則不會造成尾遞歸,以下面的間接調用不會造成尾遞歸:

  def isEven(x: Int): Boolean = if (x == 0) true else isOdd(x - 1

  def isOdd(x: Int): Boolean =  if (x == 0) false else isEven(x - 1)

雖然isEven isOdd 都是在最後一句調用,它們是兩個互相遞歸的函數,scala 編譯器沒法對這種遞歸進行優化,另外下面也不會造成尾遞歸:

  val funValue = nestedFun _ //使用偏應用表達式將方法轉換爲函數值

  def nestedFun(x: Int) {

    if (x != 0) { println(x); funValue(x - 1) }

  }

6       控制抽象

Scala 沒有內置不少控制結構,這是由於 Scala 賦予了程序員本身經過函數擴展控制結構的能力

6.1               函數封裝變化

若是方法中的某段邏輯是變化的,能夠將這段邏輯封裝在一個函數裏,而後經過方法參數將該函數值傳進去,這樣就能夠將方法中變化的邏輯剝離出來(使用Java中的接口也能夠將變化封裝起來)

 

好比下面實現一個過濾文件的方法,但過濾的算法是各類各樣的,因此將過濾算法封裝在函數裏,而後在具體過濾時經過matcher函數類型參數傳遞過去:

object FileMatcher {

  private def filesHere = (new java.io.File(".")).listFiles

//因爲匹配的邏輯是變化的,因此將匹配的邏輯封裝在函數裏經過matcher參數傳遞進來,matcher參數類型中有=>,表示函數,該函數接收兩個String類型參數,且返回布爾類型值

  def filesMatching(query: String, matcher: (String, String) => Boolean) = {//此時的matcher函數帶有兩個參數

    for (file <- filesHere; if matcher(file.getName, query)) //過濾出只須要的文件,但怎麼過濾經過matcher傳遞進來

      yield file

  }

 

//而後這樣使用:

  def filesEnding(query: String) = filesMatching(query, _.endsWith(_))  //返回以query結尾的文件名

  def filesContaining(query: String) = filesMatching(query, _.contains(_))//返回包含了query的文件名

  def filesRegex(query: String) = filesMatching(query, _.matches(_)) //返回匹配query的文件名

}

這些調用用到了函數字面量佔位符號法:

_.endsWith(_)至關於 (fileName: String, query: String) => fileName.endsWith(query) 甚至能夠省略參數的類型:(fileName, query) => fileName.endsWith(query)

  def filesEnding(query: String) = filesMatching(query, (fileName: String, query: String) => fileName.endsWith(query))

因爲第一個參數fileName在函數字面量體中第一個位置被使用,第二個參數query在第二個位置中使用,因此你能夠使用佔位符語法來簡化:_.endsWith(_),因此出現上面簡潔寫法

 

上面示例中 query傳遞給了 filesMatching,但 filesMatching方法中並無直接使用它,而又是直接把它傳給了matcher 函數,因此這個傳來傳去的過程不是必需的,所以能夠將filesMatching方法 和 matcher 函數中的參數 query 去掉,而是在函數字面量體中直接使用閉包參數query(正是由於閉包才能夠省去query參數的傳遞)

object FileMatcher2 {

  private def filesHere = (new java.io.File(".")).listFiles

  def filesMatching(matcher: String => Boolean) = {//此時的matcher函數只有一個參數

    for (file <- filesHere; if matcher(file.getName))

      yield file

  }

 

  def filesEnding(query: String) = filesMatching((fileName) => fileName.endsWith(query)) // 直接使用閉包參數query

  def filesContaining(query: String) = filesMatching(_.contains(query))

  def filesRegex(query: String) = filesMatching(_.matches(query))

}

 

下面咱們再來看看Scala類庫對變化封裝的示例:

傳統判斷集合中是否存在負數的方式:

  def containsNeg(nums: List[Int]): Boolean = {

    var exists = false

    for (num <- nums)

      if (num < 0)

        exists = true

    exists

  }

Scala集合類中的exists方法對是否存在這一變化的邏輯進行封裝,只需傳遞判斷的邏輯(即函數)便可,因此能夠這樣:

  def containsNeg(nums: List[Int]) = nums.exists(_ < 0)

 

exists方法表明了控制抽象,實際上是Scala將上面傳統的代碼替咱們進行了封裝(如循環相關的代碼),咱們只需傳入變化的邏輯便可,下面是集合的exists方法源碼:

  def exists(p: A => Boolean): Boolean = {

    var these = this

    while (!these.isEmpty) {

      if (p(these.head))

return true

      these = these.tail

    }

    false

  }

 

好比判斷是否存在偶數,只需轉入具體的判斷邏輯:

  def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1)

6.2               柯里化currying

Scala 容許程序員本身新建立一些控制結構,而且能夠使得這些控制結構在語法看起來和 Scala 內置的控制結構同樣,在 Scala 中須要藉助於柯里化(Currying)

 

柯里化將方法或函數是將一個帶有多個參數的列表拆分紅多個小的參數列表(一個或多個參數)的過程,而且將參數應用前面參數列表時會返回新的函數技術

 

scala> def plainOldSum(x: Int, y: Int) = x + y
plainOldSum: (x: Int, y: Int)Int
scala> plainOldSum(1, 2)
res4: Int = 3

plainOldSum寫成柯里化的curriedSum,前面函數使用一個參數列表,「柯里化」把函數定義爲多個參數列表(且第一個參數列表只有一個參數,剩餘的參數放在第二個參數列表中):

scala> def curriedSum(x: Int)(y: Int) = x + y //柯里化方法
curriedSum: (x: Int)(y: Int)Int
scala> curriedSum(1)(2)
res5: Int = 3

當你調用 curriedSum (1)(2) 時,其實是依次調用兩個普通函數(非柯里化函數),第一次調用使用一個參數 x,返回一個函數值,第二次使用參數y調用這個函數值。下面咱們來用兩個分開的定義來模擬 curriedSum 柯里化函數的過程:

scala> def first(x: Int) = (y: Int) => x + y
first: (x: Int)Int => Int // first方法返回的是函數值(對象)x是既是方法參數,又是函數閉包參數

調用first方法會返回函數值,即產生第二個函數:

scala> val second = first(1) //產生第二個函數
second: (Int) => Int = <function1> //second爲函數變量,引用某個函數值

scala> second(2) //調用second函數產生最終結果
res6: Int = 3

 

上面firstsecond的定義演示了柯里化函數的調用過程,它們自己和 curriedSum 沒有任何關係,可是能夠引用到第二個函數second,以下

scala> val second = curriedSum(1)_ //curriedSum(1) 至關於第二個方法的方法名。在前面示例中,當佔位符標註用在傳統方法上時,如 println _,你必須在名稱和下劃線之間留一個空格。在這裏不須要,由於 println_ Scala 裏合法的標識符, curriedSum(1)_不是
second: (Int) => Int = <function1>

scala> onePlus(2)
res7: Int = 3

注意與下面的區別:

scala> val func = curriedSum _ //這裏是將整個curriedSum方法轉換爲函數,該函數帶兩個參數,而前面只是將方法curriedSum的一部分(第二個參數列表)轉換爲函數,因此上面只帶一個參數

func: Int => (Int => Int) = <function1>

 

再看一個柯里化的例子,把帶有三個參數的函數f轉換爲只有一個參數的部分應用函數f

scala> def curry[A, B, C, D](f: (A, B, C) => D): A => (B => (C => D)) = (a: A) => (b: B) => (c: C) => f(a, b, c)//柯里化函數

curry: [A, B, C, D](f: (A, B, C) => D)A => (B => (C => D))

scala> val f = curry((_: Int) + (_: Int) + (_: Int))

f: Int => (Int => (Int => Int)) = <function1> //將帶有三個參數的函數柯里化成3個單一參數的函數

scala> f(1)

res4: Int => (Int => Int) = <function1>

scala> f(1)(2)

res5: Int => Int = <function1>

scala> f(1)(2)(3)

res6: Int = 6

下面與上面不一樣的是,把帶有三個參數的函數f轉換爲第一個是單個參數,第二個包括全部餘下參數的部分應用函數f

scala> def curry2[A, B, C, D](f: (A, B, C) => D): A => ((B, C) => D) = (a: A) => (b: B, c: C) => f(a, b, c)

curry2: [A, B, C, D](f: (A, B, C) => D)A => ((B, C) => D)

scala> val f2 = curry2((_: Int) + (_: Int) + (_: Int))

f2: Int => ((Int, Int) => Int) = <function1>

scala> f2(1)

res9: (Int, Int) => Int = <function2>

scala> f2(1)(2,3)//第二個參數列表帶兩個參數

res10: Int = 6

甚至轉換第一個參數列表帶兩個參數,第二個參數列表只帶一個參數的函數,這也是能夠的:

scala> def curry3[A, B, C, D](f: (A, B, C) => D): (A,B) => C => D = (a: A,b:B) => (c:C) => f(a, b, c)

curry3: [A, B, C, D](f: (A, B, C) => D)(A, B) => C => D

scala> val f3 = curry3((_: Int) + (_: Int) + (_: Int))

f3: (Int, Int) => Int => Int = <function2>

scala> f3(1,2) //第一個參數列表帶兩個參數

res12: Int => Int = <function1>

scala> f3(1,2)(3)

res13: Int = 6

上面是柯里化,下面進行反柯里化,將多個參數列表合併成一個參數列表

scala> def uncurry[A, B, C](f: A => B => C): (A, B) => C = (a: A, b: B) => f(a)(b)

uncurry: [A, B, C](f: A => (B => C))(A, B) => C

scala> val uf = uncurry((a:Int)=>(b:Int)=>a + b)//反柯里化

uf: (Int, Int) => Int = <function2>

scala> uf(1,2)

res14: Int = 3

 

下面是接收兩個參數的方法,進行部分應用。即咱們有一個A和一個須要AB產生C的函數,能夠獲得一個只須要B就能夠產生C的函數(由於咱們已經有A)

scala> def curry1[A, B, C](a: A, f: (A, B) => C): B => C = (b: B) => f(a, b)//也可將(b: B) => f(a, b)寫成f(a,_)

curry1: [A, B, C](a: A, f: (A, B) => C)B => C

//a參數會應用到f函數參數的第一個A類型的參數中,這樣會返回只應用了第一個A類型參數的f1的偏應用函數

scala> val f1 = curry1(1,(_:Int) +(_:Int))//f1實爲f函數的一個偏應用函數

f1: Int => Int = <function1>

scala> f1(2)

res1: Int = 3

若是將上面curry1方法中的f函數參數具體化,即在將f函數代碼直接在curry1方法中寫出來,而不是經過方法參數傳遞進去,下面示例是上面的具體化,函數代碼直接在方法體中描述出來,而非參數傳遞進來:

scala> def makeIncreaser(more: Int) = (x: Int) => x + more

makeIncreaser: (more: Int) Int => Int

scala> val inc1 = makeIncreaser(1)

inc1: Int => Int = <function1>

scala> inc1(10)

res24: Int = 11

6.3                編寫新的控制結構

將不要用戶關心的邏輯封裝起來,好比資源的打開與關閉:

  def withPrintWriter(file: File, op: PrintWriter => Unit) {

    val writer = new PrintWriter(file)

    try {

      op(writer)

    } finally {

      writer.close()

    }

  }

withPrintWriter方法只提供兩個參數:一個是對哪一個文件進行操做,二是對文件進行一個什麼樣的操做(寫仍是讀?),除此以外如打開與關閉文件則封裝起。

以下使用,對date.txt 文件進行寫println操做,具體寫什麼則在函數裏指定(這裏寫當前日期):

withPrintWriter(

new File("date.txt"),

w => w.println(new java.util.Date)

)

這樣當調用withPrintWriter方法操做文件後,文件必定會關閉

 

Scala裏,若是調用的方法只有一個參數,就能可選地使用大括號替代小括號包圍參數:

scala> println("Hello, world!")
Hello, world!
你能夠寫成:

scala> println { "Hello, world!" }
Hello, world!

上面第二種用法,使用{}替代了(),但這隻適用在使用一個參數的調用狀況。 前面定義 withPrintWriter 函數使用了兩個參數,所以不能使用{}來替代(),但若是咱們使用柯里化從新定義下這個函數以下:

  def withPrintWriter(file: File)(op: PrintWriter => Unit) {

    val writer = new PrintWriter(file)

    try {

      op(writer)

    } finally {

      writer.close()

    }

  }

將一個參數列表,變成兩個參數列表,每一個列表含一個參數,這樣咱們就能夠使用以下語法來調用:

withPrintWriter(new File("date.txt")) {

  writer => writer.println(new java.util.Date);

  //上面的語句還能夠簡寫以下:

  //_.println(new java.util.Date)

}

第一個參數仍是使用()將參數包圍起來(也能夠使用{}),第二個參數放在了花括號之間,這樣withPrintWriter看起來和Scala內置控制結構(如ifwhile等)語法同樣

6.4                傳名參數by-name parameter

上篇咱們使用柯里化函數定義一個控制機構 withPrintWriter,它使用時語法調用已經很像 Scala 內置的控制結構,有如ifwhile的使用通常:

withPrintWriter(new File("date.txt")) {

writer => writer.println(new java.util.Date)

}

不過仔細看一看這段代碼,它和 scala 內置的 if while 表達式仍是有些區別的,withPrintWrite r{}中的函數是帶參數的含有「writer=>」。 若是你想讓它徹底和 if while 的語法一致,在 Scala 中能夠使用傳名參數來解決這個問題。

 

Scala的解釋器在解析函數參數(function arguments)時有兩種方式:先計算參數表達式的值(reduce the arguments),再傳遞到函數內部;或者是將未計算的參數表達式直接應用到函數內部。前者叫作傳值調用call-by-value,後者叫作傳名調用call-by-name

  def addByName(a: Int, b: => Int) = a + b //傳名

  def addByValue(a: Int, b: Int) = a + b //傳值

使用傳名調用時,在參數名稱和參數類型中間有一個「=>」符號。若是a = 1,b = 2 + 3,調用個方法的結果都是 6,但過程是不同的:

·  addByName(1, 2 + 3)

·  ->1 + (2 + 3)

·  ->1 + 5

·  ->6

·  addByValue(1, 2 + 3)

·  ->addByValue(1, 5)

·  ->1 + 5

·  ->6

 

只有無參函數才能經過傳名參數進行傳遞,在傳入其它方法前,是不會執行的,而是將傳入的函數邏輯代碼直接嵌入(替換)到傳名參數所在的地方(有點Include的意思)

 

下面設計一個myAssert斷言方法,帶一個函數值參數predicate,若是標誌位assertionsEnabled被設置true(表示開啓斷言功能),且傳入的函數返回true時,將什麼也不作(即斷言成功),若是傳入的函數返回false時,則斷言失敗;若是標誌位assertionsEnabled被設置false(表示關閉斷言功能),則什麼也不作:

scala>var assertionsEnabled = true

def myAssert(predicate: () => Boolean) =

if (assertionsEnabled && !predicate())

throw new AssertionError

myAssert: (predicate: () => Boolean)Unit //空括號表示predicate函數不帶參數

scala> myAssert(() => 5 > 3) // 斷言成功 ,這裏是傳值,傳遞的是函數值

調用myAssert時或許你想去掉空參數列表和=>符號 ()=>,寫成以下形式,但報錯:

scala> myAssert(5 > 3)  //報錯 ,但傳名參數能夠實現這種想法

<console>:15: error: type mismatch;

 found   : Boolean(true)

 required: () => Boolean

       myAssert(5 > 3)

                  ^

上面使用的是按值傳遞(在傳入到方法就已執行並計算出結果——該結果是無參函數字面量「() => 5 > 3」函數值對象),傳遞的是函數類型的值,咱們能夠把按值傳遞參數修改成按名稱傳遞的參數。要實現一個傳名參數,參數類型應該以 => 開頭,而不是 ()=> 開頭,如上面你能夠將predicate參數的類型從「() => Boolean」改成「=> Boolean」,通過這種改造後,myAssert方法中的 predicate 參數則叫傳名參數

scala>def byNameAssert(predicate: => Boolean) =    //去掉了=>前面的括號()

if (assertionsEnabled && !predicate// predicate是名稱參數,會將predicate替換成傳入的函數邏輯代碼。這裏的predicate不是函數值對象,由於若是是函數值對象,調用時後面必定要接括號的,因此predicate在這裏至關於一個佔位符,將會使用傳入的函數代碼來替換

throw new AssertionError

byNameAssert: (predicate: => Boolean)Unit

此時調用byNameAssert方法時就能夠省略空的參數() =>了,此時使用byNameAssert看上去好像在使用內建控制結構了:

scala> byNameAssert(5 > 3) // 斷言成功。 另外一實例參考這裏的傳名參數

注:此時不會將 5 > 3 先進行計算而後再傳入byNameAssert方法,若是這樣的話,傳入的是Boolean類型,就不是函數值類型

 

上面的myAssertbyNameAssert兩個方法只是寫法上不同,均可以正確斷言。其實二者有着本質的區別,myAssert傳值參數,byNameAssert傳名參數。

 

或許你可能想將參數的類型從函數值類型直接定義爲Boolean,下面的方法boolAssert雖然看上去與byNameAssert類似,但在某些狀況下是不能正確實現斷言功能 :

scala>def boolAssert(predicate: Boolean) =

if (assertionsEnabled && !predicate)

throw new AssertionError

此時下面的斷言是能夠成功的:

scala> byNameAssert(5 > 3) // 斷言成功

但在斷言標誌assertionsEnabled設爲false關閉斷言時,針對「1 / 0 == 0」這樣的斷言就會拋異常(除0了):

scala> boolAssert(1 / 0 == 0)

java.lang.ArithmeticException: / by zero

byNameAssert將不會拋異常:

scala> byNameAssert(1 / 0 == 0)

緣由就是boolAssert方法中的參數類型直接是Boolean類型,則傳入的「1 / 0 == 0」會先進行運行,此時 1 / 0 就會拋異常;而 byNameAssert(1 / 0 == 0),表達式 「1 / 0 == 0」 不會被事先計算好傳遞給 byNameAssert,而是先將 「1 / 0 == 0」建立成一個函數類型的參數值,而後將這個函數類型的值做爲參數傳給 byNameAssert ,實質上「1 / 0 == 0」是最後由這個函數的 apply 方法去調用,但此時的assertionsEnabled標誌爲false造成短路,因此最終沒能執行,因此也不會拋異常

 

前面傳名參數傳遞的都是函數邏輯代碼,實質上傳名參數能夠接受任何邏輯代碼塊,只要代碼塊類型與傳名參數類型相同:

  def time(): Long = {

    println("獲取時間")

    System.nanoTime()

  }

 

  def delayed(t: => Long): Long = {

    println("進入delayed方法")

    println("參數t=" + t)

    t

  }

 

  def main(args: Array[String]) {

    //還能夠直接傳遞方法調用,實質上會使用這裏 time() 代替delayed方法體內的 t 名稱參數

    delayed(time())

    //因爲time是無參方法,因此調用時也可能省略括號

    //delayed(time)

    println("-------------------")

    delayed({ println("傳名參數可接受任何邏輯代碼塊"); 1 })

  }

 

前面的 withPrintWriter 咱們暫時無法使用傳名參數去掉參數裏的 writer=>,由於傳進去的op函數參數是須要參數的(即須要對哪一個目標文件進行操做),不過咱們能夠看看下面的例子,設計一個 withHelloWorld 控制結構,即這個 withHelloWorld 總會打印一個「hello,world」:

  import scala.io._

  import java.io._

  //op這個函數是不需參數的,因此能夠設計成按名傳遞

  def withHelloWorld(op: => Unit) {

    op  // op是名稱參數,會將op替換成傳入的函數邏輯代碼

    println("Hello,world")

  }

調用一:

    val file = new File("date.txt")

    withHelloWorld { //調用時,會將上面方法體內的op傳名參數所在地方,使用這對花括號中的邏輯代碼塊替換掉

      val writer = new PrintWriter(file)

      try {

        writer.println(new java.util.Date)

      } finally {

        writer.close()

      }

    }

Hello,world

調用二:

    withHelloWorld {

      println("Hello,Guidebee")

    }

Hello,Guidebee

Hello,world

能夠看到 withHelloWorld 的調用語法和 Scala 內置控制結構很是象了

 

總結,傳名參數的做用就是:給方法傳遞什麼樣的代碼,那就會使用什麼樣的代碼替換掉方法體內的傳名參數

7       組合與繼承

7.1                      抽象類

abstract class Element {

  def contents: Array[String]

}

 

一個含有抽象方法的類必須定義成抽象類,也就是說要使用abstract關鍵字來定義類

 

抽象類中不必定有抽像方法,但抽象方法所在的類必定是抽象類

 

abstract抽象類的不能實例化

 

contents 方法自己沒有使用 abstract 修飾符,一個沒有定義實現的方法就是抽象方法,和 Java 不一樣的是,抽象方法不須要使用 abstract 修飾符來表示,只要這個方法沒有具體實現,就是抽象方法

 

聲明: declaration

定義: definition

Element 聲明了抽象方法contents,但當前沒有定義具體方法

 

7.2                      無參方法

abstract class Element {

  def contents: Array[String] //抽象方法

  def height: Int = contents.length //無參方法,不帶參數也不帶括號

  def width(): Int = if (height == 0) 0 else contents(0).length //空括號方法,不帶參數但帶括號

}

注:若是定義時去掉了空括號,則在調用時也只能去掉;若是定義時帶上了空括號,則調用時能夠省略,也能夠不省略:假設eElement實例,調用上面的height只能夠是這樣:e.height,而不能是e.height();但調用width方法時,便可以是e.width,也能夠是e.width()

 

通常若是方法沒有反作用(即只是讀取對象狀態,而不會修改對象的狀態,也不會去調用其它類或方法)狀況下,推薦使用這種無參方法來定義方法,由於這樣訪問一個方法時好像在訪問其字段成員同樣,這樣訪問代碼作到了統一訪問原則,也就是說heightwidth無論定義成字段仍是方法(定義時省略空括號),客戶端訪問代碼均可以不用變,由於此時訪問的heightwidth方式都同樣

 

不帶括號的無參方法很容易變成屬性字段,只需將def改成val便可:

abstract class Element {

  def contents: Array[String] //抽象方法

  val height = contents.length

  val width = if (height == 0) 0 else contents(0).length

}

 

訪問字段要比訪問方法略快,由於字段在類的初始化時就已經計算過了,而方法則在每次調用時都要計算

 

因爲Scala 代碼能夠直接調用 Java 函數和類,但 Java 沒有使用「統一訪問原則」,如在Java 裏只能是 string.length(),而不能是 string.length。爲了解決這個問題,Scala 對於Java裏的空括號函數的使用也是同樣,也能夠省略這些空括號:

Array(1, 2, 3).toString //實際上調用的是JavaObject中的toString()方法
"abc".length //實爲調用的JavaStringlength()方法

//之前這種在Java中常規調用法在Scala中也仍是能夠的

Array(1, 2, 3).toString()

"abc".length()

 

原則上,Scala的函數調用中能夠省略全部的空括號,但若是使用的函數不是純函數,也就是說這個不帶參數的函數可能修改對象的狀態或是咱們利用它調用了其餘一些功能(好比調用其它類打印到屏幕,讀寫 I/o),通常的建議仍是使用空括號:

"hello".length // 沒有反作用,因此無須(),由於String是不可變類
println() // I/O操做,最好別省略()

 

總之,Scala推薦使用將不帶參數且沒有反作用的方法定義爲無參方法,即省略空括號,但永遠不要定義沒有括號但帶反作用的方法,由於那樣的話方法調用看上去像是訪問的字段,會讓調用都感受到訪問屬性還產生了其餘做用呢?另外,從調用角度從發(前面是從定義角度出法),若是你的調用方法執行了其餘操做就要帶上括號,但若是方法僅僅是對某個屬性字段的訪問,則能夠省略

7.3                      extends

class ArrayElement(conts: Array[String]) extends Element {

  def contents: Array[String] = conts

}

 

extends會將全部非私有的成員會繼承過來

 

若是你在定義類時沒有使用 extends 關鍵字,在 Scala 中這個定義類缺省繼承自 scala.AnyRef,如同在 Java 中缺省繼承自 java.lang.Object。這種繼承關係以下圖:

 

重寫override:子類重寫父子相同名稱的方法(方法簽名也要相同),或同名成員字段

實現implement:子類實現父類中抽象方法

 

scala> val ae = new ArrayElement(Array("hello", "world"))
ae: ArrayElement = ArrayElement@d94e60
scala> ae.width //訪問從父類Element繼承過來的成員
res1: Int = 5

 

val e: Element = new ArrayElement(Array("hello")) //父類的引用指向子類的實例

7.4                      字段重寫無參方法(或實現無參抽象方法)

Java 稍有不一樣的一點是,Scala 中方法與字段是在同一個命名空間,也就是說Scala 中不容許定義同名的無參成員函數(方法,無論是否有無空括號)和成員變量,這樣的好處是能夠使用成員變量來重寫一個無參的成員函數(或實現抽象無參方法)。好比下面B類中的屬性成員a字段實現了父類A中的a抽象方法:

abstract class A {

  def a:Int //抽象方法

}

class B extends A {

  val a = 1 //實現父類的抽象方法,這裏是實現而非重寫,因此前面能夠省略 override

}

注:字段實現父類中同名抽象無參方法時,能夠是val,也能夠是var這與字段與字段之間的重寫不太同樣

上面示例中屬於實現,因此實現時能夠省略override,但若是子類重寫父類的非抽象方法(具體方法)時前面是要帶override如:

class A {

  def a: Int = 1

}

class B extends A {

  override val a = 1 // 因爲是重寫父類同名非抽象方法,因此必定要加上 override 關鍵字

}

注:Java1.5 中, @Override 標註被引入並與 Scala override 修飾符做用同樣,但Java中的 override不是必需的

 

上面的示例都是子類中的成員字段實現或重寫父類中同名的無參方法,但無參方法是不能重寫同名成員字段

scala> class A {

     |   var a: Int = 1

     | }

defined class A

 

scala> class B extends A {

     |   override def a = 1  //這裏編譯出錯

     | }

<console>:13: error: overriding variable a in class A of type Int;

 method a cannot override a mutable variable 方法不能重寫變量(或常量,val定義的爲常量)

         override def a = 1

                      ^

 

Scala 裏禁止在同一個類裏用一樣的名稱定義字段和方法,而在 Java 裏這樣作被容許。例如,下面的 Java 類可以很好地編譯:

//Java裏的代碼

class CompilesFine {

    private int f = 0;

    public int f() {

       return 1;

    }

}

可是相應的 Scala 類將不能編譯:

class WontCompile {
private var f = 0 // 編譯不過,由於字段和方法重名
def f = 1

}

7.5                      字段重寫字段

子類的成員字段也是能夠重寫父類的同名字段的,但只有val類型的常量才能被重寫,而且重寫時也只能使用val來修飾:

class A {

  val a: Int = 1

}

class B extends A {

  override val a:Int = 2 //屬性重寫,不能省略override

}

若是將上面示例中的兩個val其中任何一個修改爲var就會報錯。

 

另外,父類私有private的對於子類是不可見的,因此不能重寫:

class A {

  private val a: Int = 1

}

class B extends A {

  val a: Int = 2

}

注:字段間的重寫不能省略override關鍵字

 

 

abstract class Fruit {

  val v: String

  def m: String

}

abstract class Apple extends Fruit {

  val v: String

  val m: String // OK to override a def with a val

}

abstract class BadApple extends Fruit {

  def v: String // ERROR: cannot override a val with a def

  def m: String

}

 

7.6                      參數化成員字段

上面的示例中,有這樣一段相似定義的代碼:

class T(a: Int) {

  val f = a

}

其中a: Int爲類的參數,在實例化時須要傳入此參:

val t = new T(1);

scala> t.f

res0: Int = 1

scala> t.a               // 不會產生名爲a的成員變量

<console>:14: error: value a is not a member of T

       t.a

         ^

雖然a是類一級別的參數,但它不會自動成爲類的成員變量,因此t.a是錯誤的,此狀況下的a僅僅是參數罷了,但若是在a: Int 的前面加上 val var呢?請看下面:

class T(val a: Int) {

  val f = a

}

val t = new T(1);

scala> t.f

res3: Int = 1

scala> t.a           // 會產生名爲a的成員變量

res4: Int = 1

scala> t.a = 2       // 因爲定義的是 val 類型,因此不能修改其值,t.at構造時會被初使化;若是定義成var,則此處能夠修改

<console>:13: error: reassignment to val

       t.a = 2

         ^

上面示例說明,只要在類的參數前面加上 val var,類的參數除了做爲類構造時的一個參數外,類的參數還自動成爲類的一個同名成員字段,這個成員字段與直接在類體中定義是沒有區別的,即此時類參數與類成員合二爲一了,即類的參數進一步提高爲了參數化成員字段

 

參數化成員字段定義前面還能夠加上privateprotectedoverride修飾符,這跟在類中直接定義沒有區別:

class Cat {

  val dangerous = false

}

class Tiger(

  override val dangerous: Boolean,//重寫父類的屬性成員,前面的override關鍵字不能省略

  private var age: Int) extends Cat

上面Tiger 的定義實質上是下面代碼寫法的簡寫,這是同樣的:

class Tiger(param1: Boolean, param2: Int) extends Cat {

  override val dangerous = param1

  private var age = param2

}

此時param1param2僅僅是類參數而已,不會成爲類的屬性成員

7.6.1      var成員變量、gettersetter方法

Scala中,對象的每一個private[this]訪問var類型成員變量都隱含定義了gettersetter方法(val變量不會生成setter方法),但這些gettersetter方法的命名方式並無沿襲Java的約定,var變量xgetter方法命名爲「x」,它的setter方法命名爲「x_=」。例如,若是類中存在var定義:

  var hour = 12//定義一個非私有的var變量時,除了會生成相應本地字段(private[this]修飾的字段)外,還會生相應的gttersetter方法

則除了有一個成員字段會生產以外,還額外生成getter方法「hour」,以及setter方法「hour_=」。無論var前面是什麼樣的訪問修飾符(除private[this]外,由於若是是private[this]則不會生成相應相應gettersetter方法),生成的成員字段始終會使用private[this]來修飾,表示只能被包含它的對象訪問,即便是同一類但不一樣對象也是不能進行訪問的;而生成的gettersetter方法前面的訪問修飾符與原val前訪問修飾符相同,即:若是var定義爲public,則它生成的gettersetter方法前訪問修飾符也是public,若是定義爲protected,那麼它們也是protected

例以下面類型了一個Time類,裏面定義了兩個公開的var變量hourminute

class Time {

  var hour = 12

  var minute = 0

}

下面是上面public var變量實際所生成的類,這是徹底相同的(類裏定義的本地字段(private[this]修飾的字段)hm的命令是隨意命名的,要求是不與任何已經存在的名稱衝突):

class Time {

  private[this] var h = 12

  private[this] var m = 0

  def hour: Int = h

  def hour_=(x: Int) { h = x }

  def minute: Int = m

  def minute_=(x: Int) { m = x }

}

因此,你也能夠直接經過上面第二種gettersetter方式來取代var變量的定義,這樣你能夠在gettersetter方法裏作一些控制。以下面再次改寫上面的代碼,加上數據有效性檢測:

class Time {

  private[this] var h = 12

  private[this] var m = 12

  def hour: Int = h

  def hour_=(x: Int) {

    require(0 <= x && x < 24) //參數有效性檢測

    h = x

  }

  def minute = m

  def minute_=(x: Int) {

    require(0 <= x && x < 60) //參數有效性檢測

    m = x

  }

}

能夠只定義gettersetter方法而不帶有本地關聯字段(private[this] var類型的字段),有時是須要這樣作的:好比溫度就有兩種,攝氏度與華氏度,但它們之間是能夠相互換算的,這樣就沒有必須設置兩個var變量分別存儲攝氏度與華氏度,而是以某一種來存儲,另外一種在settergetter時進行相應換算便可。下面就是以攝氏度來存儲,而華氏度有相應gettersetter方法來進行轉換便可:

class Thermometer {

  var celsius: Float = _ //攝氏度(),會自動生產相應的gettersetter方法

  def fahrenheit = celsius * 9 / 5 + 32//以華氏度單位來顯示

  def fahrenheit_=(f: Float) {//傳進來的是華氏度(),須要轉換後存入

    celsius = (f - 32) * 5 / 9

  }

  override def toString = fahrenheit + "F/" + celsius + "C"

}

上面的celsius變量,初始值設置爲了「_」,這個符號表示根據變量的類型來初始化初值:對於數值類型會設置爲0,布爾類型爲false,引用類型爲null

注意,Scala不能夠隨意省略「=_」初始化器,若是寫成

var celsius: Float

這將表示celsius爲抽象變量,這與Java中的成員變量省略初始化是不一樣的,Java成員變量(Java局部變量必定要顯示初始化)若是省略初始化賦值,則仍是會自動根據成員字段類型來進行初始化,而Scala中省略後則表示是一個抽象變量

 

7.7                      調用父類構造器

要調用父類構造器(主或輔助構造器均可,以參數來決定),只要把你要傳遞的參數或參數列表放在父類名以後的括號裏便可:

abstract class Element {

  def contents: Array[String]

  def height = contents.length

  def width = if (height == 0) 0 else contents(0).length

}

class ArrayElement(conts: Array[String]) extends Element {

  val contents: Array[String] = conts

}

//因爲父類ArrayElement帶了一個類參數conts,因此在定義子類LineElement時須要傳遞這個參數

class LineElement(s: String) extends ArrayElement(Array(s)) {

  override def width = s.length

  override def height = 1

}

7.8                      多態

class UniformElement(ch: Char,

  override val width: Int,

  override val height: Int) extends Element {

  private val line = ch.toString * width

  def contents = Array.fill(height)(line)

}

    val e1: Element = new ArrayElement(Array("hello", "world"))//父類引用指向子類對象,即多態

    val ae: ArrayElement = new LineElement("hello")

相關文章
相關標籤/搜索