JAVA程序员的SCALA教程
作者:Michel Schinz和Philipp Haller
介绍
本文档简要介绍了Scala语言和编译器。它适用于已经拥有一些编程经验并希望了解他们可以使用Scala做什么的人。假定了面向对象编程的基本知识,特别是在Java中。
第一个例子
作为第一个例子,我们将使用标准的Hello world程序。它不是很吸引人,但可以很容易地演示Scala工具的使用,而不必过多地了解语言。以下是它的外观:
object HelloWorld {
def main(args: Array[String]) {
println("Hello, world!")
}
}
Java程序员应该熟悉这个程序的结构:它包含一个调用main
命令行参数的方法,一个字符串数组作为参数; 此方法的主体包含对预定义方法的单个调用println
,其中友好问候语作为参数。该main
方法不返回值(它是一个过程方法)。因此,没有必要声明返回类型。
Java程序员不太熟悉的是object
包含该main
方法的声明。这样的声明引入了通常所说的单例对象,即具有单个实例的类。因此,上面的声明声明了一个被调用的类HelloWorld
和该类的一个实例,也称为HelloWorld
。该实例是在第一次使用时按需创建的。
精明的读者可能已经注意到该main
方法未在static
此处声明。这是因为Scala中不存在静态成员(方法或字段)。Scala程序员不是定义静态成员,而是在单例对象中声明这些成员。
编译示例
为了编译示例,我们使用scalac
Scala编译器。scalac
像大多数编译器一样工作:它将源文件作为参数,可能是一些选项,并生成一个或多个目标文件。它生成的目标文件是标准的Java类文件。
如果我们将上述程序保存在一个名为的文件中 HelloWorld.scala
,我们可以通过发出以下命令来编译它(大于号>
表示shell提示符,不应该键入):
> scalac HelloWorld.scala
这将在当前目录中生成一些类文件。其中一个将被调用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的import语句看起来与Java相当,但它更强大。可以从同一个包中导入多个类,方法是将它们用大括号括起来,就像在第一行一样。另一个区别是,在导入包或类的所有名称时,使用下划线字符(_
)而不是星号(*
)。这是因为星号是有效的Scala标识符(例如方法名称),我们稍后会看到。
因此,第三行上的import语句将导入DateFormat
该类的所有成员。这使静态方法 getDateInstance
和静态字段LONG
直接可见。
在main
方法内部,我们首先创建一个Java Date
类的实例, 默认情况下包含当前日期。接下来,我们使用getDateInstance
之前导入的静态方法定义日期格式。最后,我们打印根据本地化DateFormat
实例格式化的当前日期。最后一行显示了Scala语法的一个有趣属性。采用一个参数的方法可以与中缀语法一起使用。也就是说,表达
df format now
写作表达式只是另一种略显冗长的方式
df.format(now)
这似乎是一个较小的句法细节,但它有重要的后果,其中一个将在下一节中探讨。
在结束本节关于与Java集成的部分时,应该注意的是,也可以从Java类继承并直接在Scala中实现Java接口。
一切都是对象
Scala是一种纯粹的面向对象语言,因为 一切都是对象,包括数字或函数。它在这方面与Java不同,因为Java将原始类型(例如boolean
和int
)与引用类型区分开来,并且不允许将函数作为值来操作。
数字是对象
由于数字是对象,因此它们也有方法。事实上,算术表达式如下:
1 + 2 * 3 / x
由方法调用组成,因为它等同于下面的表达式,正如我们在上一节中看到的那样:
(1).+(((2).*(3))./(x))
这也意味着+
,*
等在斯卡拉有效的标识符。
第二个版本中数字的括号是必要的,因为Scala的词法分析器使用最长的匹配规则作为标记。因此,它会破坏以下表达式:
1.+(2)
入令牌1.
,+
和2
。选择此标记化的原因是因为1.
比较长的有效匹配1
。令牌1.
被解释为文字1.0
,使其成为一个Double
而不是一个Int
。将表达式写为:
1.+(2)
防止1
被解释为Double
。
功能是对象
也许Java程序员更令人惊讶,函数也是Scala中的对象。因此,可以将函数作为参数传递,将它们存储在变量中,并从其他函数返回它们。这种将函数作为值进行操作的能力是一种非常有趣的编程范式(称为函数式编程)的基石之一 。
作为将函数用作值的有用原因的一个非常简单的例子,让我们考虑一个计时器函数,其目的是每秒执行一些操作。我们如何将动作传递给它?在逻辑上,作为一种功能。这种非常简单的函数传递应该为许多程序员所熟悉:它通常用在用户界面代码中,用于注册在某些事件发生时调用的回调函数。
在下面的程序中,调用timer函数 oncePerSecond
,并获得一个回调函数作为参数。这个函数的类型是写的() => Unit
,是所有函数的类型,它们不带参数并且什么都不返回(类型 Unit
与void
C / C ++ 类似)。该程序的主要功能是通过回调调用此定时器功能,该回调在终端上打印一个句子。换句话说,这个程序每秒钟无休止地打印句子“时间飞得像箭头”。
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)
}
}
请注意,为了打印字符串,我们使用预定义的方法println
而不是使用的方法System.out
。
匿名函数
虽然这个程序很容易理解,但可以稍微改进一下。首先,请注意该函数timeFlies
仅定义为稍后传递给oncePerSecond
函数。必须命名只使用过一次的那个函数,这似乎是不必要的,实际上能够像传递给它一样构造这个函数真的很好oncePerSecond
。这在Scala中可以使用匿名函数,这正是:没有名称的函数。使用匿名函数而不是timeFlies的我们的计时器程序的修订版看起来像这样:
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
类有两个参数,它们是复合体的实部和虚部。创建类的实例时必须传递这些参数Complex
,如下所示:new Complex(1.5, 2.3)
。该类包含两个名为re
and的方法,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.AnyRef
则隐式使用前一节的示例 。
可以覆盖从Scala中的超类继承的方法。但是,必须明确指定方法使用override
修饰符覆盖另一个方法,以避免意外覆盖。作为一个例子,我们的Complex
类可以通过重新定义toString
继承自的方法来扩充Object
。
class Complex(real: Double, imaginary: Double) {
def re = real
def im = imaginary
override def toString() =
"" + re + (if (im < 0) "" else "+") + im + "i"
}
案例类和模式匹配
通常出现在程序中的一种数据结构是树。例如,解释器和编译器通常将程序内部表示为树; XML文档是树; 几种容器都是以树木为基础,如红黑树。
我们现在将研究如何通过一个小型计算器程序在Scala中表示和操作这些树。该程序的目的是操纵由和,整数常量和变量组成的非常简单的算术表达式。这种表达的两个例子是 1+2
和(x+x)+(7+y)
。
我们首先必须决定这种表达的表示。最自然的是树,其中节点是操作(这里是添加),叶子是值(这里是常量或变量)。
在Java中,这样的树将使用树的抽象超类来表示,并且每个节点或叶使用一个具体的子类。在函数式编程语言中,可以使用代数数据类型来实现相同的目的。Scala提供了案例类的概念, 它们介于两者之间。以下是它们如何用于为我们的示例定义树的类型:
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类意味着他们从标准类的区别在以下几个方面:
- 该
new
关键字不是强制性的,以创建这些类(即,一个可以写入的情况下Const(5)
,而不是new Const(5)
), - 为构造函数参数自动定义getter函数(即,可以通过写入获取
v
某个c
类实例的构造函数参数的值),Const
c.v
- 对于方法的默认定义
equals
和hashCode
设置,在其工作结构的情况下,并没有对他们的身份, -
toString
提供了方法的默认定义,并以“源表单”(例如,表达式x+1
打印的树打印为Sum(Var(x),Const(1))
)打印该值 , - 这些类的实例可以通过模式匹配进行分解, 如下所示。
现在我们已经定义了数据类型来表示我们的算术表达式,我们可以开始定义操作来操作它们。我们将从一个函数开始,在某些环境中评估表达式 。环境的目的是为变量赋值。例如,在x+1
将值5
与变量x
(写入) 相关联的环境中计算的表达式作为结果{ 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
。直观地说,上述定义的含义应该是清楚的:
- 它首先检查树
t
是否为aSum
,如果是,它将左子树绑定到一个名为的新变量l
,将右子树绑定到一个被调用的变量r
,然后继续按箭头的方式评估表达式; 该表达式可以(并且不会)使用由出现在箭头的左侧,即,图案绑定的变量,l
并且r
, - 如果第一次检查没有成功,也就是说,如果树不是a
Sum
,则继续检查是否t
为aVar
; 如果是,它将Var
节点中包含的名称绑定到变量n
并继续使用右侧表达式, - 如果第二次检查也失败,即if
t
既不是aSum
也不是aVar
,它检查它是否为aConst
,如果是,则将Const
节点中包含的值绑定到变量v
并继续右侧, - 最后,如果所有检查都失败,则会引发异常以指示模式匹配表达式的失败; 只有
Tree
在声明了更多的子类时,才会发生这种情况。
我们看到模式匹配的基本思想是尝试将值与一系列模式匹配,并且只要模式匹配,提取并命名值的各个部分,最后评估一些通常使用这些模式的代码。命名部分。
作为一名经验丰富的面向对象的编程人员可能会问,为什么我们没有定义eval
的方法类Tree
和它的子类。我们本可以做到这一点,因为Scala允许在案例类中使用方法定义,就像在普通类中一样。因此,决定是否使用模式匹配或方法是一种品味问题,但它对可扩展性也有重要影响:
- 在使用方法时,很容易添加一种新节点,因为这可以通过
Tree
为它定义一个子类来完成; 另一方面,添加一个操作树的新操作是繁琐的,因为它需要修改所有子类Tree
, - 在使用模式匹配时,情况正好相反:添加新类型的节点需要修改在树上进行模式匹配的所有函数,以考虑新节点; 另一方面,通过将其定义为独立函数,添加新操作很容易。
为了进一步探索模式匹配,让我们在算术表达式上定义另一个操作:符号派生。读者可能会记住有关此操作的以下规则:
- 和的导数是导数的总和,
- 某个变量的导数
v
是1,如果v
是相对于该导数发生的变量,则为0,否则为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
变量的表达式有一个guard,一个跟在if
关键字后面的表达式。除非其表达式为真,否则此保护可防止模式匹配成功。这里它用于确保1
只有在派生变量的名称与派生变量相同时才返回常量v
。这里使用的模式匹配的第二个新特性是写入的通配符_
,它是匹配任何值的模式,而不给它命名。
我们还没有探索模式匹配的全部功能,但我们将在此处停下来以保持此文档的简短性。我们仍然希望看到上面两个函数如何在一个真实的例子上执行。为了该目的,让我们编写一个简单的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"))
}
您将需要包装的Environment
类型和eval
,derive
以及 main
在方法Calc
编译前的对象。执行此程序,我们得到预期的输出:
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)))
通过检查输出,我们看到衍生的结果应该在呈现给用户之前简化。使用模式匹配定义基本的简化函数是一个有趣(但令人惊讶的棘手)问题,留给读者练习。
性状
除了从超类继承代码之外,Scala类还可以从一个或多个特征导入代码。
也许Java程序员理解特征的最简单方法是将它们视为可以包含代码的接口。在Scala中,当一个类继承自trait时,它实现了该trait的接口,并继承了trait中包含的所有代码。
为了看到特征的有用性,让我们看一个经典的例子:有序对象。能够比较给定类之间的对象(例如对它们进行排序)通常很有用。在Java中,可比较的对象实现Comparable
接口。在Scala中,通过定义我们Comparable
称之为特征的 等价物,我们可以比Java更好一些Ord
。
比较对象时,六个不同的谓词可能很有用:更小,更小或相等,相等,不相等,更大或更大,以及更大。然而,定义所有这些都是挑剔的,特别是因为这六个中的四个可以使用剩下的两个来表达。也就是说,给定相等和较小的谓词(例如),可以表达其他谓词。在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
,其上面使用是这是一种超级型所有其他类型Scala中的类型。它可以被看作是Java的更一般的版本Object
类型,因为它也是一个超级类型的基本类型,如Int
,Float
等。
为了使类的对象具有可比性,因此足以定义测试相等性和低劣性的谓词,并在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
特征。
然后,我们重新定义equals
继承自的方法, Object
以便通过比较各个字段来正确比较日期。默认实现equals
不可用,因为在Java中它会物理地比较对象。我们得出以下定义:
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
运算符,当且仅当应用它的对象是给定类型的实例时才返回true。第二个asInstanceOf
对应于Java的强制转换操作符:如果对象是给定类型的实例,则将其视为此类,否则ClassCastException
抛出a。
最后,定义的最后一个方法是测试劣势的谓词,如下所示。它使用另一个方法,error
从package对象scala.sys
中抛出给定错误消息的异常。
def <(that: Any): Boolean = {
if (!that.isInstanceOf[Date])
sys.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)))
}
这样就完成了Date
类的定义。可以将此类的实例视为日期或类似对象。此外,它们都限定上面提到的六个比较谓词:equals
和<
因为它们直接出现在定义Date
的类,和别人是因为它们被从遗传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数值类型, false
对于Boolean
类型,()
在Unit
类型和null
所有对象类型。
要使用此类Reference
,需要指定要用于type参数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 By Example,其中包含更多高级示例,并在需要时参考Scala语言规范。