在现实生活中代码一定要引用并打包在本期(第七期) 面向 Java 开发人员的 Scala 指南 系列中Ted Neward 介绍了 Scala 的包和访问修饰符功能纠正了以前的疏忽然后他继续探讨了 Scale 中的函数内容apply 机制
最近读者的反馈让我意识到在制作本系列的过程中我遗漏了 Scala 的语言的一个重要方面Scala 的包和访问修饰符功能所以在研究该语言的函数性元素 apply 机制前我将先介绍包和访问修饰符
打包
为了有助于隔离代码使其不会相互沖突Java; 代码提供了 package 关键词由此创建了一个词法命名空间用以声明类本质上将类 Foo 放置到名为 comtednewardutil 包中就将正式类名修改成了 comtednewardutilFoo同理必须按该方法引用类如果没有Java 编程人员会很快指出他们会 import 该包避免键入正式名的麻烦的确如此但这仅意味着根据正式名引用类的工作由编译器和字节码完成快速浏览一下 javap 的输出这点就会很明了
关于本系列
Ted Neward 将和您一起深入探讨 Scala 编程语言在这个新的 developerWorks 系列 中您将深入了解 Sacla并在实践中看到 Scala 的语言功能进行比较时Scala 代码和 Java 代码将放在一起展示但(您将发现)Scala 中的许多内容与您在 Java 编程中发现的任何内容都没有直接关联而这正是 Scala 的魅力所在!如果用 Java 代码就能够实现的话又何必再学习 Scala 呢?
然而Java 语言中的包还有几个特殊的要求一定要在包所作用的类所在的 java 文件的顶端声明包(在将注释应用于包时这一点会引发很严重的语言问题)该声明的作用域为整个文件这意味着两个跨包进行紧密耦合的类一定要在跨文件时分离这会致使两者间的紧密耦合很容易被忽略
Scala 在打包方面所采取的方法有些不同它结合使用了 Java 语言的 declaration 方法和 C# 的 scope(限定作用域)方法了解了这一点Java 开发人员就可以使用传统的 Java 方法并将 package 声明放在 scala 文件的顶部就像普通的 Java 类一样包声明的作用域为整个文件就像在 Java 代码中一样而 Scala 开发人员则可以使用 Scala 的包 (scoping)限定作用域 方法用大括号限制 package 语句的作用域如清单 所示
清单 简化的打包
package com{ package tedneward { package scala { package demonstration { object App { def main(args : Array[String]) : Unit = { Systemoutprintln(Howdy from packaged code!) argsforeach((i) => Systemoutprintln(Got + i) ) } } } } }}
这个代码有效地声明了类 App或者更确切的说是一个称为 comtednewardscalademonstrationApp 的单个类注意 Scala 还允许用点分隔包名所以清单 中的代码可以更简洁如清单 所示
清单 简化了的打包(redux)
package comtednewardscalademonstration{ object App { def main(args : Array[String]) : Unit = { Systemoutprintln(Howdy from packaged code!) argsforeach((i) => Systemoutprintln(Got + i) ) } }}
用哪一种样式看起来都比较合适因为它们都编译出一样的代码构造(Scala 将继续编译并和 javac 一样在声明包的子目录中生成 class 文件)
导入
与包相对的当然就是 import 了Scala 使用它将名称放入当前词法名称空间本系列的读者已经在此前的很多例子中见到过 import 了但现在我将指出一些让 Java 开发人员大吃一惊的 import 的特性
首先import 可以用于客户机 Scala 文件内的任何地方并非只可以用在文件的顶部这样就有了作用域的关联性因此在清单 中javamathBigInteger 导入的作用域被完全限定到了在 App 对象内部定义的方法其他地方都不行如果 mathfun 内的其他类或对象要想使用 javamathBigInteger就需要像 App 一样导入该类如果 mathfun 的几个类都想使用 javamathBigInteger可以在 App 的定义以外的包级别导入该类这样在包作用域内的所有类就都导入 BigInteger 了
清单 导入的作用域
package com{ package tedneward { package scala { // package mathfun { object App { import javamathBigInteger def factorial(arg : BigInteger) : BigInteger = { if (arg == BigIntegerZERO) BigIntegerONE else arg multiply (factorial (arg subtract BigIntegerONE)) } def main(args : Array[String]) : Unit = { if (argslength > ) Systemoutprintln(factorial + args() + = + factorial(new BigInteger(args()))) else Systemoutprintln(factorial = ) } } } } }}
不只如此Scala 还不区分高层成员和嵌套成员所以您不仅可以使用 import 将嵌套类型的成员置于词法作用域中其他任何成员均可例如您可以通过导入 javamathBigInteger 内的所有名称使对 ZERO 和 ONE 的限定了作用域的引用缩小为清单 中的名称引用
清单 静态导入
package com{ package tedneward { package scala { // package mathfun { object App { import javamathBigInteger import BigInteger_ def factorial(arg : BigInteger) : BigInteger = { if (arg == ZERO) ONE else arg multiply (factorial (arg subtract ONE)) } def main(args : Array[String]) : Unit = { if (argslength > ) Systemoutprintln(factorial + args() + = + factorial(new BigInteger(args()))) else Systemoutprintln(factorial = ) } } } } }}
您可以使用下划线(还记得 Scala 中的通配符吧?)有效地告知 Scala 编译器 BigInteger 内的所有成员都需要置入作用域由于 BigInteger 已经被先前的导入语句导入到作用域中因此无需显式地使用包名限定类名实际上可以将所有这些都结合到一个语句中因为 import 可以同时导入多个目标目标间用逗号隔开(如清单 所示)
清单 批量导入
package com{ package tedneward { package scala { // package mathfun { object App { import javamathBigInteger BigInteger_ def factorial(arg : BigInteger) : BigInteger = { if (arg == ZERO) ONE else arg multiply (factorial (arg subtract ONE)) } def main(args : Array[String]) : Unit = { if (argslength > ) Systemoutprintln(factorial + args() + = + factorial(new BigInteger(args()))) else Systemoutprintln(factorial = ) } } } } }}
这样您可以节省一两行代码注意这两个导入过程不能结合先导入 BigInteger 类本身再导入该类中的各种成员
也可以使用 import 来引入其他非常量的成员例如考虑一下清单 中的数学工具库(或许不一定有什么价值)
清单 Enron 的记帐代码
package com{ package tedneward { package scala { // package mathfun { object BizarroMath { def bizplus(a : Int b : Int) = { a b } def bizminus(a : Int b : Int) = { a + b } def bizmultiply(a : Int b : Int) = { a / b } def bizdivide(a : Int b : Int) = { a * b } } } } }}
使用这个库会越来越觉得麻烦因为每请求它的一个成员都需要键入 BizarroMath但是 Scala 允许将 BizarroMath 的每一个成员导入最高层的词法空间因此简直就可以把它们当成全局函数来使用(如清单 所示)
清单 计算 Enron的开支
package com{ package tedneward { package scala { package demonstration { object App { def main(args : Array[String]) : Unit = { import comtednewardscalamathfunBizarroMath_ Systemoutprintln( + = + bizplus()) } } } } }}
还有其他的一些构造很有趣它们允许 Scala 开发人员写出更自然的 bizplus 但是这些内容本文不予讨论(想了解 Scala 潜在的可以用于其他用途的特性的读者可以看一下 OderskySpoon 和 Venners 所着的 Programming in Scala 中谈到的 Scala implicit 构造)
访问
打包(和导入)是 Scala 封装的一部分和在 Java 代码中一样在 Scala 中打包很大一部分在于以选择性方式限定访问特定成员的能力 — 换句话说在于 Scala 将特定成员标记为 公有(public)private(私有) 或介于两者之间的成员的能力
Java 语言有四个级别的访问公有(public)私有(private)受保护的(protected )和包级别(它没有任何关键词)访问Scala
废除了包级别的限制(在某种程度上)
默认使用 公有
指定 私有 表示 只有此作用域可访问
相反Scala 定义 protected 的方式与在 Java 代码中不同Java protected 成员对于子类和在其中定义成员的包来说是可访问的Scala 中则仅有子类可访问这意味着 Scala 版本的 protected 限制性要比 Java 版本更严格(虽然按理说更加直观)
然而Scala 真正区别于 Java 代码的地方是 Scala 中的访问修饰符可以用包名来 限定用以表明直到 哪个访问级别才可以访问成员例如如果 BizarroMath 包要将成员访问权限授权给同一包中的其他成员(但不包括子类)可以用清单 中的代码来实现
清单 Enron 的记帐代码
package com{ package tedneward { package scala { // package mathfun { object BizarroMath { def bizplus(a : Int b : Int) = { a b } def bizminus(a : Int b : Int) = { a + b } def bizmultiply(a : Int b : Int) = { a / b } def bizdivide(a : Int b : Int) = { a * b } private[mathfun] def bizexp(a : Int b: Int) = } } } }}
注意此处的 private[mathfun] 表达本质上这里的访问修饰符是说该成员直到 包 mathfun 为止都是私有的这意味着包 mathfun 的任何成员都有权访问 bizexp但任何包以外的成员都无权访问它包括子类
这一点的强大意义就在于任何包都可以使用 private 或者 protected 声明甚至 com(乃至 _root_它是根名称空间的别名因此本质上 private[_root_] 等效于 public 同)进行声明这使得 Scala 能够为访问规范提供一定程度的灵活性远远高于 Java 语言所提供的灵活性
实际上Scala 提供了一个更高程度的访问规范对象私有 规范用 private[this] 表示它规定只有被同一对象调用的成员可以访问有关成员其他对象里的成员都不可以即使对象的类型相同(这弥合了 Java 访问规范系统中的一个缺口这个缺口除对 Java 编程问题有用外别无他用)
注意访问修饰符必须在某种程度上在 JVM 之上映射这致使定义中的细枝末节会在从正规 Java 代码中调用或编译时丢失例如上面的 BizarroMath 示例(用 private[mathfun] 声明的成员 bizexp)将会生成清单 中的类定义(当用 javap 来查看时)
Listing Enron 的记帐库JVM 视图
Compiled from packagingscalapublic final class comtednewardscalamathfunBizarroMath extends javalangObject{ public static final int $tag(); public static final int bizexp(int int); public static final int bizdivide(int int); public static final int bizmultiply(int int); public static final int bizminus(int int); public static final int bizplus(int int);}
在编译的 BizarroMath 类的第二行很容易看出bizexp() 方法被赋予了 JVM 级别的 public 访问修饰符这意味着一旦 Scala 编译器结束访问检查细微的 private[mathfun] 区别就会丢失因此对于那些要从 Java 代码使用的 Scala 代码我宁愿坚持传统的 private 和 public 的定义(甚至 protected 的定义有时最终映射到 JVM 级别的 public所有不确定的时候请对照实际编译的字节码参考一下 javap以确认其访问级别)
应用
在本系列上一期的文章中(集合类型)当谈及 Scala 中的数组时(确切地说是 Array[T])我说过获取数组的第 i 个元素 实际上是 那些名称很有趣的方法中的一种……尽管当时是因为我不想深入细节但不管怎么说事实证明这种说法严格来说 是不对的
好吧我承认我说谎了
技术上讲在 Array[T] 类上使用圆括号要比使用 名称有趣的方法 复杂一点Scala 为特殊的字符序列(即那些有左右括号的序列)保留了一个特殊名称关联因为它有着特殊的使用意图 做……(或按函数来说将……应用 到……)
换句话说Scala 有一个特殊的语法(更确切一些是一个特殊的语法关系)来代替 应用 操作符 ()更精确地说当用 () 作为方法调用来调用所述对象时Scala 将称为 apply() 的方法作为调用的方法例如一个想充当仿函数(functor)的类(一个充当函数的对象)可以定义一个 apply 方法来提供类似于函数或方法的语义
清单 使用 Functor!
class ApplyTest{ import orgjunit_ Assert_ @Test def simpleApply = { class Functor { def apply() : String = { Doing something without arguments } def apply(i : Int) : String = { if (i == ) Done else Applying + apply(i ) } } val f = new Functor assertEquals(Doing something without arguments f() ) assertEquals(Applying Applying Applying Done f()) }}
好奇的读者会想是什么使仿函数不同于匿名函数或闭包呢?事实证明它们之间的关系相当明显标准 Scala 库中的 Function 类型(指包含一个参数的函数)在其定义上有一个 apply 方法快速浏览一些为 Scala 匿名函数生成的 Scala 匿名类您就会明白生成的类是 Function(或者 Function 或 Function这要看该函数使用了几个参数)的后代
这意味着当匿名的或者命名的函数不一定适合期望设计方法时Scala 开发人员可以创建一个 functor 类提供给它一些初始化数据保存在字段中然后通过 () 执行它无需任何通用基类(传统的策略模式实现需要这个类)
清单 使用 Functor!
class ApplyTest{ import orgjunit_ Assert_ // @Test def functorStrategy = { class GoodAdder { def apply(lhs : Int rhs : Int) : Int = lhs + rhs } class BadAdder(inflateResults : Int) { def apply(lhs : Int rhs : Int) : Int = lhs + rhs * inflateResults } val calculator = new GoodAdder assertEquals( calculator( )) val enronAccountant = new BadAdder() assertEquals( enronAccountant( )) }}
任何提供了被适当赋予了参数的 apply 方法的类只要这些参数都按数字和类型排列了起来它们都会在被调用时运行
结束语
Scala 的打包导入和访问修饰符机制提供了传统 Java 编程人员从未享受过的更高级的控制和封装例如它们提供了导入一个对象的选择方法的能力使它们看起来就像全局方法一样而且还克服了全局方法的传统的缺点它们使得使用那些方法变得极其简单尤其是当这些方法提供了诸如本系列早期文章(Scala 控制结构内部揭密)引入的虚构的 tryWithLogging 函数这样的高级功能时
同样应用 机制允许 Scala 隐藏函数部分的执行细节这样编程人员可能会不知道(或不在乎)他们正调用的东西 事实上不是一个函数而是一个非常复杂的对象该机制为 Scala 机制的函数特性提供了另一个方面当然 Java 语言(或者 C# 或 C++)也提供了这个方面但是它们提供的语法纯度没有 Scala 的高