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

编译示例

为了编译示例,我们使用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语言规范

猜你喜欢

转载自blog.csdn.net/u010675669/article/details/81744107