作者Chikei ( )
看板Translate-CS
标题[翻译] 给Java程式设计师的Scala入门教学
时间Tue Oct 22 14:51:07 2013
译自
http://docs.scala-lang.org/tutorials/scala-for-java-programmers.html
因为原文是用markdown撰写,译文也直接用markdown格式撰写
github好读版(?)
https://github.com/chikei/scala.github.com/blob/zh_TW/zh/tutorials/
scala-for-java-programmers.md
~~~正文分隔线~~~
## 介绍
此教学将对Scala语言以及编译器做一个简易的介绍。设定的读者为具有程设经验且想
要看Scala功能概要的人。内文假设读者有着基本、特别是Java上的物件导向程设知识。
## 第一个例子
这边用标准的 *Hello world* 程式作为第一个例子。虽然它很无趣,可是这让我们在
仅用少量语言下演示Scala工具。程式如下:
object HelloWorld {
def main(args: Array[String]) {
println("Hello, world!")
}
}
Java程式员应该对这个程式的结构感到熟悉:有着一个 `main` 函式,该函式接受一
个字串阵列引数,也就是命令列引数;函式内容为呼叫已定义好的函式 `println` 并
用Hello world字串当引数。 `main` 函式没有回传值(它是程序函式)。因此并不需要
宣告回传型别。
Java程式员不太熟悉的是包着 `main` 函式的 `object` 宣告。这种宣告引入我们一
般称之 *Singleton* 的东西,也就是只有一个实体的类别。所以上面的宣告同时宣告
了一个 `HelloWorld` 类别跟一个这类别的实体,也叫做 `HelloWorld`。该实体会在
第一次被使用到的时候即时产生。
眼尖的读者可能已经注意到这边 `main` 函式的宣告没有带着 `static`。这是因为
Scala没有静态成员(函式或资料栏)。Scala程式员将这成员宣告在单实例物件中,而
不是定义静态成员。
### 编译这例子
我们用Scala编译器 `scalac`来编译这个例子。`scalac` 就像大多数的编译器一样,
它接受原码档当引数,并接受额外的选项,然後产生一个或多个物件档。它产出的物
件档为标准的Java class档案。
如果我们将上面的程式存成 `HelloWorld.scala` 档,编译的指令为( `>` 是提示字
元,不用打):
> scalac HelloWorld.scala
这会在现在的目录产生一些class档案。其中一个会叫做 `HelloWorld.class`,里面
包含着可被 `scala` 直接执行的类别。
### 执行范例
一旦编译过後,Scala程式可以用 `scala` 指令执行。它的使用方式非常的像执行
Java程式的 `java` 指令,并且接受同样的选项。上面的范例可以用以下的指令来执
行并得到我们预期的输出:
> scala -classpath . HelloWorld
Hello, world!
## 与Java互动
Scala的优点之一是它非常的容易跟Java程式码沟通。预设汇入所有 `java.lang` 底
下之类别,其他类别则需要明确汇入。
让我们看个展示这点的范例。取得现在的日期并根据某个特定的国家排版成该国的格
式,如法国。
Java的标准函式库定义了一些有用的工具类别,如 `Date` 跟 `DateFormat`。因为
Scala可以无缝的跟Java互动,这边不需要以Scala实作同样的类别--我们只需要汇入
对应的Java套件:
import java.util.{Date, Locale}
import java.text.DateFormat
import java.text.DateFormat._
object FrenchDate {
def main(args: Array[String]) {
val now = new Date
val df = getDateInstance(LONG, Locale.FRANCE)
println(df format now)
}
}
Scala的汇入陈述式跟Java的非常像,但更为强大。如第一行,同一个package下的多
个类别可以用大括号括起来一起导入。另外一个差别是,当要汇入套件或类别下所有
的名称时,用下标(`_`)而不是星号(`*`)。这是因为星号是一个合法的Scala识别符号
(如函式名称)。
所以第三行的陈述式导入所有 `DateFormat` 类别的成员。这让静态函式
`getDateInstance` 跟静态资料栏 `LONG` 可直接被使用。
在 `main` 函式中我们先创造一个Java的 `Date` 类别实体,该实体预设拥有现在的
日期。接下来用 `getDateInstance` 函式定义日期格式。最後根据地区化的
`DateFormat` 实体对现在日期排版格式化并印出。最後一行展现了一个Scala有趣的
特点。只需要一个引数的函式可以用中缀语法呼叫。就是说,这个表示式
df format now
是比较不详细版本的这个表示式
df.format(now)
这点也许看起来只是一个小小的语法细节,但是他有着重要的後果,其中一个将会在
下一节做介绍。
让我们以,Scala可以直接继承Java类别跟实作Java介面,来为这节做结尾。
## 万物皆物件
Scala是一个纯粹的物件导向语言,这句话的意思是说,*所有东西*都是物件,包括数
字、函式。因为Java将基本型别跟参照型别分开,而且没有办法像操作变数一样操作
函式,从这角度来看Scala跟Java是不同的。
### 数字是物件
因为数字是物件,他们也有函式。事实上,一个像底下的算数表示式:
1 + 2 * 3 / x
只有使用函式呼叫,因为像前一节一样,该式等价於
(1).+(((2).*(3))./(x))
这也表示着 `+`、`*` 之类的在Scala里是合法的识别符号。
因为Scala的词法分析器对於符号采用最长匹配,在第二版的表示式当中,那些括号是
必要的。也就是说分析器会把这个表示式:
1.+(2)
拆成 `1.`、`+`、`2` 这三个符号。会这样拆分是因为 `1.` 既是合法匹配同时又比
`1` 长。 `1.` 会被解释成文字 `1.0`,使得他被视为 `Double` 而不是 `Int`。把
表示式写成:
(1).+(2)
可以避免 `1` 被解释成 `Double`。
### 函式是物件
可能令Java程式员更为惊讶的会是,Scala中函式也是物件。因此,将函式当做引数传
递、把它们存入变数、从其他函式返回函式都是可能的。能够像操作变数一样的操作
函式这点是*函数编程*这一非常有趣的程设典范的基石之一。
为何把函式当做变数一样的操作会很有用呢,让我们考虑一个定时函式,它的功能是
每秒执行一些动作。我们要怎麽将这动作传给它?最直接的便是将这动作视为函式传
入。应该有不少的程式员对这种简单传递函式的行为很熟悉:通常在使用者介面相关
的程式上,用以注册一些当事件发生时被呼叫的回呼函式。
在接下来的程式中,定时函式叫做 `oncePerSecond` ,它接受一个回呼函式做参数。
该函式的型别被写作 `() => Unit` ,这个型别便是所有无引数且无返回值的函式的
型别( `Unit` 这个型别就像是C/C++的 `void` )。此程式的主函式只是呼叫定时函式
并带入回呼函式,回呼函式输出一句话到终端上。也就是说这个程式会不断的每秒输
出一次"time flies like an arrow"。
object Timer {
def oncePerSecond(callback: () => Unit) {
while (true) { callback(); Thread sleep 1000 }
}
def timeFlies() {
println("time flies like an arrow...")
}
def main(args: Array[String]) {
oncePerSecond(timeFlies)
}
}
值得注意的是,这边输出的时候我们使用Scala的函式 `println`,而不是
`System.out` 里的函式。
#### 匿名函式
这程式还有改进的空间。第一点,函式 `timeFlies` 只是为了能够被传递进
`oncePerSecond` 而定义的。赋予一个只被使用一次的函式名字似乎是没有必要的,
最好能够在传入 `oncePerSecond` 时构造出这个函式。Scala可以藉由*匿名函式*来
达到这点。利用匿名函式的改进版本程式如下:
object TimerAnonymous {
def oncePerSecond(callback: () => Unit) {
while (true) { callback(); Thread sleep 1000 }
}
def main(args: Array[String]) {
oncePerSecond(() =>
println("time flies like an arrow..."))
}
}
这例子中的右箭头 `=>` 告诉我们有一个匿名函式,右箭头将函式引数跟函式内容分
开。这个例子中,在箭头左边那组空的括号告诉我们引数列是空的。函式内容则是跟
先前的 `timeFlies` 里一样。
## 类别
之前已讲过,Scala是一个物件导向语言,因此它有着类别的概念。(更精确的说,的
确有一些物件导向语言没有类别的概念,但是Scala不是这类)。Scala宣告类别的语法
跟Java很接近。一个重要的差别是,Scala的类别可以有参数。这边用底下复数的定义
来展示:
class Complex(real: Double, imaginary: Double) {
def re() = real
def im() = imaginary
}
这个复数类别接受两个参数,分别为实跟虚部。在创造 `Complex` 的实体时,必须传
入这些参数: `new Complex(1.5, 2.3)`。这个类别有两个函式分别叫做 `re` 跟
`im` 让我们取得这两个部分。
值得注意的是,这两个函式的回传值并没有被明确的给定。编译器将会自动的推断,
它会查看这些函式的右侧并推导出这两个函式都会回传型别为 `Double` 的值。
编译器并不一定每次都能够推断出型别,而且很不幸的是我们并没有简单的规则分辨
哪种情况能推断,哪种情况不能。因为当编译器无法推断未明确给定的型别时它会回
报错误,实务上这通常不是问题。Scala的初学者在遇到那些看起来很简单就能推导出
型别的情况时,应该尝试着忽略型别宣告并看看编译器是不是也觉得可以推断。多尝
试几次之後程式员应该能够体会到何时忽略型别、何时该明确指定。
### 无引数函式
函式 `re`、`im` 有个小问题,为了呼叫函式,我们必须在函式名称後面加上一对空
括号,如这个例子:
object ComplexNumbers {
def main(args: Array[String]) {
val c = new Complex(1.2, 3.4)
println("imaginary part: " + c.im())
}
}
最好能够在不需要加括号的情况下取得实虚部,这样便像是在取资料栏。Scala完全可
以做到这件事,需要的只是在定义函式的时候*不要定义引数*。这种函式跟零引数函
式是不一样的,不论是定义或是呼叫,它们都没有括号跟在名字後面。我们的
`Complex` 可以改写成:
class Complex(real: Double, imaginary: Double) {
def re = real
def im = imaginary
}
### 继承与覆写
Scala中所有的类别都继承自一个母类别。像前一节的 `Complex` 这种没有指定的例
子,Scala会暗中使用 `scala.AnyRef`。
Scala中可以覆写继承自母类别的函式。但是为了避免意外的覆写,必须加上
`override` 修饰字来明确表示要覆写函式。我们以覆写 `Complex` 类别中来自
`Object` 的 `toString` 作为范例。
class Complex(real: Double, imaginary: Double) {
def re = real
def im = imaginary
override def toString() =
"" + re + (if (im < 0) "" else "+") + im + "i"
}
## Case Class跟模式匹配(pattern matching)
树是常见的资料结构。如:解译器跟编译器内部常见的表示程式方式便是树;XML文件
是树;还有一些容器是根基於树,如红黑树。
接下来我们会藉由一个小型计算机程式来看看Scala是如何呈现并操作树。这个程式的
功能将会是足以操作简单、仅含有整数、常数、变数跟加法的算术式。`1+2` 跟
`(x+x)+(7+y)`为两个例子。
我们得先决定这种表示式的表示法。最自然表示法便是树,其中节点是操作、叶点是
值。
Java中我们会将这个树用一个抽象母类别表示,然後每种节点跟叶点分别有各自的实
际类别。在函数边程里会用代数资料类型。Scala则是提供了介於两者之间的
*case class*。将它运用在这边会是如下:
abstract class Tree
case class Sum(l: Tree, r: Tree) extends Tree
case class Var(n: String) extends Tree
case class Const(v: Int) extends Tree
`Sum`、`Var`、`Const` 类别定义成case class代表着它们跟一般类别有所差别:
- 在创建类别实体时不需要用 `new`(也就是说我们可以写 `Const(5)`,而不是
`new Const(5)`)。
- 对应所有的建构式参数,Scala会自动定义对应的取值函式(即,对於 `Const` 类别
的实体,我们可以直接用 `c.v` 来取得建构式中的 `v` 参数)。
- `equals` 跟 `hashCode` 会有预设的定义。该定义会根据实体的*结构*而不是个别
实体的识别来运作。
- `toString` 会有预设的定义。会印出"原始型态"(即,`x+1` 的树会被印成
`Sum(Var(x),Const(1))`)。
- 这些类别的实体可以藉由*模式匹配*来拆解。
现在我们有了算术表示式的资料型别,可以开始定义各种运算。我们将从一个可以在
*环境*内对运算式求值的函式起头。环境的用处是赋值给变数。举例来说,运算式
`x+1` 在一个将 `x` 赋与 `5` 的环境(写作 `{ x -> 5 }` )下求值会得到 `6`。
因此我们需要一个表示环境的方法。当然我们可以用一些像是杂凑表的关连性资料结
构,但是我们也可以直接用函式!环境就只是一个将值对应到(变数)名称的函式。之
前提到的环境 `{ x -> 5 }` 在Scala中可以简单的写作:
{ case "x" => 5 }
这串符号定义了一个当输入是字串 `"x"` 的时候回传整数 `5`,其他输入则是用例外
表示失败的函式。
开始实作之前,让我们先给环境型别一个名字。当然,我们可以直接用
`String => Int`,但是给这型别名字可以让我们简化程式,而且在未来要改动的时
候较为简便。在Scala我们是这样表示这件事:
type Environment = String => Int
於是型别 `Environment` 便可以当做输入 `String` 回传 `Int` 函式的型别的代名。
现在我们可以给出求值函式的实作。概念上非常的简单:两个表示式和的值是两个表
示式值的和;变数的值直接从环境取值;常数的值就是常数本身。表示这些在Scala里
并不困难:
def eval(t: Tree, env: Environment): Int = t match {
case Sum(l, r) => eval(l, env) + eval(r, env)
case Var(n) => env(n)
case Const(v) => v
}
这个求值函式藉由对树 `t` 做*模式匹配*来求值。上述实作的意思应该从直观上便很
明确:
1. 首先检查树 `t` 是否为 `Sum`,如果是的话将左/右侧子树绑定到新变数 `l`/`r`
,然後再对箭头後方的表示式求值;这一个表示式可以使用(而且这边也用到)根据
箭头左侧模式所绑定的变数,也就是 `l` 跟 `r`,
2. 如果第一个检查失败,也就是说树不是 `Sum`,接下来检查 `t` 是否为 `Var`,
如果是的话将 `Var` 所带的名称绑定到变数 `n` 并求值右侧的表示式,
3. 如果第二个检查也失败,表示树不是 `Sum` 也不是 `Var`,那便检查是不是
`Const`,如果是的话将 `Const` 所带的名称绑定到变数 `v` 并求值右侧的表
示式,
4. 最後,如果全部的检查都失败,会丢出例外表示匹配失败;这只会在有更多
`Tree` 的子类别的情况下发生。
如上,模式匹配基本上就是尝试将一个值对一系列的模式做匹配,并在一个模式成功
的匹配时抽取并命名该值的各部分,最後对一些程式码求值,而这些程式码通常会利
用被命名到的部位。
一个经验丰富的物件导向程式员也许会疑惑为何我们不将 `eval` 定义成 `Tree` 类
别跟子类的*函式*。由於Scala允许在case class中跟一般的类别一样定义函式,事实
上我们可以这样做。要用模式匹配或是函式只是品味的问题,但是这会对扩充性有重
要的影响。
- 当使用函式的时候,只要定义新的 `Tree` 子类便新增新的节点,相当的容易。另
一方面,增加新的操作需要修改所有的子类,很麻烦。
- 当使用模式匹配的时候情况则反过来:增加新节点需要修改所有对树做模式匹配的
函式将新节点纳入考虑;增加新的操作则很简单,定义新的函式就好。
让我们定义新的操作以更进一步的探讨模式匹配:对符号求导数。读者们可能还记得
这个操作的规则:
1. 和的导数是导数的和
2. 如果是对变数 `v` 取导数,变数 `v` 的导数是1,不然就是0
3. 常数的导数是0
这些规则几乎可以从字面上直接翻成Scala程式码:
def derive(t: Tree, v: String): Tree = t match {
case Sum(l, r) => Sum(derive(l, v), derive(r, v))
case Var(n) if (v == n) => Const(1)
case _ => Const(0)
}
这个函式引入两个关於模式匹配的新观念。首先,变数的 `case` 运算式有一个
*看守*,也就是 `if` 关键字之後的表示式。除非表示式求值为真,不然这个看守会
让匹配直接失败。在这边是用来确定我们只在取导数变数跟被取导数变数名称相同时
才回传常数 `1`。第二个新特徵是可以匹配任何值的*万用字元* `_`。
我们还没有探讨完模式匹配的全部功能,不过为了让这份文件保持简短,先就此打住
。我们还是希望能看到这两个函式在真正的范例如何作用。因此让我们写一个简单的
`main` 函数,对表示式 `(x+x)+(7+y)` 做一些操作:先在环境
`{ x -> 5, y -> 7 }` 下计算结果,然後在对 `x` 接着对 `y` 取导数。
def main(args: Array[String]) {
val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
val env: Environment = { case "x" => 5 case "y" => 7 }
println("Expression: " + exp)
println("Evaluation with x=5, y=7: " + eval(exp, env))
println("Derivative relative to x:\n " + derive(exp, "x"))
println("Derivative relative to y:\n " + derive(exp, "y"))
}
执行这程式,得到预期的输出:
Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y)))
Evaluation with x=5, y=7: 24
Derivative relative to x:
Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0)))
Derivative relative to y:
Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1)))
研究这输出我们可以发现,取导数的结果应该在输出前更进一步的化简。用模式匹配
实作一个基本的化简函数是一个很有趣(但是意外的棘手)的问题,在这边留给读者当
练习。
## 特质(Traits)
除了由母类别继承行为以外,Scala类别还可以从一或多个*特质*导入。
对一个Java程式员最简单去理解特质的方式应该是视他们为带有实作的介面。在Scala
里,当一个类别继承特质时,他实作了该特质的介面并继承所有特质带有的功能。
为了理解特质的用处,让我们看一个经典范例:有序物件。大部分的情况下,一个类
别所产生出来的物件之间可以互相比较大小是很有用的,如排序他们。在Java里可比
较大小的物件实作 `Comparable` 介面。在Scala中藉由定义等价於 `Comparable`
的特质 `Ord`,我们可以做的比Java稍微好一点。
当在比较物件的大小时,有六个有用且不同的谓词(predicate):小於、小於等於、等
於、不等於、大於等於、大於。但是把六个全部都实作很烦,尤其是当其中有四个可
以用剩下两个表示的时候。也就是说,(举例来说)只要有等於跟小於谓词,我们就可
以表示其他四个。在Scala中这些观察可以很漂亮的用下面的特质宣告呈现:
trait Ord {
def < (that: Any): Boolean
def <=(that: Any): Boolean = (this < that) || (this == that)
def > (that: Any): Boolean = !(this <= that)
def >=(that: Any): Boolean = !(this < that)
}
这份定义同时创造了一个叫做 `Ord` 的新型别,跟Java的 `Comparable` 介面有着同
样的定位,且给了一份以第一个抽象谓词表示剩下三个谓词的预设实作。因为所有的
物件预设都有一份等於跟不等於的谓词,这边便没有定义。
上面使用了一个 `Any` 型别,在Scalla中这个型别是所有其他型别的母型别。因为它
同时也是基本型别如 `Int`、`Float`的母型别,可以将其视为更为一般化的Java
`Object` 型别。
因此只要定义测试相等性跟劣性的谓词,并且加入 `Ord`,就可以让一个类别的物件
们互相比较大小。让我们实作一个表示阳历日期的 `Date` 类别来做为例子。这种日
期是由日、月、年组成,我们将用整数来表示这三个资料。因此我们可以定义 `Date`
类别为:
class Date(y: Int, m: Int, d: Int) extends Ord {
def year = y
def month = m
def day = d
override def toString(): String = year + "-" + month + "-" + day
这边要注意的是宣告在类别名称跟参数之後的 `extends Ord`。这个语法宣告了
`Date` 继承 `Ord` 特质。
然後我们重新定义来自 `Object` 的 `equals` 函式好让这个类别可以正确的根据每
个资料栏来比较日期。因为在Java中 `equals` 预设实作是直接比较实际物件本身,
并不能在这边用。於是我们有下面的实作:
override def equals(that: Any): Boolean =
that.isInstanceOf[Date] && {
val o = that.asInstanceOf[Date]
o.day == day && o.month == month && o.year == year
}
这个函式使用了预定义函式 `isInstanceOf` 跟 `asInstanceOf`。`isInstanceOf`
对应到Java的 `instanceof` 运算子,只在当使用它的物件的型别跟给定型别一样时
传回真。 `asInstanceOf` 对应到Java的转型运算子,如果物件是给定型别的实体,
该物件就会被视为给定型别,不然就会丢出 `ClassCastException` 。
最後我们需要定义测试劣性的谓词如下。
def <(that: Any): Boolean = {
if (!that.isInstanceOf[Date])
error("cannot compare " + that + " and a Date")
val o = that.asInstanceOf[Date]
(year < o.year) ||
(year == o.year && (month < o.month ||
(month == o.month && day < o.day)))
}
这边使用了另外一个预定义函式 `error`,它会丢出带着给定错误讯息的例外。这便
完成了 `Date` 类别。这个类别的实体可被视为日期或是可比较物件。而且他们通通
都定义了之前所提到的六个比较谓词: `equals`跟`<` 直接出现在类别定义当中,其
他的则是继承自 `Ord` 特质。
特质在其他场合也有用,不过详细的探讨它们的用途并不在本文件目标内。
## 泛型
在这份教学里,我们最後要探讨的Scala特性是泛型。Java程式员应该相当的清楚在
Java 1.5之前缺乏泛型所导致的问题。
泛型指的是能够将型别也作为程式参数的功能。举例来说,当程式员在为链结串列写
函式库的时候,他必须决定串列的元素型别为何。由於这串列是要在许多不同的场合
使用,不可能决定串列的元素型别为如 `Int` 一类。这样限制太多。
Java程式员采用所有物件的母类别 `Object`。这个解决办法并不理想,一方面这并不
能用在基础型别(`int`、`long`、`float`之类),再来这表示必须靠程式员手动加入
大量的动态转型。
Scala藉由可定义泛型类别(跟函式)来解决这问题。让我们藉由最简单的类别容器来检
视这点:参照,它可以是空的或者指向某型别的物件。
class Reference[T] {
private var contents: T = _
def set(value: T) { contents = value }
def get: T = contents
}
类别 `Reference` 带有一个型别参数 `T`,这个参数会是容器内元素的型别。此型别
被用做 `contents` 变数的型别、 `set` 函式的引数型别、 `get` 函式的回传型别。
上面的程式码使用的Scala的变数语法,应该不需要过多的解释。值得注意的是赋与该
变数的初始值是 `_`,该语法表示预设值。数值型别的预设值是0,`Boolea8n` 型别
是伪, `Unit` 型别是 `()` ,所有的物件型别是 `null`。
为了使用 `Reference` 类型,我们必须指定 `T`,也就是这容器所包容的元素型别。
举例来说,创造并使用该容器来容纳整数,我们可以这样写:
object IntegerReference {
def main(args: Array[String]) {
val cell = new Reference[Int]
cell.set(13)
println("Reference contains the half of " + (cell.get * 2))
}
}
如例子中所展现,并不需要先将 `get` 函式所回传的值转型便能当做整数使用。同时
因为被宣告为储存整数,也不可能存除了整数以外的东西到这一个容器中。
## 结语
本文件对Scala语言做了快速的概览并呈现一些基本的例子。对Scala有更多兴趣的读
者可以阅读有更多进阶范例的 *Scala By Example*,并在需要的时候参阅
*Scala Language Specification*。
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 211.72.92.133