Scala学习

来源:互联网 发布:淘宝打快递单步骤图 编辑:程序博客网 时间:2024/05/22 07:09

我们使用最经典的“Hello world”作为第一个例子,这个例子虽然并不是特别炫(fascinating),但它可以很好的展示 Scala 的用法,且无须涉及太多的语言特性。示例代码如下:

object HelloWorld {  def main (args: Array[String]){    println("helloWorld")  }}

Java 程序员应该对示例代码的结构感到很熟悉:它包含一个 main 方法,其参数是一个字符串数组,用来接收命令行参数;main 的方法体只有一句话,调用预定义的 println方法输出“Hello world!”问候语。main 方法不返回值(这是一个过程方法 proceduremethod),因此,该方法不必声明返回值类型。

对于包含 main 方法的 object 声明,Java 程序员可能要相对陌生一些。这种声明方式引入了一个通常被称为单例对象(singleton object)的概念,也就是有且仅有一个实例的类。因此,上例中的声明,在定义了一个名为的 HelloWorld 类的同时,还声明了该类的一个实例,实例的名字也叫 HelloWorld。该实例在第一次被用到的时候即时(ondemand)创建。

细心(astute,机敏的,聪明的)的读者可能会注意到,main 方法并没有声明为 static。这是因为 Scala 中不存在静态成员(无论方法还是属性,methods or fields)这一概念,Scala 使用前述的单例对象(singleton objects)中的成员来代替静态成员。

要编译上面写的例子,要 scalac 命令,这就是 Scala 的编译器。scalac 的工作流程和多数编译器类似:从命令行上接收待编译的源文件名(source file)以及编译参数,生成一个或者多个目标文件(object files,或者叫对象文件)。Scala 生成的目标文件是标准的 java class 文件。

假如我们将 HelloWorld 示例程序存放到 HelloWorld.scala 文件中,则可以用以下指令进行编译(大于号’>’表示 shell/命令行 的提示符,不需要人工键入):

> scalac HelloWorld.scala 
代码编译通过以后,可以使用 scala 指令运行程序,scala 指令和 java 指令的用法非常相似,甚至它们接受的命令行参数都是一样的。前面编译好的例子,可以用如下指令运行,并输出预期的问候语:

> scala -classpath . HelloWorld 

和 Java 代码的交互能力,是 Scala 语言的强项之一。在 Scala 程序中,java.lang 包下的类是默认全部引入的,其它包下的类则需要显式(explicitly)引入。

我们可以通过一个例子来展示Scala与Java的交互能力。假如,我们想获取系统当前时间,并按照某个国家(比如法国)的显示习惯进行格式化

我们知道,在 Java 的类库中已经实现了 Date、DateFormat 等功能强大的工具类,且Scala 可以和 Java 进行无缝(seemlessly)的互操作,所以,想在 Scala 程序中使用这些功能,只需要引入这些 Java 类即可,无须从头重复实现相同的功能。

import java.util.{Date, Locale}import java.text.DateFormatimport 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 中的 import 很想象,但 Scala 的语法更强大一些。比如,要想引入一个包中的多个类,在 Scala 中可以写在一行上,只需要把多个类名放到一个大括号中(curly braces, {})即可。此外,如果要引入一个包或者类中的所有名字,Scala 使用下划线(underscore,_)而不是星号(asterisk,*),这是因为,在 Scala中,星号是一个合法的标识符(比如:方法名),后面我们会遇到这种情况。

因此,上例中第三行的 import 语句,引入了 DateFormat 类的所有成员,这样该类的静态方法 getDateInstance 和静态属性 LONG 对 FrenchDate 类来说,是直接可见的(directlyvisible)。

在 main 方法中,我们首先创建一个 Java 的 Date 实例,该实例默认取得系统当前时间;接下来,我们使用从 DateFormat 类中引入的静态方法 getDateInstance 创建一个负责日期格式化的对象 df ,创建过程中,通过参数指定了本地化区域(Locale.FRANCE);最后,使用 df 将当前时间进行格式化并打印输出到控制台。这个方法的最后一行,体现了 Scala 语法中一种很有意思的特性(interestingproperty):如果一个方法只接受一个参数,那么可以使用 infix 语法,也就是说,下面的表达式:df format now和 df.format(now)的语义完全相同,只是前者更加简洁。

这虽然是一个很小的语法细节,但它具有深远的影响,本文后续的章节中将会有进一步的论述。

最后,从 Java 与 Scala 整合的角度看,值得一提的是,Scala 中可以直接继承 Java的类或者实现 Java 的接口。

一切皆对象 - Everything is an object

Scala 中的一切都是对象,从这个意义上说,Scala 是纯粹的面向对象(pureobject-oriented)的语言。在这一点上,Scala 与 Java 不同,因为 Java 中,原子类型(primitive types)和引用类型是有区别的,而且 Java 中不能把函数(function)当做值(value)来对待。

数字是对象 - Numbers are objects

因为数字是对象,所以数字也拥有自己的方法,如下的算术表达式:

1+2*3/x

实际上完全是由方法调用(method calls)构成的。前面章节已经提到过“单参数方法”的简化写法,所以,上述表达式实际上是下面这个表达式的等价简化写法:

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

由此我们还可以看到:+, *等符号在 Scala 中是合法的标识符(和前面进行印证)。This also means that +, *, etc. are valid identifiers in Scala.在第二种写法中,加在数字上的括号是必须的,因为 Scala 的词法分析器使用贪婪算法(longest match,最长匹配)来匹配符号,所以,表达式:1.+(2)将被解释成:1.、+和 2 三个符号。虽然“1.”和“1”都是合法的符号,但“1.”的长度更长,所以被 Scala 的词法分析器优先选择,而“1.”等价于字面值(literal)“1.0”,这将产生一个 Double 浮点数而不是我们期望的 Int 整数。所以必须在数字上面加括号,就像这样:(1).+(2)以避免整数 1 被解释成 Double 类型。

函数是对象 - Functions are objects

在 Scala 中,函数(functions)也是对象(objects),所以,函数可以当做参数进行传递,可以把函数存储在变量中,也可以把函数作为其他函数的返回值,Java 程序员可能会觉得这是一项非常神奇的特性。这种将函数当做值进行操作的能力,是函数式编程(functional programming)最重要的特性(cornerstone,基石)之一。

举一个简单的例子,就可以说明把函数当做值来操作的意义何在。假如我们要开发一个定时器,该定时器每秒钟执行一定的动作,我们如何把要执行的动作传给定时器?最直观的回答是:传一个实现动作的函数(function)。许多程序员,对这种函数传递模式并不陌生:在用户界面(user-interface)相关代码中,当事件被触发时,会调用预先注册的回调函数。


下面的程序将实现简单定时器的功能,负责定时的函数(function)名为:oncePerSecond,它接受一个回调函数作为参数,该回调函数的类型记为:() => Unit,代表任何无参数、无返回值的函数(Unit 和 C/C++中的 void 类似)。程序的 main 方法调用定时函数,作为实参传进去的回调函数 timeFlies,仅仅向终端打印一句话,所以,该程序的实际功能是:每秒钟在屏幕上打印一条信息:time flies like an arrow。

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

匿名函数 - Anonymous functions

定时器的示例程序还可以做一些改进。首先,timeFlies 函数只被用过一次,也就是当做回调函数传给 oncePerSecond 的时候,对于这种函数,在用到的时候即时构造更合理,因为可以省去定义和命名的麻烦,在 Scala 中,这样的函数称为匿名函数(anonymousfunctions),也就是没有名字的函数。使用匿名函数代替 timeFlies 函数后的程序代码如下:

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

代码中的右箭头‘=>’表明程序中存在一个匿名函数,箭头左边是匿名函数的参数列表,右边是函数体。在本例中,参数列表为空(箭头左边是一对空括号),而函数体和改造前定义的 timeFlies 函数保持一致。


类 - Classes

前面已经说过,Scala是面向对象的语言,所以它有类(class)的概念2。Scala中声明类的语法和Java类似,但有一点重要的差异,那就是Scala中的类定义可以带参数(parameters),下面定义的复数类(complex number)可以很清晰的展示这一特性:

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

该复数类可以接受两个参数,分别代表复数的实部和虚部,如果要创建 Complex 类的实例,则必须提供这两个参数,比如:new Complex(1.5, 2.3)。该类有两个方法:re和 im,分别用于访问复数的实部和虚部。

需要注意的是,这两个方法的返回值都没有显式定义。在编译过程中,编译器可以根据函数定义的右部(right-hand ),推断(infer,deduce)出两个函数的返回值都是 Double类型。

但编译器并非在任何情况下都能准确推导出数据类型,而且,很难用一套简单的规则来定义什么情况下可以,什么情况下不可以。不过,对于没有显式指定类型,且无法推导出类型的表达式,编译器会给出提示信息,所以,推导规则的复杂性,对实际编程的影响不大。对于初学者,可以遵循这样一条原则:当上下文看起来比较容易推导出数据类型时,就应该忽略类型声明,并尝试是否能够编译通过,不行就修改。这样用上一段时间,程序员会积累足够的经验,从而可以比较自如的决定何时应该省略类型声明,而何时应该显式声明类型。

无参方法- Methods without arguments

Complex 类中的 re 和 im 方法有个小问题,那就是调用这两个方法时,需要在方法名后面跟上一对空括号,就像下面的例子一样:

如果能够省掉这些方法后面的空括号,就像访问类属性(fields)一样访问类的方法,则程序会更加简洁。这在 Scala 中是可行的,只需将方法显式定义为没有参数(withoutarguments)即可。无参方法和零参方法(methods with zero arguments)的差异在于:无参方法在声明和调用时,均无须在方法名后面加括号。所以,前面的 Complex 类可以重写如下:

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

继承和方法重写 - Inheritance and overriding

Scala 中的所有类都继承自某一个父类(或者说超类,super-class),若没有显式指定父类(比如前面的 Complex 类),则默认继承自 scala.AnyRef。

在 Scala 中可以重写(overriding)从父类继承的方法,但必须使用 override 修饰符来显式声明,这样可以避免无意间的方法覆盖(accidental overriding)。例如,前面定义的 Complex 类中,我们可以重写从 Object 类中继承的 toString 方法,代码如下:

class Complex(real: Double, imaginary: Double) {<span style="white-space:pre"></span>def re = real<span style="white-space:pre"></span>def im = imaginary<span style="white-space:pre"></span>override def toString() = "" +re + (if (im < 0) "" else "+") + im + "i"} 

条件类和模式匹配 - Case classes and pattern matching

树是软件开发中使用频率很高的一种数据结构,例如:解释器和编译器内部使用树来表示代码结构;XML 文档是树形结构;还有一些容器(集合,containers)也是基于树的,比如:红黑树(red-black tree,一种自平衡二叉查找树)。

接下来,我们通过一个示例程序,了解在 Scala 中如何表示和操作树形结构,这个示例将实现非常简单的计算器功能,该计算器可以处理包含加法、变量和整数常量的算术表达式,比如:1 + 2、(x + x) + (7 + y)等。


Traits(特征、特性)

Scala 中的类不但可以从父类继承代码(code),还可以从一个或者多个 traits 引入代码。

对于 Java 程序员来说,理解 traits 最简单的方法,是把它当作可以包含代码逻辑的接口(interface)。在 Scala 中,如果一个类继承自某个 trait,则该类实现了 trait 的接口,并继承了 trait 的所有代码(code)。

我们用一个经典的例子:有序对象(ordered objects)来展示 trait 的作用。有很多应用场景,需要在同类对象之间比较大小,比如排序算法。在 Java 中,可以实现Comparable 接口,而在 Scala 中,有更好的办法,那就是定义一个和 Comparable 对等的 trait,名为:Ord。

对象之间做比较,需要六种断言(predicate,谓词):小于,小于等于,等于,不等于,大于等于,大于。不过,这六种断言中的四种,可以用另外两种进行表述,比如,只要确定了等于和小于两种断言,其它四种就可以推导出来,所以,并不是每种断言都需要由具体类来实现(实现在 trait 上即可,相当于抽象类)。基于以上的分析,我们用下面的代码定义一个 trait:

trait Ord {def < (that: Any): Booleandef <=(that: Any): Boolean = (this < that) || (this == that)def > (that: Any): Boolean = !(this <= that)def >=(that: Any): Boolean = !(this < that)} 

代码中用到的类型 Any 是 Scala 中所有类型的超类。它比 java 中的 Object 类型更加通用,因为基本类型如:Int, Float 也是继承自该类的。

要想让一个类的实例可比,只需要输入(mix in,实际上就是继承)前面定义的 Ord trait(原文是 class,个人感觉可能是笔误),并实现相等和小于(inferiority,劣等,劣势)两个断言即可。接下来还是用例子说话,我们定义一个 Date 类,这个类使用三个整数分别表示公历的年、月、日,该类继承自 Ord,代码如下:

class Date(y: Int, m: Int, d: Int) extends Ord {def year = ydef month = mdef day = doverride def toString(): String = year + "-" + month + "-" + day }
请重点关注代码中紧跟在类名和参数后面的 extends Ord,这是 Date 类声明继承自 Ordtrait 的语法。

接下来,我们要重写(redefine)从 Object 上继承的 equals 方法,该方法的默认实现是比较对象的天然特性(比如内存地址),而 Date 类需要比较年、月、日字段的值才能确定大小。

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 中的 cast 强制类型转换操作:如果当前对象是特定类的实例,转换成功,否则抛出 ClassCastException异常。

最后,还需要定义一个判断小于(inferiority)的函数,该函数又用到了一个预定义方法:error,作用是抛出异常,并附带指定的错误信息。代码如下:

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)))} 

至此,Date 类就写完了,该类的实例既可以被看作是一个日期(dates),也可以被看作一个可比的对象,并且,无论那种看法,他们都有六个用作比较的断言,其中,equals和<直接定义在 Date 类上,而其它四个则继承自 Ord trait。

泛型 - Genericity
最后,我们再来看看 Scala 中的泛型。Java 在 1.5 才引入泛型,在此之前,Java 程序员对语言缺少泛型支持所引发的种种问题,应该是深有体会的。

所谓泛型,就是代码中可以使用参数化类型的能力。例如,有一个负责类库开发的程序员,他想提供一个链表(linked list)结构,他必须决定,在链表中可以存放什么类型的元素。由于链表可以被广泛使用,所以预先限定链表中元素的类型是不现实的,即便勉强作出武断的决定,势必会给类库的应用带来极大的局限性。

在这种情况下,Java 程序员选择 Object 类来降低局限性,但这种解决方案很不理想,一方面,java 中的基本类型,比如 int, long, float 等等不是对象,也就无法加入链表,另一方面,使用 Object 这样的最顶级类,意味着程序员要在代码中大量的使用动态类型转换。

Scala 引入泛型类(和泛型方法)来解决此这个问题。让我们以引用(reference)为例来了解泛型,引用是最简单的容器,可以指向某种类型的对象,或者为空(什么都不指向)。

class Reference[T] { private var contents: T = _def set(value: T) { contents = value }def get: T = contents } 
以上代码中,Reference 是参数化类,T 是类型参数。类型参数在类中用于定义变量contents 的类型、用作 set 方法的参数以及 get 方法的返回值。

例子中声明了一个变量,变量本身没有什么可讨论的,但赋给变量的初始值是‘_’,这一点比较有意思。_表示各种类型的默认值,其中,数字类型的默认值是 0,Boolean 型的默认值是 false,Unit 类型是(),而所有的对象类型(object type)的默认值为 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 方法返回的值不需要做类型转换,就可以当作整数使用,并且,该引用无法指向整数之外的任何对象。

0 0
原创粉丝点击