您的位置:首页 > 编程语言 > Java开发

给Java开发者的Scala教程

2015-11-04 12:22 495 查看


给Java开发者的Scala教程

author:Michel Schinz,Philipp Haller


1. 简介

本文将该要的介绍Scala语言和其编译。这里假设读者已经有一定的java开发经验,需要概要的了解他们可以用Scala 做些什么。


2. 第一个例子

我们用全世界最著名的代码来作为开始。虽然没什么用,但是可以很好地直观的了解Scala:
object HelloWorld {

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

没什么复杂的。只有一点,这个包含了main方法的object声明。这个object的声明是定义了一个单例。所以,上面 的声明定义了一个类和这个类的实例,这个实例也叫做HelloWorld。这个实例在第一次使用到的时候被创建。

细心的读者会发现,这个main方法并没有并声明为static的。这是因为static成员在Scala中是不存在的。需要 静态成员的时候,就把这些成员都放在单例中(object声明)。


2.1 编译这个例子

要编译使用scalac命令。

>scalac HelloWorld.scala
[/code]

编译之后生成HelloWorld.class文件。


2.2 运行这个例子

编译之后Scala代码可以使用scala命令来运行。这些命令的使用和java非常类似。
>scala -classpath . HelloWorld
[/code]


3 和Java互操作

一个scala的优势是可以很容易的使用java代码。java.lang包的全部类都是默认被import的,其他的也可以显示引入。 下面的代码会表明这一点。
import java.util.{Date, Locale}
import java.text.DateFormat
import java.text.DateFormat._

/**
* Created by home on 3/11/15.
*/
object FrenchDate {
def main(args: Array[String]): Unit = {
val now = new Date
val df = getDateInstance(LONG, Locale.FRANCE)
println(df format now)
}
}
[/code]

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的数字就是对象,所以可以包含方法。如:
1 + 2 * 3 / x
[/code]

还原出真实的方法调用后:
(1).(((2).*(3))./(x))
[/code]

这同时也说明+, *等符号是Scala的可用的标示符。


4.2 方法也是对象

这个也许更加让java的开发者吃惊。方法也是Scala的对象。因此可以把方法作为参数传递,把他们赋值给变量,从 另外方法中返回。这也就赋予了Scala另外一个能力:函数式编程。

下面就使用一个例子来演示这一点。有一个每一秒需要执行一次的方法
oncePerSecond
,并且有一个回调方法作为参数。
()
=> Unit
的写法是定义了一个无参数,返回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)
}
}
[/code]

这里为了输出字符串,我们使用了预定义的println方法,而不是System.out。


4.2.1 匿名方法

因为上面的代码非常容易理解,所以可以精简一点。首先,方法timeFlies定义出来只是为了作为参数传递 给方法
oncePerSecond
。给这个方法命名之后,也只使用一次。非常没有必要。使用Scala的匿名方法可以
省去这些麻烦。如:
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..."))
}
}
[/code]

上面的匿名函数使用=>区分函数的参数列表和函数体。此例中,函数的参数列表是空,所以括号是空的。函数体 和上面例子中的timeFlies的一样。


5 类

如上所述,Scala是一个面向对象的语言,所以也有的概念。Scala的类声明和java的基本一样,只不过 Scala的类定义中可以包含参数,如下:
class Comlex(real: Double, imaginary: Double) {
def re() = real
def im() = imaginary
}
[/code]

这个Complex类需要两个参数。这些参数必须在创建实例的时候提供,如:
new Complex(1.5, 2.3)
。这个类 包含两个方法成员,re和im,可以取得两个对应的参数值。

两个成员方法的返回类型并没有显示给出。编译器会自动推断。编译器并不是总能自动推断出返回类型,而且也没有 什么规律可以知道什么时候编译器就不能自动推断。这倒是不会造成什么困扰,因为推断不出的时候编译器会给出 warning。对于初级开发人员最好是给出一个类型,来看看编译器是不是同意(不同意就会warning)。


5.1 无参数方法

上面的例子还有一个小问题。在调用上面re,im方法的时候不得不写上两个空括号。
  def main(args: Array[String]): Unit = {
val c = new Complex(1.3, 4.6)
println("Imaginary part: " + c.im())
}
[/code]

这很不方便。这个在Scala是可以的。
class Complex(real: Double, imaginary: Double) {
def re = real
def im = imaginary
}

object TimerAnonymous {
def main(args: Array[String]): Unit = {
val c = new Complex(1.3, 4.6)
println("Imaginary part: " + c.im)
}
}
[/code]

括号要不加就全部不加。


5.2 继承和override

Scala的类全部都继承自一个超类。如果没有显示的给出超类,如上面的Complex类,那么则默认的继承自scala.AnyRef。 在Scala中也可以override超类的方法。在override的时候需要用override关键字明确的标明。如:
class Complex(real: Double, imaginary: Double) {
def re = real

def im = imaginary

override def toString() =
"" + re + (if (im < 0) "" else "+") +im + "i"
}
[/code]


6 case类和模式匹配

程序中经常出现的一种数据结构是Tree。如,解析器和编译器内部,程序作为树呈现;XML文档是树,等。我们将通过 一个小计算器程序看看Scala中有多少树需要处理。这个程序是用来处理非常简单的,包含了常量和变量的代数 表达式。两个例子是1 + 2和(x + x) + (7 + y)。

首先我们需要明确,这类的表达式要如何展现。很明显的是一个树:运算符是一个节点,这个节点的叶子就是常量 或者变量。

在java里,这样的tree会使用一个抽象超类来处理。然后,这个抽象超类的子类来处理一个节点或者叶子。在函数式 编程语言中可以使用代数式的类型来达到同样的目的。Scala的case class是间于两者之间的一个概念。如:
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
[/code]

类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} 的条件可以简单的表达为:
{case "x" => 5}
[/code]

上面的表达式定义了一个函数。当参数是x的字符串时,返回数值5,其他情况下抛出异常。

在继续往下之前,我们给这一条件类型一个名称。我们当然可以用String => Int。但是如果我们给出个名称的 话会简单很多。在Scala中可以这样:
type Environment = String => Int
[/code]

从现在开始,type Environment可以代表String到Int的函数了。

我们现在可以给出推导函数了。概念非常简单:两个表达式的和就是这两个表达式的值的和。一个常量的值就是这个常量 本身。概念转化为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
}
[/code]

这个推导方法使用模式匹配的方式处理树。上面的表达式的含义已经很明显:

检查表达式是不是Sum,如果是则其包含了左子树和右子树,分别为lr。之后继续推导箭头后面的 表达式;
如果第一个表达是没有成功执行,也许这个树不是一个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。 声明如下:
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)
}
[/code]

上面的声明创建了一个新的类型Ord,和java的Comparable接口功能一样,并给了三个比较的默认实现和一个抽象方法。 等于和不等于比较没有出现,这是因为那些是任何对象都有的默认实现。Any类型是Scala中全部其他类型的超类。 其中包括基本类型Int、Float等。

作为示例我们会定义一个Date类来实现上面的接口,以达到比较的目的。这个Date类使用格林威治时间,包含day、 month和year,这些全部都是Int型。
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
}
[/code]

首先extends了Ord,并定义了相应的年月日参数。之后我们重定义equals方法,这样在比较两个时间的时候 就是在比较时间的不同部分了。
  override def equals(that: Any): Boolean = that.isInstanceOf[Date] && {
val o = that.asInstanceOf[Date]
o.day == day && o.month == month && o.year == year
}
[/code]

这个方法使用了内置的
isInstanceOf
asInstanceOf
。第一个
isInstanceOf
相当于java的
instanceof
方法。在对象为某了类型的实例的时候返回true。第二个
asInstanceOf
相当于java的
类型转换操作符。如果一个实例是某类型的,则可以转换。否则,抛出ClassCastException异常。

最后一个需要实现的方法是小于比较。这会用到另外的一个预定义的方法
error
,这个方法会抛出一个你给定消息 的异常。
  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)))
}
[/code]

这样就完成了Date类的定义。这个类的实例也已被看做日期,也可以被视为可比较的对象。Traits比上面例子中 展现的更加灵活。读者可以深入研究。


8 泛型

本文最后要讨论的Scala特性是泛型。泛型可以编写类型为参数的代码。最典型的例子就是C++的STL。在Java1.5以前 STL里的链表等库只能接受Object为参数,在特定的类型下取出对象之后再强制类型转换。Scala可以定义泛型类 和方形方法来把你从一堆的类型转换中拯救出来。下面就定义一个最简单的容器类,可以为空也可以指向某一类型的对象。
class Reference[T] {
private var contents: T = _

def set(value: T) {
contents = value
}

def get: T = contents
}
[/code]

这个类的类型参数叫做T。这个类型在类的内部修饰contents成员,以及set方法的value和get方法的返回类型。 很有意思的是这一句
private var contents:
T = _
。下划线表示给contents赋了一个默认值。数字类型的 默认值是0,逻辑类型的默认值是false,Unit的默认值是(),其他的对象的默认值为null

下面看看如何使用。
class IntegerReference {
def main(args: Array[String]): Unit = {
val cell = new Reference[Int]
cell.set(13)
println("Reference contains the half of " + (cell.get * 2))
}
}
[/code]

get之后直接计算,无需再做任何的类型转换。


9 结语

本文只是给你可以快速的索引,让你更快的了解Scala这个语言。有兴趣的读者可以到Scala的官网查阅更多的资料。原文点击这里

欢迎加群互相学习,共同进步。QQ群:iOS: 58099570 | Android: 330987132 | nodejs:329118122 | Go-Scala:217696290 | Python:336880185 | 做人要厚道,转载请注明出处!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: