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

《Java程序员修炼之道》9.3 让代码因Scala重新绽放

关灯直达底部

我们在这一节会先介绍一下Scala编译器和交互环境(REPL)。然后讨论类型推断,接着是方法声明(跟你所熟悉的Java方式不太一样)。这两个特性能帮你减少大量的套路化代码,从而提高生产力。

我们会谈到Scala的代码封包方式和更强大的import语句,然后详细讲解一下Scala中的循环和控制结构。这些特性植根于跟Java差异巨大的编程传统,所以我们会借此机会讨论一下Scala的函数式编程,包括函数式的循环结构、match表达式和函数字面值。

看过这些之后,本章剩下的大部分内容对你来说都没什么问题了,你可以自信地说自己有能力成为一名Scala程序员了。来吧,现在我们就开始讨论编译器和内置的交互环境。

9.3.1 使用编译器和REPL

Scala是编译型语言,所以执行Scala程序通常要把它们先编译成.class文件,然后在类路径上有scala-library.jar(Scala运行时类库)的JVM环境中执行。

如果你还没装Scala,请在继续阅读之前参见附录C,了解如何安装Scala。样例程序(9.1.1中的HelloWorld)可以用scalac HelloWorld.scala编译(如果你正好在HelloWorld.scala文件所在的目录中)。

一旦得到.class文件,就可以用命令scala HelloWorld执行它了。这个命令会启动带着Scala运行时环境的JVM,然后进入类文件指定的main方法。

除了编译和运行,Scala还有个内置的交互环境,有点像第8章讲的Groovy控制台。但不像Groovy,Scala是在命令行环境里实现的。这就是说在典型的Unix/Linux环境(Path设置正确)中,你可以敲入scala,它就会在终端窗口内打开,而不会再弹出一个新窗口。

注意 这类交互环境有时被称为读入—计算—输出(Read-Eval-Print)循环,或简称为REPL。这在动态语言中很常见。在REPL环境中,前面输入的那些行的计算结果还在,在后面的表达式和计算中还可以用。在本章的剩余部分,我们偶尔会用REPL环境来演示Scala语法。

现在我们开始讨论下一个大特性:Scala的高级类型推断。

9.3.2 类型推断

在读前面的代码时你可能已经注意到了,我们在声明变量helloval时,没有指明它是什么类型。因为它很“明显”是个字符串。表面上来看这有点像Groovy,变量没有类型(Groovy是动态类型语言),但其实Scala代码中所发生的事情完全不同。

Scala是静态类型语言(所以变量确实有明确的类型),但它的编译器能分析源码,并且一般都能根据上下文推断出应该是什么类型。如果Scala自己能确定是什么类型,就不用你亲自告诉它了。

这就是类型推断,我们已经提过好几次了。Scala在这方面的能力非常突出—以致于开发人员经常在行云流水一样的代码中忘记静态类型。这经常让Scala更有动态语言的“感觉”。

Java中的类型推断

Java也有类型推断的能力,虽然有限,但确实有。最明显的例子就是我们在第1章见到的泛型钻石语法。Java的类型推断通常是用在赋值语句等号右边的值上。Scala通常是推断变量而不是值的类型,但它的确也能推断值的类型。

你已经见过其中最简单的例子了:关键字varval,Scala根据赋给变量的值来推断它们的类型。Scala类型推断的另一个重要应用是方法声明。我们来看个例子(Scala的AnyRef就是Java中的Object):

def len(obj : AnyRef) = {  obj.toString.length}  

这是一个类型推断的方法。通过检查它返回代码中的java.lang.String#length的类型(int),编译器知道这个方法要返回Int类型的值。注意,这个方法没有显式指定返回类型,我们也不需要用return关键字。实际上,如果你放了一个显式的return在这里,像这样:

def len(obj : AnyRef) = {  return obj.toString.length}  

会得到一个编译时错误:

error:  method len has return statement; needs result type       return obj.toString.length       ^  

如果你连def中的=也省略了,编译器会假定这个方法会返回Unit(就跟Java里返回void一样)。

除了前面那些限制,还有两个类型推断受限的区域:

  • 方法声明中参数的类型——传给方法的参数必须指定类型;
  • 递归函数——Scala编译器不能推断递归函数的返回类型。

关于Scala的方法,我们讨论的东西已经不少了,但还算不上系统化的讨论,所以我们来巩固一下你已经学过的东西。

9.3.3 方法

你已经见过怎么用def关键字定义方法了。随着你对Scala越来越熟悉,关于Scala的方法,还有些你应该知道的重要事实。

  • Scala没有static关键字。跟Java中的static方法对应的方法必须放在Scala的object(单例)结构中。稍后我们会向你介绍相关概念:伴生对象。

  • 跟Groovy(或Clojure)相比,Scala语言的运行时要重得多。Scala类中可能会有很多由平台自动生成的额外方法。

  • 方法调用是Scala的核心概念。在Scala中没有Java中那种意义的操作符。

  • 对于哪些字符可以出现在方法的名称中,Scala比Java更灵活。特别是那些在其他语言中作为操作符的字符,在Scala中可能是合法的方法名(比如加号+)。

间接方法调用(前面讲过)中有Scala把方法调用和操作符合并到一起的线索。举个例子,比如要把两个整型相加。在Java中,应该是写一个a+b这样的表达式。在Scala中你也可以这样写,但不止这样,还可以写成a.+(b)。换句话说,你调用了a上的+方法,并把b作为参数传给它。这就是Scala不再把操作符当做一个独立概念的秘密。

注意 你可能已经注意到了,a.+(b)是在a上调用方法。但原始类型的变量a怎么会有方法呢?9.4节会给出完整的解释。但现在,你只要知道Scala的类型系统认为所有东西都是对象,所以你可以在任何东西上调用方法,即便是Java里的原始类型变量也行。

你已经见过一个用def关键字声明方法的例子了。我们再来看一个例子,一个实现阶乘函数的简单递归方法:

def fact(base : Int) : Int = {  if (base <= 0)    return 1  else    return base * fact(base - 1)}  

对于所有负数,这个函数都返回1,这算是作弊吧。实际上,负数的阶乘是不存在的,但大家都是朋友嘛。它看起来有点像Java:有返回类型(Int),并用return关键字表明把哪个值交回给调用者。唯一需要注意的就是在函数体代码块定义之前额外符号=

Scala中还有另外一个Java中没有概念:局部函数。它是在另外一个函数内部(并且仅在这一作用域内有效)定义的函数。如果开发人员想要一个辅助函数,又不想把实现细节暴露给外部,这是一个简单的办法。在Java中除了用private方法之外别无选择,但这个函数对于同一类的其他方法都是可见的。但在Scala中,你只要这样写就行了:

def fact2(base : Int) : Int = {    def factHelper(n : Int) : Int = {        return fact2(n-1)    }    if (base <= 0)      return 1    else      return base * factHelper(base)}  

factHelperfact2的封闭作用域之外绝对是不可见的。

接下来,我们去看看Scala如何处理代码的组织和导入。

9.3.4 导入

Scala对包的使用跟Java一样,关键字也一样,分别是packageimport。Scala可以毫无障碍地导入和使用Java的包和类。Scala的varval变量可以引用任何Java类的实例,不需要任何特殊的语法或处理:

import java.io.Fileimport java.net._import scala.collection.{Map, Seq}import java.util.{Date => UDate}  

头两行代码跟Java里的标准导入和通配符导入一样。第三行用一行导入一个包里的多个类。最后一行在导入时指定了类的别名(避免缩写冲突出现)。

跟Java不一样,Scala中的import可以出现在代码中的任何位置(不仅限于文件顶部),这样你就可以把import当做文件的一部分分离出来。Scala也有默认导入,即所有.scala文件默认都会导入scala._。这里有很多有用的函数,包括我们已经讨论过的一些,比如println。对于所有默认导入的完整细节,请参见www.scala-lang.org/上的API文档。

我们接下来讨论怎么控制Scala程序的执行流。这可能和你熟悉的Java跟Groovy有些差异。

9.3.5 循环和控制结构

Scala在控制和循环结构上引入了几个有点绕的创新。在我们向你介绍这些不熟悉的形式之前,先来看几个老朋友,比如标准的while循环:

var counter = 1while (counter <= 10) {  println(/"./" * counter)  counter = counter + 1} 

还有do-while形式:

var counter = 1do {  println(/"./" * counter)  counter = counter + 1} while (counter <= 10)  

另一个是基本的for循环:

for (i <- 1 to 10) println(i)  

看起来都很好。但Scala更灵活,比如条件for循环:

for (i <- 1 to 10; if i %2 == 0) println(i)  

还能在多个变量上循环,比如:

for (x <- 1 to 5; y <- 1 to x)  println(/" /" * (x - y) + x.toString * y)  

这些多出来的形式源于Scala实现这些结构的根本性差异。Scala用函数式编程中的概念(列表推导式)来实现for循环。

列表推导式的一般概念是对一个列表中的元素进行转换(或过滤,比如在用条件for循环时)。这会产生一个新列表,然后在其中的每个元素上逐次运行for循环体中的代码。

甚至把要过滤的列表和for代码块分开都是有可能的,用yield关键字。比如下面这段代码:

val xs = for (x <- 2 to 11) yield fact(x)for (factx <- xs) println(factx)  

这段代码先设置新集合xs,然后用第二个for循环逐一输出其中的值。如果你需要一个创建一次、使用多次的集合,这个极其好用。

这一结构能成立是因为Scala支持函数式编程,我们接下来就去看看Scala如何实现函数式思想。

9.3.6 Scala的函数式编程

我们在7.5.2节提起过,Scala把函数当做内置的值。这就是说函数可以放进varval中,并和其他任何值所受的对待毫无二致。这被称为函数字面值(或匿名函数),它们是Scala世界观的重要组成部分。

在Scala中写函数字面值非常简单。其中的关键是箭头=>,Scala用它来表示取得参数列表并传递到代码块中:

(<函数参数列表>) => { ... 作为代码块的函数体 ... }  

我们用Scala的交互环境来演示一下。下面这个例子中定义的函数接受一个Int参数,然后乘以2:

scala> val doubler = (x : Int) => { 2 * x }doubler: (Int) => Int = <function1>scala> doubler(3)res4: Int = 6scala> doubler(4)res5: Int = 8  

注意看Scala怎么推断doubler的类型。它的类型是“接受一个Int并返回Int的函数”。这样的类型用Java的类型系统还不能以令人完全满意的方式表示。你看,调用doubler就是用标准的调用语法。

我们把这个概念再向前推进一点。在Scala中,函数字面值只是值。并且是函数返回的值。这就是说你可以写一个生产函数的函数——接受一个值并返回一个新的函数字面值。

比如说,可以定义一个命名为adder的函数字面值。adder能生产一个给它们的参数加上一个常量的函数:

scala> val adder = (n : Int) => { (x : Int) => x + n }adder: (Int) => (Int) => Int = <function1>scala> val plus2 = adder(2)plus2: (Int) => Int = <function1>scala> plus2(3)res2: Int = 5scala> plus2(4)res3: Int = 6  

看到了吧,Scala对函数字面值支持得很好。实际上,Scala代码一般都能用非常函数的世界观来编写,同时也能用更加命令式的风格编写。现在我们所做的不过是刚刚涉足Scala的函数式编程能力,但重要的是知道它们在那里。

在下一节中,我们会讨论Scala的对象模型和面向对象方式的细节。在一些重要方面,Scala的一些先进的特性使得它对面向对象的处理方式跟Java差异很大。