下面是我们准备展示的主要内容:
- Scala语言的精炼,包括类型推断的能力;
- match表达式,以及模式和case类等相关概念;
- Scala的并发,采用消息和actor机制,而不是像Java代码那样用老旧的锁机制。
这些不是Scala的全部内容,只掌握它们也不可能让你变成Scala开发高手。它们是用来吊你胃口的,只是给你几个具体示例表明Scala可能适用于哪些场合。要走得更远,就得做更深入的探索。你可以找些在线资源,也可以找本完整讲述Scala的书,比如Joshua Suereth的Scala in Depth(Manning,2012)。
我们要解释的第一个特性,也是Scala跟Java最重要的差别,就是它语法上的精炼性,我们就直奔主题吧。
9.1.1 简约的Scala
Scala是采用静态类型系统的编译型语言。也就是说Scala代码应该和Java代码一样详细。可Scala偏偏很精炼,它太精炼了,看起来简直和脚本语言一样。因此Scala开发人员更加快速和高效,写代码的速度几乎可以跟用动态语言编程媲美了。
我们来看一些非常简单的代码,了解一下Scala的构造方法和类。比如要写一个简单的现金流模型类。需要用户提供两项信息:现金流的额度和货币。用Scala应该这样写:
class CashFlow(amt : Double, curr : String) { def amount = amt def currency = curr}
这个类只有四行(其中一行还是用来结束的右括号)。不管怎样,它有获取方法(但没有设置方法)作为参数,还有一个单例构造方法。跟Java比起来,这简直太划算了(就这么几行代码)。请看相应的Java代码:
public class CashFlow { private final double amt; private final String curr; public CashFlow(double amt, String curr) { this.amt = amt; this.curr = curr; } public double getAmt { return amt; } public String getCurr { return curr; }}
跟Scala相比,Java代码中的重复信息太多了,就是这种重复导致了Java代码的冗长。
选择Scala,让开发人员尽量减少重复信息的输入,IDE的界面中就可以显示更多内容。面对稍微复杂点的逻辑时,开发人员就能见到更多代码,因此也有望能掌握理解它所需的更多线索。
要不要省1500美元?
CashFlow
类的Scala版长度几乎比Java版短75%。据估计,一行代码每年的成本是32美元。如果我们假定这段代码的生命期是5年,那在这个项目的生命期内,Scala版代码的维护成本就会比Java代码少花1500美元。
既然说到这儿了,我们就来看看第一个例子中展示的语法点。
类的定义(就它的参数而言)和类的构造方法是同一个东西。Scala中可以有其他的“辅助构造方法”,稍后就会谈到。
类默认是公开的,所以没必要加上
public
关键字。方法的返回类型是通过类型推断确定的,但要在定义方法的
def
从句中用等号告诉编译器做类型推断。如果方法体只是一条语句(或表达式),那就没必要用大括号括起来。
Scala不像Java一样有原始类型。数字类型也是对象。
Scala的精炼不止体现在这些方面。甚至像HelloWorld这样简单的经典程序中都有所体现:
object HelloWorld { def main(args : Array[String]) { val hello = /"Hello World!/" println(hello) }}
即便在这个最基本的例子中,也有几个帮我们去除套路化代码的特性。
关键字
object
告诉Scala编译器这个类是单例类。调用
println
没必要说明完整路径(感谢默认引入)。没必要在
main
方法前指明关键字public
和static
。不必声明
hello
的类型,编译器会自己找出来。不必声明
main
的返回类型,编译器会自动设为Unit
(等价于Java中的void
)。
这个例子中还有些相关语法需要注意一下。
跟Java和Groovy不一样,变量的类型在变量名之后。
Scala用方括号来表示泛型,所以类型参数的表示方法是
Array[String]
,而不是String
。Array
是纯正的泛型。集合类型必须指明泛型(不能像Java那样声明生类型1)。
分号绝对是可选的。
val
就相当于Java中的final
变量,用于声明一个不可变变量。-
Scala应用程序的初始入口总是在
object
中。
1 生类型(raw type)是指不带类型参数的泛型类或接口。比如泛型类Box<T>
,创建它的参数化类型时要指明类型参数的真实类型:Box<Integer> intBox = new Box<>;
。如果忽略了类型参数,Box rawBox = new Box;
则是创建了一个生类型。——译者注
在后续几节中,我们会详细解释这些语法是如何工作的,并且我们还会再选几个让你更省手指头的Scala创新介绍一下。我们也会讨论Scala的函数式编程,它对于编写精炼的代码非常有帮助。现在,我们先来讨论一个强大的Scala“本地”特性。
9.1.2 match表达式
Scala有一种非常强大的结构:match
表达式。最简单的match
用法跟Java的switch
差不多,但match
的表达力要强得多。match
表达式的形式取决于case
从句中的表达式结构。Scala调用不同类型的case
从句模式,但要注意,这些所谓的模式跟正则表达式里的“模式”是截然不同的(尽管在match
表达式里也可以用正则表达式模式)。
先看一个熟悉的例子。1.3.1节那个带字符串的swtich
被翻译成了Scala代码,请看:
var frenchDayOfWeek = args(0) match { case /"Sunday/" => /"Dimanche/" case /"Monday/" => /"Lundi/" case /"Tuesday/" => /"Mardi/" case /"Wednesday/" => /"Mercredi/" case /"Thursday/" => /"Jeudi/" case /"Friday/" => /"Vendredi/" case /"Saturday/" => /"Samedi/" case _ => /"Error: /'/"+ args(0) +/"/' is not a day of the week/"}println(frenchDayOfWeek)
我们在这个例子中只用到了两种最基本的模式:用来确定是周几的常量模式和处理默认情况的_
模式,后面我们还会遇到其他模式。
从语言的纯粹性来看,可以说Scala的语法比Java更清晰,也更正规,至少从下面这两点来看是这样的:
- 默认
case
不需要另外一个不同的关键字; - 单个
case
不会像Java中那样进入下一个case
,所以也不需要break
。
这个例子中的其他语法点如下所示。
关键字
var
用来声明一个可变(非final
)变量。没有必要尽量不要用它,但有时候确实需要它。数组用圆括号访问,比如
args(0)
是指main
的第一个参数。总应该包括默认
case
。如果Scala在运行时在所有case
中都找不到匹配项,就会抛出MatchError
。这绝不是你想看到的。Scala支持间接方法调用,所以可以把
args(0).match({ ... })
写成args(0) match { ... }
。
到目前为止一切都好。match
看起来就像稍微简洁些的switch
。但这只是它众多模式中最像Java的。Scala中有大量使用不同模式的语言结构。比如说,有一种类型化模式,对于处理类型不确定的数据很有用,不用像Java那样弄一堆乱糟糟的类型转换或instanceof
测试:
def storageSize(obj: Any) = obj match { case s: String => s.length case i: Int => 4 case _ => -1}
这个极其简单的方法以一个Any
类型(即未知类型)的值为参数,然后用模式分别处理String
和Int
类型的值。每个case
都给要处理的值绑定了一个临时别名,以便必要时可以调用其中的方法。
在Scala的异常处理代码中有一个跟变量模式非常相似的语法形式。下面是一段改编自第11章ScalaTest框架的类加载代码:
def getReporter(repClassName: String, loader: ClassLoader): Reporter = { try { val reporterCl: java.lang.Class[_] = loader.loadClass(repClassName) reporterCl.newInstance.asInstanceOf[Reporter] } catch { case e: ClassNotFoundException => { val msg = /"Can/'t load reporter class/" val iae = new IllegalArgumentException(msg) iae.initCause(e) throw iae } case e: InstantiationException => { val msg = /"Can/'t instantiate Reporter/" val iae = new IllegalArgumentException(msg) iae.initCause(e) throw iae }... }}
在getReporter
中,要加载一个定制的report
类(通过反射),以便在运行测试集时输出报告。在类加载和实例化过程中很多事都可能出错,所以要有个try-catch
块来保护程序执行。
catch
块起到的作用就跟在异常类型上放match
表达式类似。 case
类的这种思路还可以进一步延伸,接下来我们就来讨论这个。
9.1.3 case类
match
表达式的最强用法之一就是跟case
类(可以看成是枚举概念面向对象的扩展)相结合。我们来看一个温度过高发出报警信号的例子:
case class TemperatureAlarm(temp : Double)
单这一行代码就可以定义一个绝对有效的case
类。在Java中相应的类大概应该是这样子:
public class TemperatureAlarm { private final double temp; public TemperatureAlarm(double temp) { this.temp = temp; } public double getTemp { return temp; } @Override public String toString { return /"TemperatureAlarm [temp=/" + temp + /"]/"; } @Override public int hashCode { final int prime = 31; int result = 1; long temp; temp = Double.doubleToLongBits(this.temp); result = prime * result + (int) (temp ^ (temp >>>32)); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass != obj.getClass) return false; TemperatureAlarm other = (TemperatureAlarm) obj; if (Double.doubleToLongBits(temp) != Double.doubleToLongBits(other.temp)) return false; return true; }}
只需加个case
关键字就可以让Scala编译器生成这些额外的方法。它还会生成很多额外的架子方法。大多数情况下,开发人员都不会直接使用这些方法。它们是为某些Scala特性提供运行时支持的——能以“自然的Scala”方式使用case
类。
创建case
类实例不需要关键字new
,像这样:
val alarm = TemperatureAlarm(99.9)
这进一步强化了case
类是类似于“参数化枚举类型”或某种形式的值类型的观点。
Scala中的相等
Scala认为Java用
==
表示“引用相等”是个错误。所以在Scala中,==
和.equals
是一样的。如果需要判断引用相等,可以用===
。case
类的.equals
方法只有在两个实例的所有参数值都一样时才会返回true
。
case
类跟构造器模式非常合,请看:
def ctorMatchExample(sthg : AnyRef) = { val msg = sthg match { case Heartbeat => 0 case TemperatureAlarm(temp) => /"Tripped at temp /"+ temp case _ => /"No match/" } println(msg)}
我们去看看Scala观光之旅的最后一站:基于actor的并发结构。
9.1.4 actor
Scala选择用actor机制来实现并发编程。它们提供了一个异步并发模型,通过在代码单元间传递消息实现并发。很多开发人员都发现这种并发模型比Java提供的基于锁机制、默认共享的并发模型易用(不过Scala的底层模型也是JMM)。
来看个例子。假设我们在第4章遇到的兽医需要监控诊所里动物的健康状况(尤其是体温)。按我们的想法,温度感应器应该会将它们的读数消息发送给中心监控软件。
在Scala中,我们可以用一个actor类TemperatureMonitor
对这种设置建模。应该有两种不同的消息:一种是标准的“心跳”消息,一种是TemperatureAlarm
消息。第二种消息会带一个参数,表明那个警报器的温度超出了限值。代码清单9-1中列出了这些类的代码。
代码清单9-1 与actor的简单通信
case object Heartbeatcase class TemperatureAlarm(temp : Double)import scala.actors._class TemperatureMonitor extends Actor { var tripped : Boolean = false var tripTemp : Double = 0.0 def act = { //重写actor中的act方法 while (true) { receive { //接受新消息 case Heartbeat => 0 case TemperatureAlarm(temp) => tripped = true tripTemp = temp case _ => println(/"No match/") } } }}
监控actor会对三种不同的case
做出响应(通过receive
)。第一个是心跳消息,告诉你一切正常。因为这个case
类没有参数,所以技术上来说它是一个单例实例,可以按case
对象引用。actor
在收到心跳消息时什么也不用做。
如果收到TemperatureAlarm
消息,actor
会保存警报器上的温度值。你应该想象得出,兽医有另外的代码定期检查TemperatureMonitor
actor,看有没有警报被触发。
最后还有个default case
。这是为了确保有任何不期而至的消息溜进actor环境时能被捕获到。如果没有这个一切全包的 case
,actor如果看到不认识的消息类型就会抛出异常。我们在本章的最后还会再次讨论actor的更多细节,但Scala的并发是个非常大的主题,而且在这本书里我们也不想让你浅尝辄止。
我们快速浏览了Scala的一些亮点。希望其中的某些特性已经燃起了你的兴趣之火。在下一节,我们会花点时间聊聊你可能会(也可能不会)在自己的项目中选择使用Scala的原因。