首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》9.1 走马观花Scala

关灯直达底部

下面是我们准备展示的主要内容:

  • 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方法前指明关键字publicstatic

  • 不必声明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类型(即未知类型)的值为参数,然后用模式分别处理StringInt类型的值。每个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的原因。