scala 程序设计第2版-第2章

2.1 分隔符,是指scala表达式之间的分隔符,有很多种。

// 末尾的等号表面下一步还有未结束的代码
def equalsign(s:String) = 
    println("equalsing: " + s)

// 末尾的花括号表面下一行还有未结束的代码
def equalsign2(s:String) = {
    println("equalsign2: " + s)
}

// 末尾的逗号、句号和操作符都可以表明,下一行还有未结束的代码
def commas(s1:String,
           s2:String) = Console.
    println("comma: " + s1 + 
            "," + s2)
  • 与编译器相比,REPL更容易将每行视为单独的表达式。因此在REPL中输入跨越多行的表达式,最安全的做法是每行(除最后一行外) 都以上述脚本中出现过的符号结尾。反过来,你可以将多个表达是放在同一行中,变大时之间使用分隔符隔开
  • 如果需要将多行代码解释为同一行,却被系统视为多个表达式,可以使用REPL的:paste 模式

paste模式

2.2 变量声明

2.2.1 scala 允许你决定该变量是不可变(只读)的,还是可变的(读写)。不可变使用val 关键字,可变使用var关键字

// val 变量声明时必须被初始化
scala> val array:Array[String] = new Array(5)
array: Array[String] = Array(null, null, null, null, null)

// val 类型不能够更改。array是个引用,它不能指向其他的Array,但是所指向的Array中的元素是可变的
scala> array = new Array(2)
<console>:12: error: reassignment to val
       array = new Array(2)
             ^
// array 不可变,但是里面的元素是可变的
scala> array(0) = "hello"

scala> array(1) = "scala"

scala> array(0)
res2: String = hello

scala> array(0) = "spark"

scala> array(0)
res4: String = spark

scala> array
res5: Array[String] = Array(spark, scala, null, null, null)

// 尽管stockPrice 是可变的,但是在声明的时候必须初始化,也就是无论变量是可变的还是不可变的,声明的时候必须初始化
scala> var stockPrice:Double = 100.0
stockPrice: Double = 100.0

// 这里修改的是stockPrice 本身,然而,stockPrice 所引用的"对象"没有修改,因为在scala中Double 类型是不可变的
scala> stockPrice = 200.0
stockPrice: Double = 200.0

2,2.2 如果在构造函数中的参数,这个时候的变量是不需要初始化的,无论是只读的还是读写的

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

scala> val p = new Person("Dean Wample",29)
p: Person = Person@42dafa95

scala> p.name
res6: String = Dean Wample

scala> p.age
res7: Int = 29

// name 是val 类型的,只读,不可变。为了减少可变性引起的bug,应尽量使用常量
scala> p.name = "Buck Trends"
<console>:13: error: reassignment to val
       p.name = "Buck Trends"
              ^

scala> p.age = 30
p.age: Int = 30

scala> p.age
res8: Int = 30

2.3 Range 生成数字/字母序列。Range 支持的类型包括Int,Long,Float,Double,Char,BigInt(任意大小的整数),BigDecimal(任意大小的浮点数)
start to/until end by setp

scala> 1 to 10
res9: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

scala> 1 until 10
res10: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> 1 to 10 by 3
res11: scala.collection.immutable.Range = Range(1, 4, 7, 10)

scala> 10 to 1 by 3
res12: scala.collection.immutable.Range = Range()

scala> 10 to 1 by -3
res13: scala.collection.immutable.Range = Range(10, 7, 4, 1)

scala> 1L to 10L by 2
res14: scala.collection.immutable.NumericRange[Long] = NumericRange(1, 3, 5, 7, 9)

scala> 1.1f to 10.3f by 2.1
<console>:12: error: type mismatch;
 found   : Double(2.1)
 required: Float
       1.1f to 10.3f by 2.1
                        ^

scala> 1.1f to 10.3f by 2.1f
res16: scala.collection.immutable.NumericRange[Float] = NumericRange(1.1, 3.1999998, 5.2999997, 7.3999996, 9.5)

scala> 1.1f to 15.3f by 0.32f
res17: scala.collection.immutable.NumericRange[Float] = NumericRange(1.1, 1.4200001, 1.74, 2.06, 2.3799999, 2.6999998, 3.0199997, 3.3399997, 3.6599996, 3.9799995, 4.2999997, 4.62, 4.94, 5.26, 5.5800004, 5.9000006, 6.2200007, 6.540001, 6.860001, 7.1800013, 7.5000014, 7.8200016, 8.140001, 8.460001, 8.780001, 9.1, 9.42, 9.74, 10.059999, 10.379999, 10.699999, 11.019999, 11.339998, 11.659998, 11.979998, 12.299997, 12.619997, 12.939997, 13.259996, 13.579996, 13.899996, 14.2199955, 14.539995, 14.859995, 15.179995)

scala> 1.2 to 10.5 by 2.0
res18: scala.collection.immutable.NumericRange[Double] = NumericRange(1.2, 3.2, 5.2, 7.2, 9.2)

scala> 1.2 to 10.5 by 2.5
res19: scala.collection.immutable.NumericRange[Double] = NumericRange(1.2, 3.7, 6.2, 8.7)

scala> 'a' to 'g' by 2
res20: scala.collection.immutable.NumericRange[Char] = NumericRange(a, c, e, g)

scala> BigInt(1) to BigInt(20) by 3
res21: scala.collection.immutable.NumericRange[BigInt] = NumericRange(1, 4, 7, 10, 13, 16, 19)

// scala 大小写敏感
scala> BigDecimal(1.1) to Bigdecimal(15.5) by 2.1
<console>:12: error: not found: value Bigdecimal
       BigDecimal(1.1) to Bigdecimal(15.5) by 2.1
                          ^


scala> BigDecimal(1.1) to BigDecimal(15.5) by 2.1
res23: scala.collection.immutable.NumericRange.Inclusive[scala.math.BigDecimal] = NumericRange(1.1, 3.2, 5.3, 7.4, 9.5, 11.6, 13.7)

2.3.1 scala 一个数字默认是Int 类型,一个小数 默认是double类型。10L 是Long类型,2.1f 是浮点类型

2.4 偏函数:trait PartialFunction[-A, +B] extends (A) ⇒ B
PartialFunction[A, B] = { case expression }

  • 在于它们并不处理所有可能的输入,而只处理那些能与至少一个case语句匹配的输入。
  • 在偏函数中只能使用case语句,而整个函数必须使用花括号包围。这与普通的函数字面量不同,普通函数字面量可以用花括号,也可以使用圆括号包围
  • 如果偏函数被调用,而函数的输入却与所有语句都不匹配,系统就会抛出一个MatchError,运行时错误
  • 链式连接:pf1 orElse pf2 orElse pf3…。如果pf1 不匹配,就会尝试pf2,接着是pf3,以此类推。如果以上偏函数都不匹配,才会抛出MathError
  • 使用 isDefinedAt 函数检查特定的输入是否与偏函数匹配,这样避免抛出MathError 错误
object partial {
    def main(args: Array[String]) {
        val pf1:PartialFunction[Any,String] = { case s:String => "YES" } 
        val pf2:PartialFunction[Any,String] = { case d:Double => "YES" }
        var pf = pf1 orElse pf2 // 匹配字符串和Double 类型的浮点数


        println("      | pf1 - String     |  pf2 - Double   |  pf - All")
        println("x     | def?  | pf1(x)   | def?  | pf2(x)  | def?  |  pf(x)") 
        println("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
        List("str",3.14,10) foreach { x =>
            printf("%-5s | %-5s  | %-6s  | %-5s  | %-6s | %-5s | %-6s\n",x.toString,
                d(x,pf1),tryPF(x,pf1),d(x,pf2),tryPF(x,pf2),d(x,pf),tryPF(x,pf))
        }
    }

    // 如果输入的是字符串或者DOUBLE 类型的浮点数就返回YES,否则返回ERROR
    // f(x) => pf1 orElse pf2 orElse pf
    def tryPF(x: Any,f: PartialFunction[Any,String]): String =
        try { f(x).toString } catch { case _: MatchError => "ERROR" }        

    def d(x: Any, f: PartialFunction[Any,String]): String =
        f.isDefinedAt(x).toString
}

//  输出:
      | pf1 - String     |  pf2 - Double   |  pf - All
x     | def?  | pf1(x)   | def?  | pf2(x)  | def?  |  pf(x)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
str   | true   | YES     | false  | ERROR  | true  | YES   
3.14  | false  | ERROR   | true   | YES    | true  | YES   
10    | false  | ERROR   | false  | ERROR  | false | ERROR

// 对上面的语句详解
scala> val pf1: PartialFunction[Any,String] = {case s:String => "YES"}
scala> pf1("abc")
res4: String = YES
scala> pf1(123) // 报 MatchError  错误
scala.MatchError: 123 (of class java.lang.Integer)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:253)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:251)
  at $anonfun$1.applyOrElse(<console>:11)
  at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:36)
  ... 32 elided

scala> pf("abc")
res6: String = YES

scala> pf(1.23)
res7: String = YES

scala> pf(1.32f) // float 类型的浮点数就会报错
scala.MatchError: 1.32 (of class java.lang.Float)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:253)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:251)
  at $anonfun$1.applyOrElse(<console>:11)
  at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:36)
  at $anonfun$1.applyOrElse(<console>:11)
  at scala.PartialFunction$OrElse.apply(PartialFunction.scala:167)
  ... 32 elided

// tryPF 两个参数,第一个是输入的值,第二个是偏函数名
scala> tryPF(123,pf1)
res10: String = Error

scala> tryPF('abc',pf1)
<console>:1: error: unclosed character literal
tryPF('abc',pf1) // 字符串 要使用双引号括起来 ("")
          ^

scala> tryPF("abc",pf1)
res11: String = YES

scala> tryPF("abc",pf2)
res12: String = Error

// d() 函数接受两个参数,第一个参数:输入,第二参数是偏函数(pf1/pf2/pf)
scala> d("123",pf1)
res47: String = true

scala> d(3.2,pf2)
res48: String = true

scala> d(3.2,pf1)
res49: String = false

scala> d("avc",pf2)
res50: String = false

scala> d(123,pf2)
res51: String = false

scala> d(123,pf1)
res52: String = false
scala> d("cde",pf)
res53: String = true

scala> d(12.3,pf)
res54: String = true

scala> d(124,pf)
res55: String = false

在看一个偏函数的例子

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

        val sample = 1 to 10
        // input type:Int  output type String (输入数据类型:Int,输出数据类型:String)
        val isEven: PartialFunction[Int,String] = {
            case x if x % 2 == 0 => x + ":" + "is even" 
        }

        // sample.collect(isEven)
        // sample collect(isEven)
        // sample collect isEven
        // 这3 种写法都可以,所得到的结果是一样的
        /*
            def collect[B](pf: PartialFunction[A, B]): Vector[B]
            [use case]
            Builds a new collection by applying a partial function to all elements of this vector on which the function is defined.
            B
            the element type of the returned collection.
            pf
            the partial function which filters and maps the vector.
            returns
            a new vector resulting from applying the given partial function pf to each element on which it is defined and collecting the results. The order of the elements is preserved.
            Definition Classes
            TraversableLike → GenTraversableLike
             Full Signature
            def collect[B, That](pf: PartialFunction[A, B])(implicit bf: CanBuildFrom[Vector[A], B, That]): That
         */
        val evenNumbers = sample collect isEven

        val isOdd: PartialFunction[Int,String] = {
            case x if x % 2 == 1 => x + ":" + "is odd"
        }
        val numbers = sample map (isEven orElse isOdd)
        println("numbers: " + numbers)
    }
}

2.5 方法默认值和命名参数列表。

  • 命名参数列表让客户端代码更具可读性。当参数列表很长,且有若干参数是同一类型时,bug容易避免,因为在这种情况很容易搞错参数传入的顺序。当然,更好的做法是一开始就避免出现过长的参数列表
object methods {
    case class Point(x: Double = 0.0, y: Double = 0.0) {
        def shift ( deltax: Double = 0.0, deltay: Double = 0.0) =
            copy (x + deltax,y + deltay) // copy() 方法是case 类自动创建的
    }
    def main(args: Array[String])  = {
        val p1 = new Point(x = 3.3, y = 4.4)
        val p0 = new Point() // 不给x,y的值就使用默认值(0.0,0.0)
            // copy 方法允许创建case 类新实例的时候只给出原对象不同的参数,这一点对于大些的case类非常有用
        val p2 = p1.copy(y = 6.6)
        val p3 = p1.shift(3.3,4.4)
        println("p1 = new Point(x = 3.3, y = 4.4)" + p1)
        println("p0 = new Point()" + p0)
        println("p2 = p1.copy(y=6.6) :" + p2)
        println("p3 = p1.shift(3.3,4.4) :" + p3)
    }
}
/*  输出:
p1 = new Point(x = 3.3, y = 4.4)Point(3.3,4.4)
p0 = new Point()Point(0.0,0.0)
p2 = p1.copy(y=6.6) :Point(3.3,6.6)
p3 = p1.shift(3.3,4.4) :Point(6.6,8.8)
*/

2.6 case class 样例类,特点

  • 实例化对象,不需要使用new 关键字,因为case class 默认有apply 方法,它负责创建实例化对象。这是与普通类的一个区别
scala> case class Book(isbn:String)
defined class Book

scala> val frankenstin = Book("978-0486282114")
frankenstin: Book = Book(978-0486282114)
  • case class 参数列表是public val
scala> case class Message(sender:String,recipient:String,body:String)

scala> val message1 = Message("guillaume@quebec.ca","jorge@catalonia.es","ca va ?")

scala> println(message1.sender)
guillaume@quebec.ca

scala> println(message1.recipient)
jorge@catalonia.es

scala> println(message1.body)
ca va ?

// val 变量只读。不可更改
scala> message1.sender = "gravis@washington.us"
<console>:14: error: reassignment to val
       message1.sender = "gravis@washington.us"
                       ^

// 实例化 var 变量,使得可以修改
scala> var message2 = Message("jorge@catalonia.es", "guillaume@quebec.ca", "Com va?")
message2: Message = Message(jorge@catalonia.es,guillaume@quebec.ca,Com va?)

scala> message1.recipient = "mark@catalonia.es"
<console>:14: error: reassignment to val
       message1.recipient = "mark@catalonia.es"
                          ^

scala> message1.body = "es va"
<console>:14: error: reassignment to val
       message1.body = "es va"
                     ^

// 修改 message2.sender
scala> println(message2.sender)
jorge@catalonia.es

// 虽然显示指定Message的实例是var类型,使得可读写,但是case class 参数还是 public val,虽然case class 参数列表可以使用var,但是不鼓励这样使用
scala> message2.sender = "mark@catalonia.es"
<console>:14: error: reassignment to val
       message2.sender = "mark@catalonia.es"
                       ^
scala> message2.recipient = "mark@catalonia.es"
<console>:14: error: reassignment to val
       message2.recipient = "mark@catalonia.es"
                          ^

scala> message2.body = "es va"
<console>:14: error: reassignment to val
       message2.body = "es va"
                     ^

scala> message2 = Message("mark@catalonia.es","lili@catalonia.ca","es ca?")
message2: Message = Message(mark@catalonia.es,lili@catalonia.ca,es ca?)
  • case class 实例对象比较的是结构而不是引用
scala> message2 = Message("mark@catalonia.es","lili@catalonia.ca","es ca?")
message2: Message = Message(mark@catalonia.es,lili@catalonia.ca,es ca?)

scala> val message2 = Message("jorge@catalonia.es","guillaume@quebe.ca","Com va?")
message2: Message = Message(jorge@catalonia.es,guillaume@quebe.ca,Com va?)

scala> val message3 = Message("jorge@catalonia.es","guillaume@quebe.ca","Com va?")
message3: Message = Message(jorge@catalonia.es,guillaume@quebe.ca,Com va?)

// 虽然引用不相同,但是结构相同,所以返回True
scala> val messageAreTheSame = message2 == message3
messageAreTheSame: Boolean = true

scala> println(messageAreTheSame)
true
  • 使用copy 函数,创建一个case class 的实例,可以选择地更改构造函数的参数
scala> case class Message(sender:String,recipient:String,body:String)
defined class Message

scala> val message4 = Message("julien@bretagne.fr","travis@washingto.us","Me zo o komz gant ma amezeg")
message4: Message = Message(julien@bretagne.fr,travis@washingto.us,Me zo o komz gant ma amezeg)

// body 在 message5 中没有显示赋值,就沿用message4.body 的值
scala> val message5 = message4.copy(sender = message4.recipient,recipient = "claire@bourgogne.fr")
message5: Message = Message(travis@washingto.us,claire@bourgogne.fr,Me zo o komz gant ma amezeg)

scala> println(message4.sender)
julien@bretagne.fr

scala> println(message5.sender)
travis@washingto.us

scala> println(message4.recipient)
travis@washingto.us

scala> println(message4.body)
Me zo o komz gant ma amezeg

scala> println(message5.body)
Me zo o komz gant ma amezeg

scala> println(message4.recipient)
travis@washingto.us
  • case class 自动生成许多方法,如String,hashCode,equals等方法
scala> case class Point(x:Double = 0.0,y:Double = 0.0)
scala> val p00 = Point()

// to.String 方法的输出
scala> println(p00)
Point(0.0,0.0)

// 调用equals 方法
scala> p00 == p20
res73: Boolean = false
  • case class 自动生成一个伴生类对象(与类同名的实例对象),自动添加很多方法,如:apply 方法
// Point 为 Point() 类的伴生对象
scala> val p00 = new Point
p00: Point = Point(0.0,0.0)

scala> val p00 = new Point()
p00: Point = Point(0.0,0.0)

// apply 方法可以省略
scala> val p1 = Point.apply(1.0,2.0)
p1: Point = Point(1.0,2.0)

scala> val p2 = Point(1.0,2.0)
p2: Point = Point(1.0,2.0)

scala> p2 == p1
res75: Boolean = true


// 伴生类对象与下列代码生成的对象一样
object Point { def apply (x: Double = 0.0,y: Double = 0.0) = new Point(x,y) }
  • case class 伴生类对象 apply 方法可以用于决定相对复杂的类继承结构。父类对象需要判断参数列表与那个子类型最吻合,并依此选着实例化的子类型。比方说,某一数据类型必须分别为元素最少的情况和元素数量最多的情况个提供一个不同的最佳实现,此时选择工厂方法可以屏蔽这一逻辑,为用户提供统一的接口

2.7 方法具有多个参数列表,有下面3个好处

  1. 多个参数列表的形式拥有整齐的语法结构
abstract class Shape1() {

        //def draw(f: String => Unit) : Unit = f(s"draw: ${this.toString}")
        def draw(offset: Point = Point(0.0,0.0))(f: String => Unit) : Unit = 
            f(s"draw(offset = $offset),${this.toString}")
    }

// 调用draw 方法

s.draw(Point(1.0,2.0))(str => println(s"ShapesDrawingActor: $str"))

// 下面的写法等价

s.draw(Point(1.0,2.0)){str => println(s"ShapesDrawingActor: $str")}

// 下面的写法等价

s.draw(Point(1.0,2.0)) { str =>
    println(s"ShapesDrawingActor: $str")
}

// 下面的写法等价

s.draw(Point(1.0,2.0)) { 
    str => println(s"ShapesDrawingActor: $str")
}

// 如果使用缺省的偏移量,第一个圆括号就不能省略
s.draw() {
    str => println(s"ShapesDrawingActor: $str")
}

// draw 方法可以使用一个带两个参数的参数列表
s.draw(Point(1.0,2.0), str => println(s"ShapesDrawingActor: $str")

// 这份代码并没有那么清晰和优雅,使用默认值开启offset 也没有那么便捷,因此我们不得不对参数进行命名:
s.draw(f = str => println(s"ShapesDrawingActor: $str")

2.参数列表中进行类型推断

scala> def m1[A](a :A,f: A => String) = f(a)
m1: [A](a: A, f: A => String)String

scala> def m2[A](a: A)(f: A => String) = f(a)
m2: [A](a: A)(f: A => String)String

scala> m1(100,i => s"$i + $i")
<console>:13: error: missing parameter type
       m1(100,i => s"$i + $i")
              ^

scala> m2(100)(i => s"$i + $i")
res1: String = 100 + 100

// m1 和 m2 看起来几乎一模一样,但是我们需要注意使用相同的参数调用他们是m1 和 m2 的表现。我们闯入Int 和一个函数 Int => String ,对于m1,Scala无法推断该函数的参数i,m2 则可以

3.使用多个参数列表的第三个好处,我们可以使用最后一个参数列表来推断隐含参数。隐含参数是用implicit 关键字声明的参数。当相应的方法被调用时,我们可以显示指定这个参数,或者也可以不指定,这时编译器会在当前作用域中找到一个合适的值作为参数。隐含参数可以代替参数默认值,而且更加灵活。

2.8 Future

  1. scala.concurrent.Future 是Scala提供的一个并发工具,其中的API使用隐含参数来减少冗余代码。
  2. Akka 使用了Future,但如果你并不需要actor的所有功能,也可以单独使用Akka中的Future部分
  3. 任务封装在Future中执行时,该任务的执行是异步的。Future API 提供了多种机制去获取执行结果,如提供回调函数。当结果就绪时,回调函数将被调用。
  4. 如果某个值声明为implicit?导入的scala.concurrent.ExecutionContext.Implicits.global 是在Future 中常用的默认 ExecutionContext。它使用impicit 关键字声明,因此如果调用时未显示给出ExecutionContext 参数,编译器就会使用这个默认值,本例就是这种情况。
  5. 只有由implicit 关键字声明的,在当前作用域可见的对象才能用作隐含值,只有被声明为implicit 的函数参数才允许调用时不给出实参,而采用隐含的值。
object Implicits {
    implicit val global: ExecutionContextExecutor = impl.ExecutionContextImpl.fromExecutor(null: Executor)
}

并发5个任务,并在任务结束时处理任务返回的结果

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def sleep(millis: Long) = {
    // Thread的sleep方法模拟程序进行繁忙的处理工作
    Thread.sleep(millis)
}

def doWork(index: Int) = {

    // 传入 0 - 1000 范围的随机数
    sleep((math.random * 1000).toLong)
    index
}
(1 to 5) foreach { index =>
    // 调用了scala.concurrent.Future.apply. Future.apply 传入了一个匿名函数
    val future = Future {
        doWork(index)
    }

    // 注册onSucess 一个回调函数,future成功执行完毕后,该回调会被执行
    future onSuccess {
        case answer: Int => println(s"Success! returned: $answer")
    }


    future onFailure {
        case th: Throwable => println(s"FAILURE! returned: $th") 
    }
}

sleep(1000)
println("finito!")

// 输出:
[root@master scalar]# scala futures.sc 
Success! returned: 2
Success! returned: 1
Success! returned: 5
Success! returned: 3
Success! returned: 4
finito!

/* 
每个future运行在各自独立的线程中,但这个说法不够严谨。事实上,Future API 允许我们通过ExecutionContext 来配置并发操作的执行
import 导入了默认的ExecutionContext,使用Java的ForkJoinPoll设置来管理Java线程池
此示例中调用了3个方法,其中这些方法的第二个参数列表隐含的ExecutionContext
apply 方法是上述3种方法之一。apply 声明:
apply[T](body: =>T)(implicit executor: ExecutonContext) : Future[T]
注意第二个参数列表中有implicit 关键字
其他两个方法在Fure.onSuccess 和 Future.onFailure 的声明
def onSuccess[U](func:(Try[T]) => U) (
    implicit executor: ExecutionContext) : Unit
def onFailure[U](callback:PartialFunction[Throwable,U])(implicit executor: ExecutionContext) : Unit
*/

/*
scala> (1 to 5) foreach( x=>
     | println(x))
1
2
3
4
5
*/

2.9 嵌套方法的定义与递归

  1. 很容易忘记调用嵌套函数! 如果编译器提升,能找到Unit 但找不到Long,可能就是忘记调用嵌套函数
  2. 两次用i作为参数名,第一次是factorial,第二次是fact。在fact方法这两个使用i参数屏蔽了外部factorial方法的i参数,这样是允许的,因为在fact方法中并不需要外部的i,只是在factorial结尾调用fact的时候才需要它
  3. 类似方法中声明的局部变量,嵌套方法也只有在声明它的方法中可见
  4. fact 必须声明返回类型,因为这是一个递归方法,Scala采用的是局部作用域类型推到,无法推到出递归函数的返回类型
  5. JVM 并不对尾递归做优化,否则尾递归会见递归转为循环,可以避免栈溢出。尾递归:表示调用递归函数是该函数中最后一个表达式,该表达式的返回值就是所谓的递归函数的返回值
  6. 递归是函数式编程的特点,也是优雅地实现很多算法的强大工具。所以Scala编译器对尾递归做了有限的优化。它会对函数调用自身做优化,但不会优化所谓的trampoline的情况,就是“a” 调用 “b”,“b” 调用“a”
  7. 添加一个关键字 tailrec 编译器会告诉你代码是否正确地实现尾递归
  8. 外层方法所在作用域中的一切在嵌套方法中都是可见的,包括传递给外层方法的参数
def countTo(n:Int) : Unit = {
    def count(i: Int) : Unit = {
        if ( i <= n) {println(i);count(i+1)}
    }
    count(1)
}
countTo(6)
def factorial(i: Int): Long = {
    def fact(i: Int,accumulator: Int) : Long = {
        if (i <= 1) accumulator
        else fact(i - 1,i * accumulator)
    }
    fact(i,1)
}
(0 to 5) foreach ( i => println(factorial(i)))

// 上面代码的改进版
import scala.annotation.tailrec
def factorial(i: Int): Long = {

    @tailrec
    def fact(i: Int,accumulator:Int) : Long = {
        if (i <= 1) accumulator
        else fact(i-1,i * accumulator)
    }
    fact(i,1)
}

(0 to 5) foreach (i => println(factorial(i)))

// 这个例子不是尾递归
scala> @tailrec
     | def fibonacci(i: Int) : Long = {
     | if (i <= 1) 1L
     | else fibonacci(i - 2) + fibonacci(i - 1)
     | }
<console>:15: error: could not optimize @tailrec annotated method fibonacci: it contains a recursive call not in tail position
       else fibonacci(i - 2) + fibonacci(i - 1)
                             ^

2.10 推断类型信息

  1. scala 支持局部类型推断,Haskell,可以推断出几乎所有的类型,因为他们可以执行全局类型推断
  2. 什么时候需要显示类型注解
    1. 声明了可变的var变量或不可变的val变量,没有进行初始化。(例如:在类中的抽象声明,如 val book: String,val count: Int)
    2. 所有的方法参数 (如 def deposit(amount: Money) = {….})
    3. 方法的返回值类型,在以下情况下必须显示声明类型
      1. 在方法中明显地使用return(即使在方法末尾也是如此)
      2. 递归方法
      3. 两个或者多个方法重载(拥有相同的函数名),其中一个方法调用了另一个重载方法,调用者需要显示类型注释
      4. scala 推断出的类型比你期望的类型更为宽泛,如Any
// java 7 
HashMap<Integer,String> intToStringMap = new HashMap<>()
// scala
val intToStringMap: HashMap[Interger,String] = new HashMap
// Scala 更简洁的写法
val intToStringMap = new HashMap[Interger,String]
object StringUtilV1 {
    def joiner(strings: String*): String = strings.mkString("-")

    // 编译报错    
    def joiner(strings: List[String]) = joiner(strings :_*)
}
println(StringUtilV1.joiner(List("Programing","Scala")))

/*
/data/scala/method_overloaded-return-v1.scX:4: error: overloaded method joiner needs result type
    def joiner(strings: List[String]) = joiner(strings :_*)
                                              ^
one error found
*/
// 改进
object StringUtilV1 {
    def joiner(strings: String*): String = strings.mkString("-")

    // def joiner(strings: List[String]) = joiner(strings :_*)
    // joiner(strings : String *) scala 不允许这么写
    def joiner(strings: List[String]): String = joiner(strings :_*)
}
println(StringUtilV1.joiner(List("Programing","Scala")))

/*
输出:
[root@spark1 scala]# scala method_overloaded-return-v1.scX 
Programing-Scala
*/

//  此代码可以改得更简洁,明了
object StringUtilV1 {
     | def joiner(strings : String*) : String = strings.mkString("-")
     | }
scala> val l = List("Programing","Scala")
// joiner 这个函数的参数需要Seq[String]
scala> StringUtilV1.joiner(l : _*)
res6: String = Programing-Scala

2.11 方法的可变参数,String* 表示0个或者多个String。方法还可以拥有其他参数,但是必须位于可变参数之前,而且只能有1个可变参数

def joiner(strings: String*): String = joiner(strings :_*)

2.12 函数定义的时候有一个常见的错误

scala> def double (i: Int) { 2 * i }
double: (i: Int)Unit

scala> println(double(2))
()
// 函数定义一般是有 "="
scala> def double (i: Int) = { 2 * i }
double: (i: Int)Unit

scala> println(double(2))
4
/*
这里解释下声明函数的时候没有 "=" 返回的值是Unit,也就是()。即使函数体最后一个表达式的值不是Unit类型也是如此
*/

2.13 保留字

这里写图片描述

  1. scala 没有break和continue。scala鼓励使用函数式编程来实现相同的break和continue功能,函数式编程通常会更加简洁,不容出现bug
  2. 一些java中的方法名是scala中的保留字,如java.util.Scanner.match. 为了避免编译错误,引用该方法名时,在名字两边加上反引号,如:java.util.Scanner.match

2.14 字面量

  1. 整数字面量
    1. 十进制:0或者一个非零值,后面跟上0个或者多个数字(0-9)。简单讲就是10进制的数字
    2. 十六进制:0x后面跟上一个或多个十六进制数字(0-9,A-F,a-f)
    3. 数字字面量前加上”-” 可表示负数
    4. 对于Long类型字面量,除非将该字面量赋值给一个Long类型的变量,否则需要在数字字母两后面加上一L或者l,如若不加,字面量的类型将默认推断为Int
    5. Long 取值范围:
      2632631 − 2 63 − − 2 63 − 1
      -9223372036854775808 – 9223372036854775807
    6. Int 取值范围:
      2312311 − 2 31 − − 2 31 − 1
      -2147483648 — 2147483647
    7. Short 取值范围:
      2152151 − 2 15 − − 2 15 − 1
      -32768 — 32767
    8. Char 取值范围:
      02161 0 − − 2 16 − 1
      0 — 65535
    9. Byte 取值范围:
      27271 − 2 7 − − 2 7 − 1
      -128 – 127
字面量超出范围会引发编译错误
scala> val i = 1234567890123
<console>:1: error: integer number too large
val i = 1234567890123
        ^

scala> val i = 1234567890123L
i: Long = 1234567890123

2.15 浮点数字面量

  1. Double 64位双精度浮点数。默认的小数都是这个类型。也可以显示指定浮点数是Double类型,在其最后面加”D”或者”d”
  2. Float 32 位单精度浮点数,后面需要加”F” 或者 “f”
  3. 小数点后不带数字的浮点数字面量在Scala 2.10 之后版本被废除
scala> val i = 1.f
<console>:11: error: value f is not a member of Int
       val i = 1.f
  1. 浮点数表示方法:
    1. .14
    2. 3.14
    3. 3.14f
    4. 3.14F
    5. 3.14d
    6. 3.14D
    7. 3e5 等价于
      3e5 3 ∗ e 5
    8. 3E5 等价于
      3E5 3 ∗ E 5
    9. 3.14e+5 等价于
      3.14e+5 3.14 ∗ e + 5
    10. 3.14e-5 等价于
      3.14e5 3.14 ∗ e − 5
    11. 3.14e+5f
    12. 3.14e+5F
    13. 3.14e+5D
    14. 3.14e+5d

2.16 布尔类型字面量

scala> val b1 = true
b1: Boolean = true

scala> val b2 = false
b2: Boolean = false

2.17 字符字面量

  1. 单引号括起来的一个可打印的Unicode 字符,要么是一个转义序列
  2. 范围:0-255。可以使用八进制数字的转义形式表示如 ‘\237’ 注意\ 后面最多跟3个8进制数字
  3. 如果反斜杠后面不是有效的转义序列会发生编译错误
  4. ‘A’,’\n’,’\u0041’(Unicode中的’A’)
  5. 不可打印的Unicode 字符,如:\u0009(水平制表符) 是不允许的。应该使用等价的转移形式\t
  6. 3个Unicode 字符,可以有效地替换相应的ASCII 序列:⇒ 替换=>,→替换->,←替换<-
  7. 转义字符序列见下表
转义序列含义
\b退格(BS)
\t水平制表符(HT)
\n换行(LF)
\f表格换行(FF)
\r回车(CR)
\”双引号(“)
\’单引号(‘)
\反斜杠()

2.18 字符串字面量

  1. 使用双引号或者3重双引号括起来。如”“”….”“”
  2. 对于使用双引号包围的字符串字面量,允许出现的字符与字符字面量相同。但是,如果双引号字符出现字符串中,则必须使用反斜杠\进行转义
scala> println("Programming\nScala")
Programming
Scala

scala> println("He exclaimed, \"Scala is great!\"")
He exclaimed, "Scala is great!"

scala> println("First\tSecond")
First   Second

3.三重双引号可以实现夸行,换行符是字符串的一部分,可以包含任意字符,可以是一个双引号或者连续多个双引号,但是不可以出现3个连续的双引号。反斜杠\ 不用于构成Unicode字符,也不用于构成有效的转义序列

// 包括换行符
scala> println("""hello
     | scala""")
hello
scala

// 反斜杠\不转义
scala> println("""First line\n Second line\tFourth line""")
First line\n Second line\tFourth line
def hello(name: String) = s"""Welcome!
  Hello, $name!
  * (Gratuitous Star!!)
  |We're glad you're here
  |      Have some extra whitespace.""".stripMargin

println(hello("Programming Scala"))
/*
输出:
[root@master scalar]# vim multiline-strings.sc
[root@master scalar]# scala multiline-strings.sc 
Welcome!
  Hello, Programming Scala!
  * (Gratuitous Star!!)
We're glad you're here
      Have some extra whitespace.
*/

/*
|We're glad you're here  删除人为添加的最左边的空格

|      Have some extra 删除最左边添加的空格,然后又人为的添加了空格,要以"|We're glad you're here" 这一行为标准
*/

4.移除整个字符串的前缀和后缀

def goodbye(name: String) = 
  s"""xxxGoodby,${name}yyy
  xxxCome again!yyy""".stripPrefix("xxx").stripSuffix("yyy")

// 只移除最前面的"xxx" 和最后面的"yyy" 中间的"xxx","yyy" 不移除
println(goodbye("Programming Scala"))
/*
输出:
[root@master scalar]# scala multiline-strings2.sc 
Goodby,Programming Scalayyy
  xxxCome again!

*/

2.18 符号字面量

  1. 两个同名的符号会指向内存中的同一个对象
  2. ‘使用单引号开头,后面跟字母,数字或者下划线”_”,但是不能以数字开头。’1symbol 这个无效的符号
  3. ‘id 等价于 Symbol(“id)
scala> val b = 'id
b: Symbol = 'id

scala> val a = Symbol("id")
a: Symbol = 'id

scala> a == b
res10: Boolean = true

2.19 函数字面量
(i:Int, s:String) => s+i 是一个类型为Function2[Int,String,String] (返回String)的函数字面量

// 使用函数字面量来声明变量,这两句的等价的

scala> val f1: (Int,String) => String = (i,s) => s + i
f1: (Int, String) => String = <function2>
// 最后那个String 是函数的返回类型
scala> val f2: Function2[Int,String,String] = (i,s) => s + i
f2: (Int, String) => String = <function2>

scala> f1 == f2
res7: Boolean = false

scala> f1(34,"abc")
res8: String = abc34

scala> f2(34,"abc")
res9: String = abc34

scala> val f3: (Int) => String = i => i + "abc"
f3: Int => String = <function1>

scala> val f4: Function1[Int,String] = i => i + "abc"
f4: Int => String = <function1>

scala> f3 == f4
res10: Boolean = false

scala> f3(93)
res11: String = 93abc

scala> f4(93)
res12: String = 93abc

// Function1,Function2  是Scala 自带的函数,第一个函数有一个参数,加一个返回类型,第二个参数有2个参数,加一个返回类型

2.20 元组字面量

  1. Scala 库中包含TupleN类(如Tuple2),用于组建N元素组。它以小括号加逗号分隔的元素序列的形式创建元素组
  2. TupleN N 取值1-22
  3. 3.
scala> val t1:(Int,String) = (1,"two")
t1: (Int, String) = (1,two)

scala> val t2: Tuple2[Int,String] = (1,"two")
t2: (Int, String) = (1,two)

scala> t1 == t2
res35: Boolean = true

val t=("Hello",1,2,3)
println("Print the whole tuple: " + t)
println("Print the first item: " + t._1)
println("Print the second item: " + t._2)
println("Print the third item: " + t._3)
println("Print the forth item: " + t._4)
// 编译错误
// println("Print the zero item: " + t._0)

val(t1,t2,t3) = ("World","!",0x22)

println(t1 + "," + t2 + "," + t3)

val(t4,t5,t6) = Tuple3("World",'!',0x22)
println(t4 + "," + t5 + "," + t6)
/*
输出:
Print the whole tuple: (Hello,1,2,3)
Print the first item: Hello
Print the second item: 1
Print the third item: 2
Print the forth item: 3
World,!,34
World,!,34
*/

两个元素的元组创建的4种方法

val t1 = (1,"one")
// 箭头 -> 适用于两个元素的元组
val t2 = 1 -> "one"
// →  用 -> 代替
val t3 = 1"one"
val t4 = Tuple2(1,"one")

// t1 == t2 == t3 == t4

scala> val tt = (2,3,4,"abc")
tt: (Int, Int, Int, String) = (2,3,4,abc)

2.21 Option、Some和None: 避免使用null

  1. Option 是抽象类,有两个子类Some(有值)、None
  2. Option 可以避免空指针
  3. Map.get 方法返回了Option[T], 这里类型T为String。在java中Map.get 返回T,T可能是null或实际的值。通过返回Option,我们就不会”忘记”去检查是否有实际值返回。换而言之,对于给定的key,对应的值可能并不存在,这一事实已经包含在方法返回的类型中了
  4. 第一组println 语句非直接第对get返回的实例执行了toString方法。事实上,我们是在对Some或None执行toString方法,因为当映射表中存在key对应的值时。Map.get的返回值被自动包装在Some对象中。相反,当我们请求一个映射表中不存在的数据时,Map.get 就返回None,而不是null,最后一个println就是这种情况
  5. 第二组 println 更进一步,调用Map.get后,又对Option 实例调用了get或者getOrElse,以取出其中包含的值
  6. Option.get 方法有些危险,如果Option 是一个Some,Some.get 则会返回其中的值。然而,如果Option 事实是一个None,None.get 就会抛出一个NoSuchElementException 异常
  7. Map.get 返回Option,这明细告诉读者映射表中有可能找不到指定的key
  8. Scala的静态类型性质,可以避免”忘记” 返回的是Option,从而调用Option里面的只(如果有值的话)来启动方法。Scala编译器的类型检查便强制要求你先从Option中提取值,再对它调用方法,这一机制”提现”你去检查Option是否等于None,所以Option的使用强烈鼓励更具有弹性的编程习惯
val stateCapitals = Map (
    "Alabama" -> "Montgomery",
    "Alaska" -> "Juneau",
    "Wyoming" -> "Cheyenne")

println( "Get the capitals wrapped in Options" )
println( "Alabama: " + stateCapitals.get("Alabama") )
println( "Wyoming: " + stateCapitals.get("Wyoming") )
println( "Unknown: " + stateCapitals.get("Unknown") )

println( "Get the capitals themselves out of the Options:" )
println( "Alabama: " + stateCapitals.get("Alabama").get )
println( "Wyoming: " + stateCapitals.get("Wyoming").getOrElse("Oops!") )
println( "Unknown: " + stateCapitals.get("Unknown").getOrElse("Oops2!") )

/*
输出:
Get the capitals wrapped in Options
Alabama: Some(Montgomery)
Wyoming: Some(Cheyenne)
Unknown: None
Get the capitals themselves out of the Options:
Alabama: Montgomery
Wyoming: Cheyenne
Unknown: Oops2!
*/

// 这个会抛出异常
println( "Unknown: " + stateCapitals.get("Unknown").get )

java.util.NoSuchElementException: None.get

2.22 封闭类的继承

  1. 关键字sealed 告诉编译器,所有子类必须在同一个源文件中声明。而在Scala库中Some 与 None就是Option声明在同一个源文件中的。这一技术有效防止了Option 派生其他子类型
  2. 如果防止用户派生任何子类,也可用使用final 关键字

2.23 用文件和名称空间组织代码

  • Scala 沿用Java用包来命名空间的这一做法,但它却更灵活性。文件名不必与类名一致,包结构不一定要与目录结构一致,所以,你可以定义与文件的”物理”位置独立的包结构
package com {
    package example {
        package pkg1 {
            class Class11 {
                def m = "m11"
            }
            class Class12 {
                def m = "m12"
            }
        }
        package pkg2 {
            class Class21 {
                def m = "m21"
                def makeClass11 = {
                    new pkg1.Class12
                }
            }
        }
        package pkg3.pkg31.pkg311 {
            class Class311 {
                def m = "m21"
            }
        }
    }
}
  • 使用单独的package语句,称为连续包
package com.example
package mypkg
  • Scala 遵循Java的惯例,将Scala库的root包命名为scala
  • 包不能在类或对象中定义
  • scala不允许在脚本中定义包,脚本被隐含包装在一个对象中,在对象中声明包是不允许的

2.24 导入类型及其成员

  • 要使用包的声明,必须先导入
// 下划线"_" 表示通配符,导入包中所有的类型
import java.awt._
// 单独导入一个包
import java.io.File
// 导入java.io.File._ 中所有的静态方法和属性。Scala 没有import static 这样的写法,因为Scala将Object类型与其他类型一视同仁
import java.io.File._
// 选择导入包,只导入 java.util.Map 和 java.util.HashMap
import java.util.{Map,HashMap}
  • import 语句几乎可以放到任何位置,因此可以将其可见性限制在需要的作用域中,可以在导入时对类型做重命名,也可以限制不想要的类型的可见性
def stuffWithBigInteger() = {
    import java.math.BigInteger .{
        ONE => _,
        TEN,
        ZERO => JAVAZERO
    }
    // println( "ONE: " + ONE ) // ONE 未定义
    println( "TEN" + TEN )
    println( "ZERO: " + JAVAZERO )
}
  • import语句在位于stuffWithBigInteger 函数中,导入的类型和值在函数外是不可见的
  • java.math.BigInteger.ONE 常量重命名为下划线”_”,使得常量不可见。当你需要导入除少部分以外的所有声明时,可以采用这一技术
  • java.math.BigInteger.TEN 导入时未经重命名,所以可以用TEN 引用它
  • java.math.BigInteger.ZERO 常量被赋予了JAVAZERO 的”别名”。别名非常重要,可以避免与其他scala中同名类型的冲突

2.25 导入是相对的

import scala.collection.mutable._
import collection.immutable._ // scala 已经导入,不需要给出全路径
import _root_.scala.collection.parallel._ // 从 "根" 开始全路径

当编译器错误的指出某个包无法找到时,这时需要检查导入语句中相对路径和绝对路径是否正确。在少数情况下,你还需要添加root 前缀。通常,使用顶层的包,如com、org 或 scala 就足够了。但必须保证问题库所在的路径包含在CLASSPATH中

2.26 包对象

package com.example // 文件目录:com/example/json 上层包的作用域

// 使用package 关键字给包名之后的对象命名,在这里对象名为json
package object json {
    class JSONObject { ... } // 适合暴露给客户端的成员
    def fromString(string: String) : JSONObject = { ... }
}
// 这样客户端可以用 import com.example.json._ 导入所有的定义,或用通常的方法单独导入元素

2.27 抽象类型与参数化类型

  • scala 参数化类型对应java 中的泛型,在语法上scala 使用方括号[…],scala 中<> 常用做方法名
//在集合List[A] 中使用任何类型作为类型A,这种特性被称为参数的多态。在方法List的通用实现中,允许使用任何类型的实例作为List的元素
val s1:List[String] = List("one","two","three")

sealed abstract class List[+A]
// + 表示:如果B是A的子类,则List[B] 也是List[A]的子类型,这被称为协类型,如果我们有一个函数f(list:List[Any]),那么将参数List[String]传递 给这个函数,也应该能够正常工作

// 如果类型参数前有"-" 减号 则表示另一种关系: 如果B是A的子类型,且Foo[A] 被声明为Foo[-A],则Foo[B] 是Foo[A] 的父类(称为逆类型)

// scala 还支持抽象类型的抽象机制,它可以运用在许多参数化类型中,也能够解决设计上的问题。然而,尽管两种机制有重合,但并不冗余 
// 此代码的功能将文件读入,然后按原有输出
import java.io._

abstract class BulkReader {
    // 抽象类BulkReader 有3个虚拟成员
    type In // 类型成员
    val source: In // source 只读变量,In类型
    def read: String // read 方法

}
// 派生类 StringBulkReader 和 FileBulkReader 为父类的3个成员提供具体化的定义
class StringBulkReader(val source:String) extends BulkReader {
    type In = String
    def read: String = source // 返回source
}

class FileBulkReader(val source: File) extends BulkReader {
    type In = File
    // 读取文件的内容
    def read: String = {

        val in = new BufferedInputStream(new FileInputStream(source))
        val numBytes = in.available()
        val bytes = new Array[Byte](numBytes)
        in.read(bytes,0,numBytes)
        new String(bytes)
    }
}
println(new StringBulkReader("Hello Scala!").read)

println(new FileBulkReader( new File("/tmp/test.txt")).read)

// 上述代码可以修改这样的
import java.io._

abstract class BulkReader[In] {
    // type In
    val source: In
    def read: String

}

class StringBulkReader(val source:String) extends BulkReader[String] {
    // type In = String
    def read: String = source
}

class FileBulkReader(val source: File) extends BulkReader[File] {
    // type In = File
    def read: String = {

        val in = new BufferedInputStream(new FileInputStream(source))
        val numBytes = in.available()
        val bytes = new Array[Byte](numBytes)
        in.read(bytes,0,numBytes)
        new String(bytes)
    }
}
println(new StringBulkReader("Hello Scala!").read)

println(new FileBulkReader( new File("/tmp/test.txt")).read)
  • 就像参数化类型,如果我们定义In类型为String,则 source 属性也必须被定义为String。
  • 当类型参数与参数化的类型无关时,参数化类型更适用。例如:List[A], A 可能是Int,String或Person等
  • 当类型成员与所封装的类型同步变化时,类型成员最适用。正如BulkReader 这个例子,类型成员需要与封装的类型行为一致。有时候这种特点被称为家族多态,或者协特化
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值