Scala学习
第一个 Scala 程序:Hello World
object HelloWord {
def main(args: Array[String]) Unity= {
println("Hello, world!")
}
}
简介
Scala简介
Scala 是 Scalable Language 的简写,是一门多范式的编程语言
联邦理工学院洛桑(EPFL)的Martin Odersky于2001年基于Funnel的工作开始设计Scala。
Funnel是把函数式编程思想和Petri网相结合的一种编程语言。
Odersky先前的工作是Generic Java和javac(Sun Java编译器)。Java平台的Scala于2003年底/2004年初发布。.NET平台的Scala发布于2004年6月。该语言第二个版本,v2.0,发布于2006年3月。
截至2009年9月,最新版本是版本2.7.6 。Scala 2.8预计的特性包括重写的Scala类库(Scala collections library)、方法的命名参数和默认参数、包对象(package object),以及Continuation。
2009年4月,Twitter宣布他们已经把大部分后端程序从Ruby迁移到Scala,其余部分也打算要迁移。此外, Wattzon已经公开宣称,其整个平台都已经是基于Scala基础设施编写的。
Scala特性
面向对象特性
Scala是一种纯面向对象的语言,每个值都是对象。对象的数据类型以及行为由类和特质描述。
类抽象机制的扩展有两种途径:一种途径是子类继承,另一种途径是灵活的混入机制。这两种途径能避免多重继承的种种问题。
函数式编程
Scala也是一种函数式语言,其函数也能当成值来使用。Scala提供了轻量级的语法用以定义匿名函数,支持高阶函数,允许嵌套多层函数,并支持柯里化。Scala的case class及其内置的模式匹配相当于函数式编程语言中常用的代数类型。
更进一步,程序员可以利用Scala的模式匹配,编写类似正则表达式的代码处理XML数据。
静态类型
Scala具备类型系统,通过编译时检查,保证代码的安全性和一致性。类型系统具体支持以下特性:
泛型类
协变和逆变
标注
类型参数的上下限约束
把类别和抽象类型作为对象成员
复合类型
引用自己时显式指定类型
视图
多态方法
扩展性
Scala的设计秉承一项事实,即在实践中,某个领域特定的应用程序开发往往需要特定于该领域的语言扩展。Scala提供了许多独特的语言机制,可以以库的形式轻易无缝添加新的语言结构:
任何方法可用作前缀或后缀操作符
可以根据预期类型自动构造闭包。
并发性
Scala使用Actor作为其并发模型,Actor是类似线程的实体,通过邮箱发收消息。Actor可以复用线程,因此可以在程序中可以使用数百万个Actor,而线程只能创建数千个。在2.10之后的版本中,使用Akka作为其默认Actor实现。
谁使用了 Scala
2009年4月,Twitter宣布他们已经把大部分后端程序从Ruby迁移到Scala,其余部分也打算要迁移。
此外,Wattzon已经公开宣称,其整个平台都已经是基于Scala基础设施编写的。
瑞银集团把Scala用于一般产品中。
Coursera把Scala作为服务器语言使用。
Scala Web 框架
以下列出了两个目前比较流行的 Scala 的 Web应用框架:
Lift 框架
Play 框架
Scala基础语法
Scala 与 Java 的最大区别是:Scala 语句末尾的分号 ; 是可选的。
我们可以认为 Scala 程序是对象的集合,通过调用彼此的方法来实现消息传递。接下来我们来理解下,类,对象,方法,实例变量的概念:
- 对象 - 对象有属性和行为。例如:一只狗的状属性有:颜色,名字,行为有:叫、跑、吃等。对象是一个类的实例。
- 类 - 类是对象的抽象,而对象是类的具体实例。
- 方法 - 方法描述的基本的行为,一个类可以包含多个方法。
- 字段 - 每个对象都有它唯一的实例变量集合,即字段。对象的属性通过给字段赋值来创建。
基本语法
Scala 基本语法需要注意以下几点:
- 区分大小写 - Scala是大小写敏感的,这意味着标识Hello 和 hello在Scala中会有不同的含义。
- 类名 - 对于所有的类名的第一个字母要大写。
如果需要使用几个单词来构成一个类的名称,每个单词的第一个字母要大写。
示例:class MyFirstScalaClass- 方法名称 - 所有的方法名称的第一个字母用小写。
如果若干单词被用于构成方法的名称,则每个单词的第一个字母应大写。
示例:def myMethodName()- 程序文件名 - 程序文件的名称应该与对象名称完全匹配。
保存文件时,应该保存它使用的对象名称(记住Scala是区分大小写),并追加".scala"为文件扩展名。 (如果文件名和对象名称不匹配,程序将无法编译)。
示例: 假设"HelloWorld"是对象的名称。那么该文件应保存为’HelloWorld.scala"- def main(args: Array[String]) - Scala程序从main()方法开始处理,这是每一个Scala程序的强制程序入口部分。
标识符
Scala 可以使用两种形式的标志符,字符数字和符号。
字符数字使用字母或是下划线开头,后面可以接字母或是数字,符号"$“在 Scala 中也看作为字母。然而以”$“开头的标识符为保留的 Scala 编译器产生的标志符使用,应用程序应该避免使用”$"开始的标识符,以免造成冲突。
Scala 的命名规则采用和 Java 类似的 camel 命名规则,首字符小写,比如 toString。类名的首字符还是使用大写。此外也应该避免使用以下划线结尾的标志符以避免冲突。符号标志符包含一个或多个符号,如+,:,? 等,比如:
+ ++ ::: < ?> :->
Scala 内部实现时会使用转义的标志符,比如:-> 使用
c
o
l
o
n
colon
colonminus$greater 来表示这个符号。因此如果你需要在 Java 代码中访问:->方法,你需要使用 Scala 的内部名称
c
o
l
o
n
colon
colonminus$greater。
混合标志符由字符数字标志符后面跟着一个或多个符号组成,比如 unary_+ 为 Scala 对+方法的内部实现时的名称。字面量标志符为使用"定义的字符串,比如
`x` `yield`。
你可以在"之间使用任何有效的 Scala 标志符,Scala 将它们解释为一个 Scala 标志符,一个典型的使用为 Thread 的 yield 方法, 在 Scala 中你不能使用 Thread.yield()是因为 yield 为 Scala 中的关键字, 你必须使用 Thread.yield
()来使用这个方法。
Scala 关键字
下表列出了 scala 保留关键字,我们不能使用以下关键字作为变量:
- | - | - | - |
---|---|---|---|
abstract | case | catch | class |
def | do | else | extends |
false | final | finally | for |
forSome | if | implicit | import |
lazy | match | new | null |
object | override | package | private |
protected | return | sealed | super |
this | throw | trait | try |
true | type | val | var |
while | with | yield | |
- | : | = | => |
<- | <: | <% | >: |
# | @ |
Scala 注释
Scala 类似 Java 支持单行很多行注释。多行注释可以嵌套,但必须正确嵌套,一个注释开始符号对应一个结束符号。注释在 Scala 编译中会被忽略,实例如下:
object HelloWorld {
/* 这是一个 Scala 程序
* 这是一行注释
* 这里演示了多行注释
*/
def main(args: Array[String]) {
// 输出 Hello World
// 这是一个单行注释
println("Hello, world!")
}
}
注意:默认情况下,Scala 总会引入 java.lang._ 、 scala._ 和 Predef._,这里也能解释,为什么以scala开头的包,在使用时都是省去scala.的。
Scala数据类型
Scala 与 Java有着相同的数据类型,下表列出了 Scala 支持的数据类型:
数据类型 | 描述 |
---|---|
Byte | 8位有符号补码整数。数值区间为 -128 到 127 |
Short | 16位有符号补码整数。数值区间为 -32768 到 32767 |
Int | 32位有符号补码整数。数值区间为 -2147483648 到 2147483647 |
Long | 64位有符号补码整数。数值区间为 -9223372036854775808 到 9223372036854775807 |
Float | 32位IEEE754单精度浮点数 |
Double | 64位IEEE754单精度浮点数 |
Char | 16位无符号Unicode字符, 区间值为 U+0000 到 U+FFFF |
String | 字符序列 |
Boolean | true或false |
Unit | 表示无值,和其他语言中void等同。用作不返回任何结果的方法的结果类型。Unit只有一个实例值,写成()。 |
Null | null 或空引用 |
Nothing | Nothing类型在Scala的类层级的最低端;它是任何其他类型的子类型。 |
Any | Any是所有其他类的超类 |
AnyRef | AnyRef类是Scala里所有引用类(reference class)的基类 |
Scala变量
在 Scala 中,使用关键词 “var” 声明变量,使用关键词 “val” 声明常量。
声明变量实例如下:
var myVar : String = "Foo"
var myVar : String = "Too"
以上定义了变量 myVar,我们可以修改它。
声明常量实例如下:
val myVal : String = "Foo"
Scala 访问修饰符
Scala 访问修饰符基本和Java的一样,分别有:private,protected,public。
如果没有指定访问修饰符符,默认情况下,Scala对象的访问级别都是 public。
Scala 中的 private 限定符,比 Java 更严格,在嵌套类情况下,外层类甚至不能访问被嵌套类的私有成员。
私有(Private)成员
用private关键字修饰,带有此标记的成员仅在包含了成员定义的类或对象内部可见,同样的规则还适用内部类。
class Outer{
class Inner{
private def f(){println("f")}
class InnerMost{
f() // 正确
}
}
(new Inner).f() //错误
}
(new Inner).f( ) 访问不合法是因为 f 在 Inner 中被声明为 private,而访问不在类Inner之内。
但在 InnerMost 里访问f就没有问题的,因为这个访问包含在 Inner 类之内。
Java中允许这两种访问,因为它允许外部类访问内部类的私有成员。
保护(Protected)成员
在 scala 中,对保护(Protected)成员的访问比 java 更严格一些。因为它只允许保护成员在定义了该成员的的类的子类中被访问。而在java中,用protected关键字修饰的成员,除了定义了该成员的类的子类可以访问,同一个包里的其他类也可以进行访问。
package p{
class Super{
protected def f() {println("f")}
}
class Sub extends Super{
f()
}
class Other{
(new Super).f() //错误
}
}
上例中,Sub 类对 f 的访问没有问题,因为 f 在 Super 中被声明为 protected,而 Sub 是 Super 的子类。相反,Other 对 f 的访问不被允许,因为 other 没有继承自 Super。而后者在 java 里同样被认可,因为 Other 与 Sub 在同一包里。
公共(Public)成员
Scala中,如果没有指定任何的修饰符,则默认为 public。这样的成员在任何地方都可以被访问。
class Outer {
class Inner {
def f() { println("f") }
class InnerMost {
f() // 正确
}
}
(new Inner).f() // 正确因为 f() 是 public
}
作用域保护
Scala中,访问修饰符可以通过使用限定词强调。格式为:
private[x]
或
protected[x]
这里的x指代某个所属的包、类或单例对象。如果写成private[x],读作"这个成员除了对[…]中的类或[…]中的包中的类及它们的伴生对像可见外,对其它所有类都是private。
这种技巧在横跨了若干包的大型项目中非常有用,它允许你定义一些在你项目的若干子包中可见但对于项目外部的客户却始终不可见的东西。
package bobsrocckets{
package navigation{
private[bobsrockets] class Navigator{
protected[navigation] def useStarChart(){}
class LegOfJourney{
private[Navigator] val distance = 100
}
private[this] var speed = 200
}
}
package launch{
import navigation._
object Vehicle{
private[launch] val guide = new Navigator
}
}
}
}
上述例子中,类Navigator被标记为private[bobsrockets]就是说这个类对包含在bobsrockets包里的所有的类和对象可见。
比如说,从Vehicle对象里对Navigator的访问是被允许的,因为对象Vehicle包含在包launch中,而launch包在bobsrockets中,相反,所有在包bobsrockets之外的代码都不能访问类Navigator。
Scala IF…ELSE 语句
scala程序员的平衡感
1.崇尚val,不可变对象和没有副作用的方法
2.首先想到他们,只有在特定需要和判断之后才选择var,可变对象和有副作用的方法
类和对象
类,字段和方法
类是对象的蓝图。一旦你定义了类,你就可以用关键字new从类的蓝图里创建对象。比方说,如果给出了类的定义:
class ChecksumAccumulator {
// class definition goes here
}
你就能创建ChecksumAccumulator对象:
new CheckSumAccumulator
类定义里,可以放置字段和方法,这些被笼统地称为成员:member。字段,不管是用val或是用var定义的,都是指向对象的变量。
方法,用def定义,包含了可执行的代码。
字段保留了对象的状态或者数据,而方法使用这些数据对对象做运算工作。
当你实例化类的时候,执行期环境会设定一些内存来保留对象状态的镜像——也就是说,变量的内容。
举例来说,如果你定义了ChecksumAccumulator类并给它一个叫做sum的var字段
class ChecksumAccumulator {
var sum = 0
}
单例对象
Scala没有静态成员,替代品是Scala有单例对象:singleton object.除了用object 关键字替换了class关键字以外,单例对象的定义看上去就像是类定义。
区别:类和单例对象间的一个差别是,单例对象不带参数,而类可以。因为你不能用new关键字实例化一个单例对象,你没机会传递给它参数。每个单例对象都被作为由一个静态变量指向的虚构类:synthetic class的一个实例来实现,因此它们与Java静态类有着相同的初始化语法。特别要指出的是,单例对象会在第一次被访问的时候初始化。 不与伴生类共享名称的单例对象被称为孤立对象:standalone object。由于很多种原因你会用到它,包括把相关的功能方法收集在一起,或定义一个Scala应用的入口点。
Scala程序
虚构类的名字是对象名加上一个美元符号。因此单例对象ChecksumAccumulator的虚构类是ChecksumAccumulator$。
基本类型和操作
一些基本类型
Byte: 8位有符号补码整数
Short: 16位有符号补码整数
Int: 32位有符号补码整数
Long: 64位有符号补码整数
Char: 16位无符号Unicode字符(0~216
String: 字符序列
Float: 32位IEEE754单精度浮点数
Double: 64位IEEE754单精度浮点数
Boolean: true或false
Scala的==与Java的有何差别:
Java里的既可以比较原始类型也可以比较参考类型。对于原始类型,Java的比较值的相等性,如Scala。然而对于参考类型,Java的比较了参考相等性:reference equality,也就是说这两个变量是否都指向于JVM堆里的同一个对象。Scala也提供了这种机制,名字是eq。不过,eq和它的反义词,ne,仅仅应用于可以直接映射到Java的对象。eq和ne的全部细节将在11.1节和11.2节给出。还有,可以看一下第二十八章,了解如何编写好的equals方法。
函数式对象
函数式对象:没有任何可变状态的对象的类。
隐式转换
implicit def intToRational(x: Int) = new Rational(x)
这行代码定义了从Int到Rational的转换方法。方法前面的implicit修饰符告诉编译器若干情况下自动调用它。
内建控制结构
Scala里没有多少内建控制结构。仅有的包括if,while,for,try,match和函数调用。如此之少的理由是,从一开始Scala就包括了函数文本。代之以在基本语法之上一个接一个添加高层级控制结构,Scala把它们汇集在库里。
函数和闭包
当程序变得庞大时,你需要一些方法把它们分割成更小的,更易管理的片段。为了分割控制流,Scala提供了所有有经验的程序员都熟悉的方式:把代码分割成函数。实际上,Scala提供了许多Java中没有的定义函数的方式。除了作为对象成员函数的方法之外,还有内嵌在函数中的函数,函数文本和函数值。
方法
定义函数最通用的方法是作为某个对象的成员。这种函数被称为方法:method。
函数是第一类值
Scala拥有第一类函数:first-class function。你不仅可以定义函数和调用它们,还可以把函数写成没有名字的文本:literal并把它们像值:value那样传递。
函数文本被编译进一个类,类在运行期实例化的时候是一个函数值:function value。任何函数值都是某个扩展了若干scala包的FunctionN特质之一的类的实例,如Function0是没有参数的函数,Function1是有一个参数的函数等等。每个FunctionN特质有一个apply方法用来调用函数。
因此函数文本和值的区别在于函数文本存在于源代码,而函数值存在于运行期对象。这个区别很像类(源代码)和对象(运行期)的那样。 以下是对数执行递增操作的函数文本的简单例子: (x: Int) => x + 1 =>指明这个函数把左边的东西(任何整数x)转变成右边的东西(x + 1)。所以,这是一个把任何整数x映射为x + 1的函数。
于是现在你已经看到了有如螺丝和螺帽的函数文本和函数值。许多Scala库给你使用它们的机会。例如,所有的集合类都能用到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
函数文本的短格式
Scala提供了许多方法去除冗余信息并把函数文本写得更简短。注意留意这些机会,因为它们能让你去掉代码里乱七八糟的东西。 一种让函数文本更简短的方式是去除参数类型。因此,前面带过滤器的例子可以写成这样:
scala> someNumbers.filter((x) => x > 0)
res7: List[Int] = List(5, 10)
Scala编译器知道x一定是整数,因为它看到你立刻使用了这个函数过滤整数列表(由someNumbers暗示)。这被称为目标类型化:target typing,因为表达式的目标使用——本例中someNumbers.filter()的参数——影响了表达式的类型化——本例中决定了x参数的类型。目标类型化的精确细节并不重要。你可以简单地从编写一个不带参数类型的函数文本开始,并且,如果编译器不能识别,再加上类型。几次之后你就对什么情况编译器能或不能解开谜题有感觉了。 第二种去除无用字符的方式是省略类型是被推断的参数之外的括号。前面例子里,x两边的括号不是必须的:
scala> someNumbers.filter(x => x > 0)
res8: List[Int] = List(5, 10)
占位符语法
如果想让函数文本更简洁,可以把下划线当做一个或更多参数的占位符,只要每个参数在函数文本内仅出现一次。比如,_ > 0对于检查值是否大于零的函数来说就是非常短的标注:
scala> someNumbers.filter(_ > 0)
res9: List[Int] = List(5, 10)
你可以把下划线看作表达式里需要被“填入”的“空白”。这个空白在每次函数被调用的时候用函数的参数填入。例如,由于someNumbers在第115页被初始化为值List(-11, -10, -5, 0, 5, 10),filter方法会把_ > 0里的空格首先用-11替换,就如-11 > 0,然后用-10替换,如-10 > 0,然后用-5,如-5 > 0,这样直到List的最后一个值。因此,函数文本_ > 0与稍微冗长一点儿的x => x > 0相同,演示如下:
scala> someNumbers.filter(x => x > 0)
res10: List[Int] = List(5, 10)
偏应用函数
尽管前面的例子里下划线替代的只是单个参数,你还可以使用一个下划线替换整个参数列表。例如,写成println(_),或者更好的方法你还可以写成println _。下面是一个例子:
someNumbers.foreach(println _)
Scala把这种短格式直接看作是你输入了下列代码:
someNumbers.foreach(x => println(x))
因此,这个例子中的下划线不是单个参数的占位符。它是整个参数列表的占位符。请记住要在函数名和下划线之间留一个空格,因为不这样做编译器会认为你是在说明一个不同的符号,比方说是,似乎不存在的名为println_的方法。
重复参数
Scala允许你指明函数的最后一个参数可以是重复的。这可以允许客户向函数传入可变长度参数列表。想要标注一个重复参数,在参数的类型之后放一个星号。例如:
scala> def echo(args: String*) = for (arg <- args) println(arg)
echo: (String*)Unit
这样定义,echo可以被零个至多个String参数调用:
scala> echo()
scala> echo("one")
one
scala> echo("hello", "world!")
hello
world!
函数内部,重复参数的类型是声明参数类型的数组。因此,echo函数里被声明为类型“String*”的args的类型实际上是Array[String]。然而,如果你有一个合适类型的数组,并尝试把它当作重复参数传入,要实现这个做法,你需要在数组参数后添加一个冒号和一个_*符号,像这样:
scala> echo(arr: _*)
What's
up
doc?
这个标注告诉编译器把arr的每个元素当作参数,而不是当作单一的参数传给echo。
控制抽象
叫名参数:by-name parameter
withPrintWriter方法不同于语言的内建控制结构,如if和while,在于大括号之间的代码带了参数。withPrintWriter方法需要一个类型为PrintWriter的参数。这个参数以“writer =>”方式显示出来: withPrintWriter(file) { writer => writer.println(new java.util.Date) }
然而如果你想要实现某些更像if或while的东西,根本没有值要传入大括号之间的代码,那该怎么做呢?为了解决这种情况,Scala提供了叫名参数。
为了举一个有现实意义的例子,请设想你需要实现一个称为myAssert的断言架构。
myAssert函数将带一个函数值做输入并参考一个标志位来决定该做什么。如果标志位被设置了,myAssert将调用传入的函数并证实其返回true。如果标志位被关闭了,myAssert将安静地什么都不做。 如果没有叫名参数,你可以这样写myAssert:
var assertionsEnabled = true
def myAssert(predicate: () => Boolean) =
if (assertionsEnabled && !predicate())
throw new AssertionError
这个定义是正确的,但使用它会有点儿难看:
myAssert(() => 5 > 3) 你或许很想省略函数文本里的空参数列表和=>符号,写成如下形式: myAssert(5 > 3) // 不会有效,因为缺少() => 叫名函数恰好为了实现你的愿望而出现。要实现一个叫名函数,要定义参数的类型开始于=>而不是() =>。例如,你可以通过改变其类型,“() => Boolean”,为“=> Boolean”,把myAssert的predicate参数改为叫名参数。代码9.5展示了它的样子:
def byNameAssert(predicate: => Boolean) =
if (assertionsEnabled && !predicate)
throw new AssertionError
使用叫名参数 现在你可以在需要断言的属性里省略空的参数了。使用byNameAssert的结果看上去就好象使用了内建控制结构: byNameAssert(5 > 3) 叫名类型中,空的参数列表,(),被省略,它仅在参数中被允许。没有什么叫名变量或叫名字段这样的东西。
组合与继承
组合意味着一个类持有另一个的参考,使用参考类帮助实现任务。继承是超类/子类的关系。除此之外,我们还将讨论抽象类,无参数方法,扩展类,重载方法和字段,参数化字段,调用超类构造器,多态和动态绑定,final成员和类,还有工厂对象和方法。
抽象类
这个类里,contents被声明为没有实现的方法。换句话说,方法是类Element的抽象:abstract成员。具有抽象成员的类本身必须被声明为抽象的,只要在class关键字之前加上abstract修饰符即可:
abstract class Element {
def contents: Array[String]
}
总结起来,Scala里定义不带参数也没有副作用的方法为无参数方法,也就是说,省略空的括号,是鼓励的风格。另一方面,永远不要定义没有括号的带副作用的方法,因为那样的话方法调用看上去会像选择一个字段。这样你的客户看到了副作用会很奇怪。相同地,当你调用带副作用的函数,请确信写这个调用的时候包括了空的括号。另一种考虑这个问题的方式是,如果你调用的函数执行了操作,使用括号,但如果仅提供了对某个属性的访问,省略括号。
特质
特质:trait是Scala里代码复用的基础单元。特质封装了方法和字段的定义,并可以通过混入到类中重用它们。不像类的继承那样,每个类都只能继承唯一的超类,类可以混入任意个特质。特质最常用到的两种方式:拓宽瘦接口为胖接口和定义可堆叠的改变。