给Java开发者的Scala教程
author:Michel Schinz,Philipp Haller
1. 简介
本文将该要的介绍Scala语言和其编译。这里假设读者已经有一定的java开发经验,需要概要的了解他们可以用Scala 做些什么。
2. 第一个例子
我们用全世界最著名的代码来作为开始。虽然没什么用,但是可以很好地直观的了解Scala:
没什么复杂的。只有一点,这个包含了main方法的object声明。这个object的声明是定义了一个单例。所以,上面 的声明定义了一个类和这个类的实例,这个实例也叫做HelloWorld。这个实例在第一次使用到的时候被创建。
细心的读者会发现,这个main方法并没有并声明为static的。这是因为static成员在Scala中是不存在的。需要 静态成员的时候,就把这些成员都放在单例中(object声明)。
2.1 编译这个例子
要编译使用scalac命令。
编译之后生成HelloWorld.class文件。
2.2 运行这个例子
编译之后Scala代码可以使用scala命令来运行。这些命令的使用和java非常类似。
3 和Java互操作
一个scala的优势是可以很容易的使用java代码。java.lang包的全部类都是默认被import的,其他的也可以显示引入。 下面的代码会表明这一点。
Scala的import语句和java的几乎一样,不过,更加灵活。一个包得多个类可以在一个import语句里用大括号 括起来一次引用进来。另一个不同是,使用下划线而不是星号引入包中的全部类。所以,第三行import语句引入了 DateFormat的全部成员。
在main函数中,首先创建了一个Java的Date类实例,默认包含当前时间。然后用静态方法gerDateInstance 定义了一个日期格式。最后打印格式化以后的法国的时期。最后的df format now
是一个很有意思的Scala语法。 只需要一个参数的方法可以用中缀语法。按照一般的写法是这样的:df.format(now)
。
最后需要注意的是,Scala可以直接从java继承一个类,也可以实现java的接口。
4 什么都是对象
Scala是一个纯的面向对象的语言,所以其内部的全部都是对象。包括数字和方法。在这一帮面,Scala和java有 很大的不同。因为,java区分值类型和引用类型,也不允许把方法当做值来处理。
4.1 数字和对象
因为Scala的数字就是对象,所以可以包含方法。如:
还原出真实的方法调用后:
这同时也说明+, *等符号是Scala的可用的标示符。
4.2 方法也是对象
这个也许更加让java的开发者吃惊。方法也是Scala的对象。因此可以把方法作为参数传递,把他们赋值给变量,从 另外方法中返回。这也就赋予了Scala另外一个能力:函数式编程。
下面就使用一个例子来演示这一点。有一个每一秒需要执行一次的方法oncePerSecond
,并且有一个回调方法作为参数。 () => Unit
的写法是定义了一个无参数,返回Unit的方法(Unit就相当于c/c++的void)。代码:
这里为了输出字符串,我们使用了预定义的println方法,而不是System.out。
4.2.1 匿名方法
因为上面的代码非常容易理解,所以可以精简一点。首先,方法timeFlies定义出来只是为了作为参数传递 给方法oncePerSecond
。给这个方法命名之后,也只使用一次。非常没有必要。使用Scala的匿名方法可以 省去这些麻烦。如:
上面的匿名函数使用=>区分函数的参数列表和函数体。此例中,函数的参数列表是空,所以括号是空的。函数体 和上面例子中的timeFlies的一样。
5 类
如上所述,Scala是一个面向对象的语言,所以也有类的概念。Scala的类声明和java的基本一样,只不过 Scala的类定义中可以包含参数,如下:
这个Complex类需要两个参数。这些参数必须在创建实例的时候提供,如:new Complex(1.5, 2.3)
。这个类 包含两个方法成员,re和im,可以取得两个对应的参数值。
两个成员方法的返回类型并没有显示给出。编译器会自动推断。编译器并不是总能自动推断出返回类型,而且也没有 什么规律可以知道什么时候编译器就不能自动推断。这倒是不会造成什么困扰,因为推断不出的时候编译器会给出 warning。对于初级开发人员最好是给出一个类型,来看看编译器是不是同意(不同意就会warning)。
5.1 无参数方法
上面的例子还有一个小问题。在调用上面re,im方法的时候不得不写上两个空括号。
这很不方便。这个在Scala是可以的。
括号要不加就全部不加。
5.2 继承和override
Scala的类全部都继承自一个超类。如果没有显示的给出超类,如上面的Complex类,那么则默认的继承自scala.AnyRef。 在Scala中也可以override超类的方法。在override的时候需要用override关键字明确的标明。如:
6 case类和模式匹配
程序中经常出现的一种数据结构是Tree。如,解析器和编译器内部,程序作为树呈现;XML文档是树,等。我们将通过 一个小计算器程序看看Scala中有多少树需要处理。这个程序是用来处理非常简单的,包含了常量和变量的代数 表达式。两个例子是1 + 2和(x + x) + (7 + y)。
首先我们需要明确,这类的表达式要如何展现。很明显的是一个树:运算符是一个节点,这个节点的叶子就是常量 或者变量。
在java里,这样的tree会使用一个抽象超类来处理。然后,这个抽象超类的子类来处理一个节点或者叶子。在函数式 编程语言中可以使用代数式的类型来达到同样的目的。Scala的case class是间于两者之间的一个概念。如:
类Sum、Var和Const被声明为case class,表明他们在某些方面和标准类是不一样的:
- new关键字在创建实例的时候不是必须的(如:Const(5),不用写new Const(5)),
- getter方法自动根据构造函数的参数(如Const类的实例c,从c中取构造函数参数v的值可以用c.v来取得),
- 提供默认的
toString
方法,并按照最基本的调用方法打印。如:x+1打印为:Sum(Var(x), Const(1)), - 这些类的实例可以用模式匹配来分解,这个下面会讲到。
我们已经定义好了表达代数式的数据类型。接下来可以定义他们之上的操作。我们来定义一个方法来推导这个表达式 在某些条件下的值。这些“条件”就是给定变量某些特定的值。比如,表达式 x+1 在某一种条件下(x的值为5的时候) 推导的值是6。
所以我们需要找到一种可以代表这种条件。你会想到hash table这种数据结构,但是在scala中我们可以直接用方法 一个特定的条件无非就是一个变量和这个变量此时拥有的值。上面说到的 {x = 5} 的条件可以简单的表达为:
上面的表达式定义了一个函数。当参数是x的字符串时,返回数值5,其他情况下抛出异常。
在继续往下之前,我们给这一条件类型一个名称。我们当然可以用String => Int。但是如果我们给出个名称的 话会简单很多。在Scala中可以这样:
从现在开始,type Environment可以代表String到Int的函数了。
我们现在可以给出推导函数了。概念非常简单:两个表达式的和就是这两个表达式的值的和。一个常量的值就是这个常量 本身。概念转化为Scala非常容易:
这个推导方法使用模式匹配的方式处理树。上面的表达式的含义已经很明显:
- 检查表达式是不是Sum,如果是则其包含了左子树和右子树,分别为l和r。之后继续推导箭头后面的 表达式;
- 如果第一个表达是没有成功执行,也许这个树不是一个Sum。继续检查如果t(形参)是否为Var,如果是则将Var节点 包含的值和n变量绑定并继续处理右手表达式。
- 如果第二个检查也失败了,那么t既不是Sum也不是一个Var。检查t是否为一个Const,如果是则将Const节点包含的 值和v变量绑定并继续处理右手表达式。
- 最后,如果全部检查失败,则抛出一个异常。在这里,只会在更多的Tree子类被定义的时候发生。
你会发现,模式匹配就是把一个值和一系列的模式进行匹配。一旦匹配了,就提取值的不同部分,并给其命名。最后 使用这些命名的组件做最后的推导。
一个经验丰富的面向对象开发者会问为什么我们不把eval定义成类Tree和他的子类的一个方法呢。我们可以这么做, 因为Scala允许在case class中定义方法。什么时候使用模式匹配,什么时候使用方法就是个人口味的事了。 但是,也有一些很明显的经验可以借鉴:
- 当使用方法时,添加新的节点更加容易:只需要定义一个新的Tree子类。另一方面来说:给树添加一种新的操作 (比如加法之外的减、乘除等)就比较麻烦了。因为,需要给每一个子类都做出相应的修改。
- 使用模式匹配的时候,情况正好相反。添加一个新的节点需要修改全部的模式匹配方法。添加一个操作就很简单了 ,只需要定义另外的一个方法。
7 接口(Traits)
除了可以继承自一个超类,一个Scala类还可以实现一个或者多个traits。
对于一个java开发者来说最容易理解trait的方法是告诉他,这就是java的interface。java的interface也可以 包含代码。在Scala中,一个类继承了trait的时候,他实现了trait的接口,也继承了这个trait的全部代码。
我们使用一个经典案例来演示trait的特性。对对象列表排序是一个经常使用的功能。java比较的时候会实现Comparable 接口。Scala可以比单纯的实现一个Comparable的trait做的更好一些。这里我们定义一个trait为Ord。 声明如下:
上面的声明创建了一个新的类型Ord,和java的Comparable接口功能一样,并给了三个比较的默认实现和一个抽象方法。 等于和不等于比较没有出现,这是因为那些是任何对象都有的默认实现。Any类型是Scala中全部其他类型的超类。 其中包括基本类型Int、Float等。
作为示例我们会定义一个Date类来实现上面的接口,以达到比较的目的。这个Date类使用格林威治时间,包含day、 month和year,这些全部都是Int型。
首先extends了Ord,并定义了相应的年月日参数。之后我们重定义equals方法,这样在比较两个时间的时候 就是在比较时间的不同部分了。
这个方法使用了内置的isInstanceOf
和asInstanceOf
。第一个isInstanceOf
相当于java的 instanceof
方法。在对象为某了类型的实例的时候返回true。第二个asInstanceOf
相当于java的 类型转换操作符。如果一个实例是某类型的,则可以转换。否则,抛出ClassCastException异常。
最后一个需要实现的方法是小于比较。这会用到另外的一个预定义的方法error
,这个方法会抛出一个你给定消息 的异常。
这样就完成了Date类的定义。这个类的实例也已被看做日期,也可以被视为可比较的对象。Traits比上面例子中 展现的更加灵活。读者可以深入研究。
8 泛型
本文最后要讨论的Scala特性是泛型。泛型可以编写类型为参数的代码。最典型的例子就是C++的STL。在Java1.5以前 STL里的链表等库只能接受Object为参数,在特定的类型下取出对象之后再强制类型转换。Scala可以定义泛型类 和方形方法来把你从一堆的类型转换中拯救出来。下面就定义一个最简单的容器类,可以为空也可以指向某一类型的对象。
这个类的类型参数叫做T。这个类型在类的内部修饰contents成员,以及set方法的value和get方法的返回类型。 很有意思的是这一句private var contents: T = _
。下划线表示给contents赋了一个默认值。数字类型的 默认值是0,逻辑类型的默认值是false,Unit的默认值是(),其他的对象的默认值为null。
下面看看如何使用。
get之后直接计算,无需再做任何的类型转换。
9 结语
本文只是给你可以快速的索引,让你更快的了解Scala这个语言。有兴趣的读者可以到Scala的官网查阅更多的资料。原文点击这里。