名著阅读 > Java程序员修炼之道 > 9.1 走马观花Scala >

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 Heartbeat
case 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的原因。