A Scala Tutorial for Java Programmers

Language

Michel Schinz和Philipp Haller

Introduction

本文档简要介绍了Scala语言和编译器. 本指南适用于已经具有一定编程经验并且想要概述他们可以使用Scala进行工作的人员. 假定具有面向对象编程的基础知识,尤其是Java语言.

A First Example

作为第一个示例,我们将使用标准的Hello world程序. 它不是很吸引人,但是可以很容易地演示Scala工具的用法,而无需过多地了解该语言. 外观如下:

object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("Hello, world!")
  }
}

该程序的结构应为Java程序员所熟悉:它由一个称为main方法组成,该方法将命令行参数,字符串数组作为参数. 该方法的主体包括对预定义方法println的单个调用,并以友好问候作为参数. main方法不返回值. 因此,其返回类型声明为Unit .

Java程序员最不熟悉的是包含main方法的object声明. 这样的声明引入了通常被称为单例对象的东西 ,即具有单个实例的类. 因此,上面的声明同时声明了一个名为HelloWorld的类以及该类的一个实例,也称为HelloWorld . 该实例是在首次使用时按需创建的.

精明的读者可能已经注意到main方法在这里未声明为static方法. 这是因为静态成员(方法或字段)在Scala中不存在. Scala程序员不是定义静态成员,而是在单例对象中声明这些成员.

Compiling the example

为了编译该示例,我们使用scalac (Scala编译器). scalac像大多数编译器一样工作:它使用源文件作为参数(可能包含一些选项),并生成一个或多个目标文件. 它产生的目标文件是标准的Java类文件.

如果将上面的程序保存在一个名为HelloWorld.scala的文件中,则可以通过发出以下命令来编译它(大于号>表示shell提示符,不应键入):

> scalac HelloWorld.scala

这将在当前目录中生成一些类文件. 其中一个名为HelloWorld.class ,其中包含一个可以使用scala命令直接执行的类,如下节所示.

Running the example

编译后,可以使用scala命令运行Scala程序. 它的用法与用于运行Java程序的java命令非常相似,并且接受相同的选项. 可以使用以下命令执行以上示例,该命令将产生预期的输出:

> scala -classpath . HelloWorld

Hello, world!

Interaction with Java

One of Scala’s strengths is that it makes it very easy to interact with Java code. All classes from the java.lang package are imported by default, while others need to be imported explicitly.

让我们看一个演示这一点的例子. 我们希望根据特定国家(例如法国)使用的约定来获取当前日期并设置其格式. (其他地区,例如瑞士讲法语的地区也使用相同的约定.)

Java的类库定义了功能强大的实用程序类,例如DateDateFormat . 由于Scala与Java无缝互操作,因此无需在Scala类库中实现等效的类-我们可以简单地导入相应Java包的类:

import java.util.{Date, Locale}
import java.text.DateFormat._

object FrenchDate {
  def main(args: Array[String]): Unit = {
    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语法的一个有趣特性. 带有一个参数的方法可以与infix语法一起使用. 也就是说,表达

df format now

只是另一种稍稍冗长的表达方式

df.format(now)

这可能看起来像是次要的语法细节,但它具有重要的后果,下一节将探讨其中之一.

总结本节有关与Java集成的部分,应注意,还可以从Java类继承并直接在Scala中实现Java接口.

Everything is an Object

Scala是一种纯粹的面向对象语言,从某种意义上说, 一切都是对象,包括数字或函数. 在这方面,它与Java不同,因为Java将原始类型(例如booleanint )与引用类型区分开.

Numbers are objects

由于数字是对象,因此它们也具有方法. 实际上,算术表达式如下:

1 + 2 * 3 / x

如上一节所述,它完全由方法调用组成,因为它等效于以下表达式:

1.+(2.*(3)./(x))

这也意味着+*等在Scala中是有效的标识符.

Functions are objects

函数也是Scala中的对象. 因此,可以将函数作为参数传递,将其存储在变量中以及从其他函数返回它们. 这种将函数作为值进行操作的能力是一种非常有趣的编程范例(称为函数编程)的基石之一.

作为一个非常简单的示例,说明为什么将函数用作值很有用,让我们考虑一个定时器函数,其目的是每秒执行一些操作. 我们如何将执行的动作传递给它? 从逻辑上来说,这是一个函数. 这种非常简单的函数传递应该为许多程序员所熟悉:它通常用于用户界面代码中,以注册在发生某些事件时被调用的回调函数.

在下面的程序中,timer函数被调用oncePerSecond ,并获得一个回调函数作为参数. 此函数的类型写为() => Unit并且是不带参数也不返回任何值的所有函数的类型( Unit类型类似于C / C ++中的void ). 该程序的主要功能只是通过回调来调用此计时器功能,该回调将在终端上打印句子. 换句话说,该程序每秒都无休止地打印句子"时光如箭".

object Timer {
  def oncePerSecond(callback: () => Unit): Unit = {
    while (true) { callback(); Thread sleep 1000 }
  }
  def timeFlies(): Unit = {
    println("time flies like an arrow...")
  }
  def main(args: Array[String]): Unit = {
    oncePerSecond(timeFlies)
  }
}

请注意,为了打印字符串,我们使用了预定义的方法println而不是使用System.out .

Anonymous functions

While this program is easy to understand, it can be refined a bit. First of all, notice that the function timeFlies is only defined in order to be passed later to the oncePerSecond function. Having to name that function, which is only used once, might seem unnecessary, and it would in fact be nice to be able to construct this function just as it is passed to oncePerSecond. This is possible in Scala using 匿名功能, which are exactly that: functions without a name. The revised version of our timer program using an anonymous function instead of timeFlies looks like that:

object TimerAnonymous {
  def oncePerSecond(callback: () => Unit): Unit = {
    while (true) { callback(); Thread sleep 1000 }
  }
  def main(args: Array[String]): Unit = {
    oncePerSecond(() =>
      println("time flies like an arrow..."))
  }
}

在此示例中,通过右箭头=>揭示了匿名函数的存在,它将功能的参数列表与其主体分开. 在此示例中,参数列表为空,如箭头左侧的空括号对所示. 该函数的主体与上面的timeFlies之一相同.

Classes

如上所述,Scala是一种面向对象的语言,因此具有类的概念. (为了完整起见,应该注意,某些面向对象的语言没有类的概念,但是Scala不是其中的一种.)Scala中的类是使用与Java语法相似的语法声明的. 一个重要的区别是Scala中的类可以具有参数. 在下面的复数定义中对此进行了说明.

class Complex(real: Double, imaginary: Double) {
  def re() = real
  def im() = imaginary
}

这个Complex类有两个参数,它们是Complex的实部和虚部. 创建类Complex的实例时,必须传递这些参数,如下所示: new Complex(1.5, 2.3) . 该类包含两个方法,分别称为reim ,可以访问这两个部分.

应该注意的是,这两个方法的返回类型没有明确给出. 它会由编译器自动推断,编译器会查看这些方法的右侧,并推论两者均返回Double类型的值.

编译器并不总是能够像在这里那样推断类型,不幸的是,没有简单的规则可以确切知道何时以及何时不知道. 实际上,这通常不是问题,因为编译器在无法推断未明确给出的类型时会抱怨. 作为一个简单的规则,新手Scala程序员应该尝试忽略似乎很容易从上下文中推断出的类型声明,并查看编译器是否同意. 一段时间后,程序员应该对何时省略类型以及何时明确指定类型有很好的了解.

Methods without arguments

reim方法的一个小问题是,要调用它们,必须在它们的名称后放置一对空括号,如以下示例所示:

object ComplexNumbers {
  def main(args: Array[String]): Unit = {
    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
}

Inheritance and overriding

Scala中的所有类都继承自超类. 如上一节的" Complex示例中一样,如果未指定超类,则隐式使用scala.AnyRef .

可以覆盖从Scala中的超类继承的方法. 但是,为了避免意外覆盖,必须明确指定使用override修饰符覆盖一个方法. 例如,可以通过重新定义从Object继承的toString方法来扩展我们的Complex类.

class Complex(real: Double, imaginary: Double) {
  def re = real
  def im = imaginary
  override def toString() =
    "" + re + (if (im >= 0) "+" else "") + im + "i"
}

我们可以如下调用覆盖的toString方法.

object ComplexNumbers {
  def main(args: Array[String]): Unit = {
    val c = new Complex(1.2, 3.4)
    println("Overridden toString(): " + c.toString)
  }
}

Case Classes and Pattern Matching

树是程序中经常出现的一种数据结构. 例如,解释器和编译器通常在内部将程序表示为树. 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

SumVarConst类声明为case类的事实意味着它们在几个方面与标准类不同:

  • new关键字不是创建这些类的实例所必需的(即,可以编写Const(5)而不是new Const(5) ),
  • 自动为构造函数参数定义getter函数(即,只需编写cv ,就可以获取Const类的某个实例cv构造函数参数的值),
  • 提供了方法equalshashCode默认定义,它们用于实例的结构而不是其标识,
  • 提供方法toString的默认定义,并以"源形式"打印该值(例如,表达式x+1的树打印为Sum(Var(x),Const(1)) ),
  • 这些类的实例可以通过模式匹配来分解,如下所示.

现在我们已经定义了数据类型来表示我们的算术表达式,我们可以开始定义操作它们的操作了. 我们将从在某些环境中评估表达式的函数开始. 环境的目的是为变量赋值. 例如,在将值5与变量x相关联的环境中求值的表达式x+1 (写为{ x -> 5 } )给出6作为结果.

因此,我们必须找到一种表示环境的方法. 当然,我们可以使用一些关联的数据结构,例如哈希表,但是我们也可以直接使用函数! 环境实际上只不过是将值与(变量)名称相关联的函数. 上面给出的环境{ x -> 5 }可以简单地在Scala中编写如下:

{ case "x" => 5 }

该符号定义了一个函数,当给定字符串"x"作为参数时,该函数返回整数5 ,否则失败,并出现异常.

在编写评估函数之前,让我们先命名环境的类型. 我们当然可以在环境中始终使用String => Int类型,但是如果我们为该类型引入名称,它将简化程序,并使将来的更改更加容易. 这是在Scala中使用以下符号完成的:

type Environment = String => Int

从那时起, Environment类型可以用作从StringInt的函数类型的别名.

现在我们可以给出评估函数的定义. 从概念上讲,这很简单:两个表达式之和的值就是这些表达式的值之和; 变量的值直接从环境中获取; 常量的值就是常量本身. 在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的变量,然后继续对表达式进行求值跟随箭头; 该表达式可以(并且确实)利用由箭头左侧出现的模式所限制的变量,即lr .
  2. 如果第一次检查不成功,也就是说,如果树不是Sum ,则继续检查t是否是Var ; 如果是,它将绑定在Var节点中的名称绑定到变量n并继续右侧表达式,
  3. 如果第二次检查也失败,即t既不是Sum也不是Var ,则检查它是否是Const ,如果是,则将Const节点中包含的值绑定到变量v并继续进行右移-手侧
  4. 最后,如果所有检查均失败,则引发异常以指示模式匹配表达式失败; 仅当声明了Tree更多子类时,才可能在此发生.

我们看到模式匹配的基本思想是尝试将值与一系列模式匹配,并且一旦模式匹配,就提取并命名值的各个部分,以最终评估一些通常使用这些代码的代码命名零件.

一个经验丰富的面向对象的编程人员可能会问,为什么我们没有定义eval作为类的方法 Tree和它的子类. 我们实际上可以做到,因为Scala允许在条件类中进行方法定义,就像在普通类中一样. 因此,决定是否使用模式匹配或方法是一个问题,但这对可扩展性也具有重要意义:

  • when using methods, it is easy to add a new kind of node as this can be done just by defining a sub-class of Tree for it; on the other hand, adding a new operation to manipulate the tree is tedious, as it requires modifications to all sub-classes of Tree,
  • 当使用模式匹配时,情况就相反了:添加新类型的节点需要修改所有在树上进行模式匹配的功能,以将新节点考虑在内; 另一方面,只需将其定义为独立的函数,即可轻松添加新操作.

为了进一步探索模式匹配,让我们在算术表达式上定义另一个运算:符号导数. 读者可能会记住有关此操作的以下规则:

  1. 和的导数是导数的和,
  2. 如果v是与之相关的变量,则某个变量v的导数为1,否则为零,
  3. 常数的导数为零.

这些规则几乎可以转换为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关键字之后的表达式. 除非表达式为真,否则此保护措施可防止模式匹配成功. 在此用于确保仅在要派生的变量的名称与派生变量v相同的情况下返回常量1 . 此处使用的模式匹配的第二个新功能是通配符 _ ,这是一个与任何值匹配的模式,但没有命名.

我们还没有探索模式匹配的全部功能,但是为了使本文简短,我们将在此处停止. 我们仍然想看看上面的两个函数如何在一个真实的例子中执行. 为此,让我们编写一个简单的main函数,该函数对表达式(x+x)+(7+y)执行几个操作:首先在环境{ x -> 5, y -> 7 }计算其值,然后计算其相对于x导数,然后相对y .

def main(args: Array[String]): Unit = {
  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类型和evalderivemain方法包装在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)))

通过检查输出,我们看到导数的结果在呈现给用户之前应该被简化. 使用模式匹配来定义基本的简化功能是一个有趣的(但出人意料的棘手)问题,留给读者练习.

Traits

除了从超类继承代码之外,Scala类还可以从一个或多个trait导入代码.

Java程序员了解特性的最简单方法就是将它们视为也可以包含代码的接口. 在Scala中,当类从特征继承时,它将实现该特征的接口,并继承该特征中包含的所有代码.

(请注意,自Java 8起,Java接口还可以使用default关键字或作为静态方法来包含代码.)

为了了解特征的有用性,让我们看一个经典的例子:有序对象. 能够在给定类别的对象之间进行比较(例如对它们进行排序)通常很有用. 在Java中,可比较的对象实现Comparable接口. 在Scala中,通过将Comparable定义为特征,我们可以将其称为Ord ,从而比Java更好.

比较对象时,六个不同的谓词可能会有用:较小,较小或相等,相等,不相等,较大或相等和较大. 但是,定义所有这些都是很费力的,特别是因为可以使用剩余的两个来表示这六个中的四个. 也就是说,给定相等且较小的谓词(例如),一个可以表示其他谓词. 在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类型的更通用版本,因为它还是基本类型(如IntFloat等)的超类型.

为了使一个类的对象具有可比性,因此只需定义测试相等性和自卑性的谓词,然后在上述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方法,以便它通过比较它们的各个字段来正确比较日期. 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
  }

此方法使用预定义的方法isInstanceOfasInstanceOf . 第一个是isInstanceOf ,它对应于Java的instanceof运算符,并且仅当应用了该对象的对象是给定类型的实例时,才返回true. 第二个asInstanceOf ,对应于Java的asInstanceOf运算符:如果对象是给定类型的实例,则将其视为此类,否则ClassCastException .

最后,定义的最后一种方法是谓词,用于测试自卑感,如下所示. 它利用的另一种方法中, error从包对象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特性继承而来的.

当然,在其他情况下,性状在此处显示的情况下也很有用,但详细讨论其应用不在本文讨论范围之内.

Genericity

我们将在本教程中探讨的Scala的最后一个特性是通用性. Java程序员应该充分意识到他们的语言缺乏通用性所带来的问题,这是Java 1.5中解决的一个缺陷.

Genericity is the ability to write code parametrized by types. For example, a programmer writing a library for linked lists faces the problem of deciding which type to give to the elements of the list. Since this list is meant to be used in many different contexts, it is not possible to decide that the type of the elements has to be, say, Int. This would be completely arbitrary and overly restrictive.

Java程序员使用Object ,它是所有对象的超类型. 但是,该解决方案远非理想之选,因为它不适用于基本类型( intlongfloat等),并且它意味着程序员必须插入许多动态类型强制转换.

Scala可以定义通用类(和方法)来解决此问题. 让我们用一个最简单的容器类示例进行检查:一个引用,该引用可以为空,也可以指向某种类型的对象.

class Reference[T] {
  private var contents: T = _
  def set(value: T) { contents = value }
  def get: T = contents
}

Reference类由称为T的类型进行参数化, T是其元素的类型. 该类型在类的主体中用作contents变量的类型, set方法的参数以及get方法的返回类型.

上面的代码示例在Scala中引入了变量,不需要进一步说明. 但是,有趣的是,赋予该变量的初始值为_ ,它代表默认值. 对于数字类型,此默认值为0;对于Boolean类型,此默认值为false ;对于Unit类型,此默认值为() ;对于所有对象类型,默认值为null .

要使用此Reference类,需要指定要用于类型参数T的类型,即单元格包含的元素的类型. 例如,要创建和使用包含整数的单元格,可以编写以下内容:

object IntegerReference {
  def main(args: Array[String]): Unit = {
    val cell = new Reference[Int]
    cell.set(13)
    println("Reference contains the half of " + (cell.get * 2))
  }
}

As can be seen in that example, it is not necessary to cast the value returned by the get method before using it as an integer. It is also not possible to store anything but an integer in that particular cell, since it was declared as holding an integer.

Conclusion

该文档简要介绍了Scala语言,并提供了一些基本示例. 感兴趣的读者可以继续阅读,例如,阅读Scala ,其中包含更多说明和示例,并在需要时查阅Scala语言规范 .

Contributors to this page:

by  ICOPY.SITE