Chris's Blog

Keep Walking......

Scala学习笔记

断断续续总算是读完了《Scala程序设计 - Java虚拟机多核编程实战》,习惯了Java中规中矩的语言规范,面对这种灵活的语法,着实有点不知所措,之前学习Python的时候,也有过类似的感受,不过这次学习Scala的感觉更强烈点。这本薄薄的不到两百页的书,硬是让我看了个把月,但也总算是对Scala有了初步的认识。对于这本书,只能算是入门书籍,很多东西都只是提到,但讲的不够详细。下面是学习过程中记下来的一些笔记,之后再对一些重要语法详细了解吧。

val vs. var

用val定义的变量是不可变的,用var定义的变量是可变的。在Scala中应尽量使用val以提升不变性。

遍历

RichInt中的两个方法to和until都返回一个Range的实例。
to() 产生的范围包含了上界和下界。

i <- 1 to 3

until() 产生的范围排除上界。

i <- 1 until 3

foreach()属于Range类,以一个函数值作为参数。

(1 to 3).foreach(i => print(i))

<- 左边为定义的val变量,右边为生成器表达式。
=> 左边为参数列表,右边为实现。

点和括号

如果方法有0或1个参数,点和括号是可选的,如果参数多于一个,则必须使用括号,但点仍然是可选的。

a+b  --> a.+(b)
1 to 3 -->  1.to(3)

元组

元组是一个不变的对象序列,可用逗号分隔的值进行创建。如:(“a”, “b”, “c”)

元组元素可以同时赋给多个var或val。如:val (name, age, sex) = getPerson()

将元组赋给数量不一致的变量会出现异常,可以通过._来访问单个元素。如:

val info = getPerson();
print(info._1) //first element
print(info._2) //second element

字符串

三个双引号之间的字符串可作为多行字符串。如:””“multiple line string”“”
多行字符串中允许嵌入双引号,多行字符串会保留字符串的原有格式。
stripMargin()可用于去除先导符前所有的空白或控制字符,默认先导符为“|”。

在Scala里,类、字段和方法默认是public,无需显示声明。

protected类只可以由子类访问,且子类只可以访问本类的protected成员。

return是可选的,方法调用会自动返回最后求值的表达式。若使用了return,则必须显示定义返回类型。

如果用”=“定义方法,Scala可推演返回类型,否则该方法返回类型为void。

分号是可选的,如果在一行内放置多条语句,可以用分号进行分隔。

方法若没有参数,可以省略方法定义中的括号。若构造函数没有参数,new对象后面的括号也可以省略。

给字段加上@scala.reflect.BeanProperty注解,即可以生成JavaBean风格的getter/setter。

Scala默认会导入两个包:java.langscala,和一个对象:scala.Predef

赋值结果是Unit类型,因此不可以多重赋值。如:

a = b  // return Unit, not a
a = b = c // throw error: type mismatch, found Unit, require Int

==对所有类型都是基于值的比较,而非引用的比较。eq()用于引用的比较。

变量赋值时的“_”代表该变量类型的默认值:Int为0,Double为0.0,引用类型为null

import语句里的“_”等价于Java中的“*”。如果“_”是跟在类名后,则导入类的所有成员,等价于Java中的static import。

主构造函数放在类定义中,若参数声明为val,则定义为private final类型;若参数声明为var,则定义为private类型;若参数既不是val,也不是var,则创建一个private字段以及private的getter/setter,就不能在类的外部访问该参数。

除主构造函数外,还可以通过this()定义两个或多个副构造函数。副构造函数的第一条语句必须是调用主构造函数,或调用另外一个副构造函数。如:

class Person(val firstName: String, val lastName: String) {
    private var gender: String = _


    def this (firstName: String, lastName: String, sex: String) {
         this (firstName, lastName)
         gender = sex
    }
}

类里定义的var字段会映射为一个private字段,并提供对应的getter/setter,字段上附加的访问权限会加到访问方法getter/setter上。

类继承时,重写方法必须使用override关键字,只有主构造函数才能通过override关键字往基类构造函数中传参数。如:

class Vehicle(val id: Int, val year: Int){
    override def toString() : String = "ID: " + id + " Year: " + year
}

class Car(override val id: Int, override val year: Int, var fuelLevel: Int) extends Vehicle(id, year) {
    override def toString() : String = super.toString() + " Fuel Level: " + fuelLevel
}

创建单例对象要用object关键字,而非class。因为单例对象无法初始化,所以不能给主构造函数传递参数。

class Marker(val color: String){
    override def toString() : String = "maker color " + color
}

object MarkerFactory {
   private val markers = Map(
       "red" -> new Marker("red")
   )

   def getMarker(color: String) = if (markers.contains(color)) markers(color) else null
}

上例中依然可以跳过MarkerFactory,直接创建Marker实例。

在Scala中可以创建一个关联到类上的单例,单例同类共享相同的名字,称为伴生对象,对应的类称为伴生类。类和伴生对象之间可以互相访问彼此的private字段和方法,可以把类的构造函数标记为private,这样就只可以通过伴生对象获取实例。每个类都可以有伴生对象,它们跟伴生类写在同一个文件中。如:

class Marker private (val color: String){
    override def toString() : String = "maker color " + color
}

object Marker {
   private val markers = Map(
       "red" -> new Marker("red")
   )

   def getMarker(color: String) = if (markers.contains(color)) markers(color) else null
}

Scala中没有静态字段和静态方法,伴生对象中的字段和方法可作为替代实现。

将创建伴生类的实例方法命名为apply(),可以更加容易获取实例,这是Scala提供的语法糖。在上例中需要通过 Marker getMarker “red” 来获取,若将getMarker改名为apply,则可以直接通过Marker(“red”)来获取。

def apply(color: String) = if (markers.contains(color)) markers(color) else null

Nothing

Nothing是所有类的子类。

Any

Any是所有类的父类。Any的直接子类是AnyVal和AnyRef,AnyVal是所有可以映射为Java基本类型的类的父类,AnyRef是所有引用类型的父类,可直接映射为Java的Object。

Option

用于强制检查实例是否存在。getOrElse()可处理结果不存在的情况。

可变参数(vararg)

方法参数在类型之后使用“*”,则标识该参数为可变数目实参。可变参数会当作数组处理,但不可传入数组作为实参。若相使用数组中的值作为可变实参,可以使用“_*”展开数组。如:

def max(values: Int*) = values.foldLeft(values(0)) { Math.max }
val numbers = Array(2, 4, 3, 7, 6)
println(max(numbers: _*))

参数化类型(Parameterized Type)

Scala使用“<:”来定义参数化类型。如 T<:Pet 表示T为Pet的子类。也可以使用“>:”,如 T>:Dog 表示T为Dog的父类。

def playWithPets[T <: Pet](pets: Array[T]) = println("……….")

参数化类型使用“+T”表示允许协变,即可接收某个类型或其父类。

参数化类型使用”-T“表示允许逆变,即可接收某个类型或其子类。

Curry化

将函数从接收多个参数转变为接收多个参数列表,以减少传递函数值。如:

原函数:

def foo(a: Int, b: Int, operation: (Int, Int) => Int) : Int = {}

Curry化:

def foo(a: Int)(b: Int)(operation: (Int, Int) => Int) : Int = {}

调用如下,函数值不需要以参数的形式写在括号中了,这种形式更优雅。

val sum = foo(1)(2) { (c, d) => c + d}

Trait

Trait里定义和初始化的val和var会在混入trait的类的内部得到实现。如果类没有继承其它类,则可以使用extends关键字混入trait;如果类已继承了其它类,则可以使用with关键字混入一个或多个trait。

trait Friend {
    val name: String
    def listen() = println("Your friend " + name + " is listening")
}

class Human(val name: String) extends Friend

class Dog(val name: String) extends Animal with Friend

Trait会被编译成Java的接口,还有对应的实现类,里面包含了trait已实现的方法。

Trait需要混入类实现未初始化的变量和值,trait的构造器不能有任何参数。

Trait也可以在实例中进行混入。如:

class Cat(val name: String) extends Animal

val snowy = new Cat("Snowy") with Friend

当Trait用于装饰对象时,trait只能混入继承自trait父类的类。在trait里,super并不是对基类的调用,而是对其左边混入的trait的调用,如果这个trait已经是最左的trait,则调用就会解析成混入这个trait的类的方法。

abstract class Check {
    def check() : String = "Checked Details…..."
}

trait CreditCheck extends Check {
    override def check() : String = "Checked Credit……" + super.check()
}

trait EmploymentCheck extends Check {
    override def check() : String = "Checked Employment….." + super.check()
}

val emplomentApp = new Check with EmploymentCheck with CreditCheck
println(emplomentApp check)

打印结果:

Checked Credit……Checked Employment….Checked Details……

若trait的基类的方法是抽象的,则trait在继承这个类时,需要将该方法声明为abstract override。这个方法的最终实现由混入这个trait的类提供。

abstract class Writer {
    def writeMessage(message: String)
}

trait UpperCaseWriter extends Writer {
    abstract override def writeMessage(message: String) = super.writeMessage(message.toUpperCase)
}

class StringWriterDelegate extends Writer {
    val writer = new java.io.StringWriter

    def writeMessage(message: String) = writer.write(message)
    override def toString() : String = writer.toString 
}

val writerTest = new StringWriterDelegate with UpperCaseWriter
writerTest writeMessage "String writer test"
println(writerTest)

打印结果:

STRING WRITER TEST

隐式转换

若需转换数据类型,可定义一个转换方法,并将该方法标记为implicit,该方法需要在变量使用的当前范围内可见(通过当前import可见,或位于当前文件)。

在Predef对象中,Scala已经定义了一些隐式转换,且默认导入它们。Scala一次最多应用一个隐式转换。

集合类

Scala的集合类包括:List, SetMap,除了List外,其它两种集合同时提供了可变和不可变的两个版本,默认使用不可变的集合类。Predef对象为集合类提供的别名指向不可变的实现。

可变版本:scala.collection.mutable
不可变版本:scala.collection.immutable

若需要修改集合且对集合的所有操作都在一个线程里,则可以选择可变集合类。

_:用于表示遍历集合时的每个元素。

Set

filter():用于过滤Set中的数据。
mkString():为Set中的每个元素创建了字符串,然后用参数字符串将结果连接起来。与apache common-lang中StringUtils的join方法作用相同。
++():合并两个Set成一个新的Set。
**():执行交集运算。
map():对每个元素应用给定的函数值,将结果收集到一个新的Set中。
toArray():将Set中的元素复制到数组中,以通过索引来访问Set中的元素。
foreach():迭代Set中的元素。

Map

key与value之间使用->。如:val feeds = Map(“Scala” -> “www.scala-lang.org”)

filterKeys():对Key进行过滤。
filter():对Key和Value进行过滤。提供给filter()的函数值接收一个(key, value)元组。如:

val result = feeds filter { element => 
    val (key, value) = element 
    (key startsWith "S") && (value contains "lang") 
}

get():用给定的Key获取Value。由于给定的Key可能没有对应的Value,所以该方法的返回类型是Option[T],结果可能是Some[T]或None,T为Value的类型。
apply():对类或对象使用括号时,即调用该方法。用于获取给定Key对应的Value,若没有对应的Value,则抛出异常,应将该方法放在try-catch中。
update():更新Map,并返回一个新的Map。有一种简写的方式:X() = b,等价于X.update(b),若参数多于一个,则将最后一个参数之外的所有参数放在括号里,X(a) = b 等价于X.update(a, b)。也可以将Map定义为可变容器scala.collection.mutable.Map,则可以直接更新。

List

List只有不可变实现。

head():可快速访问List的第一个元素。
tail():用于访问第一个元素之外的其它元素。访问最后一个元素需要遍历List,因此相比较head(),该操作的代价较大。
::():将元素添加在第一个元素之前。a :: list,即在list前添加a。该方法的完整表示:list.::(a)
:::():将一个list附加到另一个list之前。list ::: listA,即在listA前添加list。由于List是不可变的,所以这两个List都不会改变,会创建一个新的List并返回。将一个元素或List添加到另一个List的后面,实际上是调用后面那个List的前缀运算符,由于访问List的头元素比遍历到最后一个元素快的多,因此这种方式的性能会更好。
filter():对List进行过滤。
forall():检查List中的元素是否都满足指定的条件。
exists():检查List中是否存在满足指定条件的元素。
map():对每个元素应用给定的函数值,将结果收集到一个新的List中。
mkString():将List中的元素用参数字符串连接起来。与apache common-lang中StringUtils的join方法作用相同。
foldLeft():从List的最左端开始为每个元素调用给定的函数值。它会传递两个参数给函数值,第一个参数是对之前元素执行函数值的部分结果,初始值为方法的参数,第二个参数是List的元素。该方法的另一种表示:/:()

val total = feeds.foldLeft(0) { (total, feed) => total + feed.length }

等价于

val total = (0 /: feeds) { (total, feed) => total + feed.length }

foldRight():与foldLeft()的操作相同,但是从List的最右端开始。该方法的另一种表示:\:()

case表达式

case表达式通常与match一起使用做模式匹配。case会按照顺序自上而下进行比较。

_用作通配符,在所有指定的case都不匹配的情况下,作为默认处理,否则会抛出MatchError异常。

_*用作匹配List时,除去指定元素之外的所有元素。若需要引用这些元素,可以用@放在_*和变量名之间,如:

case List("apple", "orange", otherFruits @ _*) => println("apples, oranges, and " + otherFruits)

case语句可以根据类型进行匹配,还可以使用guard语句,用if从句表示。

def process(input: Any) {
    input match {
         case (a: Int, b: Int) => print("Processing (int, int)…..")
         case (a: Double, b: Double) => print("Processing (double, double)…..")
         case msg : Int if (msg > 1000) => print("Processing int > 1000")
         case msg : Int => print("Processing int…..")
         case _ => printf("Can't handle %s… ", input)
    }
}

case表达式中,模式变量要以小写字母开头,常量要以大写字母开头,否则无法通过编译。

sealed abstract用于告知Scala除该文件中已有的类之外,不会再有其它的类作为case类。若case表达式没有包含所有的case类,则会在编译时遇到warning。

case类若没有参数,在调用case表达式的方式时,也需要使用括号。否则参数就不是case类的实例,而是它的伴生对象。

定义Extractor可以在case表达式中匹配任意模式。unapply()方法用于接收要匹配的值。Extractor方法中的参数并不是传入的实参,而是用于接收从方法中返回的值。Extractor方法中的参数还可以引用另外一个Extractor方法用以验证返回的值,在参数和调用的Extractor方法之间使用@符合。

正则表达式

使用scala.util.matching包中的Regex类的实例进行匹配。

String的r()方法会将String转换成RichString,然后调用该方法并返回Regex实例。

正则表达式可直接在case表达式中作为Extractor使用。正则表达式中每个放在括号里的匹配都展开到一个模式变量中,如:“(S|s)cala”.r的unapply()方法会返回Option[String],“(S|s)(cala)”.r的unapply()方法会返回Option[String, String]。

findFirstIn():用于查找第一个正则表达式的匹配。
findAllIn():用于查找正则表达式的所有匹配,并返回List结果。
replaceFirstIn():用于替换第一个正则表达式的匹配。
replaceAllIn():用于替换所有正则表达式的匹配。

actor模型

actor提供了一种基于事件的轻量级线程。使用scala.actors.Actor伴生对象的actor()方法就可以创建一个actor。它接受一个函数值/闭包做参数,一创建好就开始运行。用!()方法给actor发消息,用reveive()方法从actor接收消息,通常用模式匹配处理接收到的消息。

Scala的Actor是个trait,混入Actor的类必须实现act()。start()方法用于启动actor并调用act()方法,没启动之前给actor发送的消息会进入队列。exit()方法用于停止actor,该方法会抛出异常,以终止当前线程的执行。

如果无需控制何时启动actor,则可以使用actor()方法创建actor。在actor中将消息委托给另一个actor,可以并发的处理多个请求。

receive()会造成程序阻塞,指导收到应答为止。receiveWithin()方法接收一个timeout参数,若超时时限内未收到消息,receiveWithin()会收到一个TIMEOUT消息。

使用SingleThreadedScheduler可以让Scala在主线程里运行actor,通过设置 Scheduler.impl = new SingleThreadedScheduler,就可以控制整个应用的actor调度策略。通过继承Actor trait,并改写scheduler()方法,可以控制线程是否运行在主线程中。

与Java互操作

若要在编译过的Scala或Java代码里使用Scala类,则必须编译Scala文件。

若Java代码中的方法或字段的名字与Scala的关键字有冲突,在调用时会导致Scala编译器死掉。可将冲突的变量名或方法名放在反引号中,以避免该问题。

Java暂不支持闭包,因此若Scala要提供给Java使用,则应避免使用闭包,或同时提供普通函数。

trait若没有方法实现,则可在Java中当作接口使用;若trait中有方法实现,则只可以在Java中持有该引用。

Scala的单例对象,在Java中可以像有static方法的Java类一样使用。

Scala会为同名类的伴生对象创建两个类,若类名为Buddy,则一个伴生类Buddy,和一个伴生对象类Buddy$。在Java中访问伴生类可以直接使用类的名字,访问伴生对象则需要使用MODULE$,如Buddy$.MODULE$。

Scala不支持throws,若Java中继承的Scala方法会抛出异常,就需要在Scala的方法前定义注解@throws。

Resources

IBM developerWorks Scala 指南:http://www.ibm.com/developerworks/cn/java/j-scala/
Programming Scala:http://ofps.oreilly.com/titles/9780596155957/

Comments