欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA程序员的SCALA教程

程序员文章站 2022-03-30 11:49:08
...

作者: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程序员不是定义静态成员,而是在单例对象中声明这些成员。

编译示例

为了编译示例,我们使用scalacScala编译器。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的类库定义了强大的实用程序类,例如 DateDateFormat。由于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将原始类型(例如booleanint)与引用类型区分开来,并且不允许将函数作为值来操作。

数字是对象

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

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,是所有函数的类型,它们不带参数并且什么都不返回(类型 UnitvoidC / 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中的所有类都继承自超类。如果没有指定超类Complexscala.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

那类的事实SumVarConst被声明为case类意味着他们从标准类的区别在以下几个方面:

  • new关键字不是强制性的,以创建这些类(即,一个可以写入的情况下Const(5),而不是 new Const(5)),
  • 为构造函数参数自动定义getter函数(即,可以通过写入获取v 某个c类实例的构造函数参数的值),Constc.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可被用作功能从类型的别名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是否为a Sum,如果是,它将左子树绑定到一个名为的新变量l,将右子树绑定到一个被调用的变量r,然后继续按箭头的方式评估表达式; 该表达式可以(并且不会)使用由出现在箭头的左侧,即,图案绑定的变量,l并且 r
  2. 如果第一次检查没有成功,也就是说,如果树不是a Sum,则继续检查是否t为a Var; 如果是,它将Var节点中包含的名称绑定到变量n并继续使用右侧表达式,
  3. 如果第二次检查也失败,即if t既不是a Sum也不是a Var,它检查它是否为a Const,如果是,则将Const节点中包含的值绑定到变量v并继续右侧,
  4. 最后,如果所有检查都失败,则会引发异常以指示模式匹配表达式的失败; 只有Tree在声明了更多的子类时,才会发生这种情况。

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

作为一名经验丰富的面向对象的编程人员可能会问,为什么我们没有定义eval方法Tree和它的子类。我们本可以做到这一点,因为Scala允许在案例类中使用方法定义,就像在普通类中一样。因此,决定是否使用模式匹配或方法是一种品味问题,但它对可扩展性也有重要影响:

  • 在使用方法时,很容易添加一种新节点,因为这可以通过Tree为它定义一个子类来完成; 另一方面,添加一个操作树的新操作是繁琐的,因为它需要修改所有子类 Tree
  • 在使用模式匹配时,情况正好相反:添加新类型的节点需要修改在树上进行模式匹配的所有函数,以考虑新节点; 另一方面,通过将其定义为独立函数,添加新操作很容易。

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

  1. 和的导数是导数的总和,
  2. 某个变量的导数v是1,如果v是相对于该导数发生的变量,则为0,否则为0
  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关键字后面的表达式。除非其表达式为真,否则此保护可防止模式匹配成功。这里它用于确保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类型和evalderive以及 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类型,因为它也是一个超级类型的基本类型,如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特征。

然后,我们重新定义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, longfloat等),这意味着大量的动态类型的强制转换必须由程序员插入。

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语言规范