java

位置:IT落伍者 >> java >> 浏览文章

面向Java开发人员的Scala指南: Scala控制结构内部揭密


发布日期:2021年01月11日
 
面向Java开发人员的Scala指南: Scala控制结构内部揭密

摘要Java&#;开发人员可以将对象作为理解 Scala 的出发点本文是面向 Java 开发人员的 Scala 指南 系列 的第二期作者 Ted Neward 遵循对一种语言进行评价的基本前提一种语言的威力可以直接通过它集成新功能的能力衡量在本文中就是指对复数的支持跟随本文您将了解在 Scala 中与类的定义和使用有关的一些有趣特性

迄今为止在此 系列 中我们已经讨论了 Scala 对生态环境的保真度展示了 Scala 如何将众多的 Java 核心对象功能合并在一起如果 Scala 只是编写对象的另一种方式那么它不会有任何引人注意的地方或者说不再那么功能强大Scala 的函数概念和对象概念的合并以及它对编程人员效率的重视这些使得学习 Scala 语言比 JavacumScala 编程人员所想象的体验更加复杂更加微妙

关于本系列

Ted Neward 潜心研究 Scala 编程语言并带您跟他一起徜徉在这个新的 developerWorks 系列 中您将深入了解 Scala并在实践中看到 Scala 的语言功能在进行相关比较时Scala 代码和 Java 代码将放在一起展示但(您将发现)Scala 中的许多内容与您在 Java 编程中发现的任何内容都没有直接关联而这正是 Scala 的魅力所在!毕竟如果 Java 代码可以做到的话又何必学习 Scala 呢?

例如对控制结构(比如 ifwhile 和 for)使用 Scala 的方法尽管这些控制结构看起来类似一些老的还比较不错的 Java 结构但实际上 Scala 为它们增加了一些完全不同的特性本月的文章是关于使用 Scala 控制结构时能够期望获得哪些东西的入门级读物而不是在制造许多错误(并编写一堆错误代码)之后让您冒着遭受挫折的风险去寻找差异

修订后的 Personscala

在 本系列的上一篇文章 中可以了解到 Scala 能够通过定义一些方法来定义 POJO这些方法模仿基于 POJO 的环境所需的传统 getter 和 setter在这篇文章发表之后我收到了 Bill Venners 发来的电子邮件Bill Venners 是即将发表的正式的 Scala 参考资料使用 Scala 编程(请参阅 参考资料)的合着者之一Bill 指出了实现上述操作的一个更简单的方法即使用 scalareflectBeanProperty 标注如下所示

清单 修改后的 Personscala

class Person(fn:String ln:String a:Int)

{

@scalareflectBeanProperty

var firstName = fn

@scalareflectBeanProperty

var lastName = ln

@scalareflectBeanProperty

var age = a

override def toString =

[Person firstName: + firstName + lastName: + lastName +

age: + age + ]

}

清单 中的方法(上一篇文章 中的清单 的修订版)为指定的 var 生成了 get/set 方法对惟一的缺陷是这些方法并不实际存在于 Scala 代码中因此其他 Scala 代码无法调用它们这通常不是什么大问题因为 Scala 将对为自己生成的字段使用已生成的方法如果事先不知道那么这些对您而言可能是一个惊喜

在查看了清单 中的代码之后最让我感到震动的是Scala 并没有只演示组合函数概念和对象概念的强大威力它还演示了自 Java 首次发布之后的 年里对象语言带来的一些益处

控制是一种幻想

您将看到的许多奇怪的不可思议的东西都可以归功于 Scala 的函数特性因此简单介绍一下函数语言开发和演变的背景可能非常有用

在函数语言中将越来越高级的结构直接构建到语言中是不常见的此外语言是通过一组核心原语结构定义的在与将函数作为对象传递的功能结合之后可用来定义功能的高阶函数 看起来 像是超出了核心语言的范围但实际上它只是一个库类似于任何库此功能可以替换扩充或扩展

根据一组核心原语构建语言的合成 特性由来已久可以追溯到 世纪 年代和 年代使用 SmalltalkLisp 和 Scheme 的时候诸如 Lisp 和 Scheme 之类的语言因为它们在更低级别的抽象上定义更高级别抽象的能力而受到人们的狂热追捧编程人员可以使用高级抽象用它们构建更高级的抽象如今听到讨论这个过程时它通常是关于特定于域的语言(或 DSL)的(请参阅 参考资料)实际上它只是关于如何在抽象之上构建抽象的过程

在 Java 语言中惟一选择就是利用 API 调用完成此操作在 Scala 中可以通过扩展语言本身实现它试图扩展 Java 语言会带来创建极端场景(corner case)的风险这些场景将威胁全局的稳定性而试图扩展 Scala 则只意味着创建一个新库

If 结构

我们将从传统的 if 结构开始 —— 当然此结构必须是最容易处理的结构之一不是吗?毕竟从理论上说if 只检查一个条件如果条件为真则执行后面跟着的代码

但是这种简单性可能带有欺骗性传统上Java 语言对 if 的 else 子句的使用是随意的并且假定如果条件出错可以只跳过代码块但在函数语句中情况不是这样为了保持函数语句的算术特性所有一切都必须以表达式计算的方式出现包括 if 子句本身(对于 Java 开发人员这正是三元操作符 —— ? 表达式 —— 的工作方式)

在 Scala 中非真代码块(代码块的 else 部分)必须以与 if 代码块中值种类相同的形式呈现并且必须产生同一种类的值这意味着不论以何种方式执行代码总会产生一个值例如请参见以下 Java 代码

清单 哪个配置文件?(Java 版)

// This is Java

String filename = defaultproperties;

if (ntains(configFile))

filename = (String)optionsget(configFile);

因为 Scala 中的 if 结构自身就是一个表达式所以重写上述代码会使它们成为清单 中所示的更正确的代码片段

清单 哪个配置文件?(Scala 版)

// This is Scala

val filename =

if (ntains(configFile))

optionsget(configFile)

else

defaultproperties

val 与 var

您可能想更多地了解 valvar 之间的不同实际上它们的不同之处在于 —— 一个是只读的另一个是可变的变量通常函数语言特别是被认为是 函数语言(不允许带有副作用比如可变状态)的那些函数语言只支持 val 概念但是因为 Scala 要同时吸引函数编程人员和命令/对象编程人员所以这二种结构它都提供

也就是说Scala 编程人员通常应该首选 val 结构并在明确需要可变性的时候选择 var原因很简单除了使编程更容易之外val 还能确保程序的线程安全性Scala 中的一个内在主题是几乎每次认为需要可变状态时其实都不需要可变状态让我们从不可变字段和本地变量(val)开始这是展示上述情况的一种方法甚至对最坚定的 Java 怀疑论者也是如此从 Java 中的 final 开始介绍可能不是很合理或许是因为 Java 的非函数特性尽管此原因不可取一些好奇的 Java 开发人员可能想尝试一下

尽管真正的赢家是 Scala但可以通过编写代码将结果分配给 val而不是 var在设置之后就无法对 val 进行更改这与 Java 语言中 final 变量的操作方式是相同的不可变本地变量最显着的副作用是很容易实现并发性试图用 Java 代码实现同样的操作时会带来许多不错的易读的好代码如清单 中所示

清单 哪个配置文件?(Java 版三元式)

//This is Java

final String filename =

ntains(configFile) ?

optionsget(configFile) : defaultproperties;

用代码评审解释这一点可能需要点技巧也许这样做是正确的但许多 Java 编程人员会不以为然并且询问 您做那个干什么

已公开的 while 结构

接下来让我们来看一下 while 及其同胞 dowhile它们做的基本上是同一件事测试一个条件如果该条件为真则继续执行提供的代码块

通常函数语言会避开 while 循环因为 while 实现的大多数操作都可以使用递归来完成函数语言真地非常类似于 递归例如可以考虑一下 Scala by Example(请参阅 参考资料)中展示的 quicksort 实现该实现可以与 Scala 实现一起使用

清单 Quicksort(Java 版)

//This is Java

void sort(int[] xs) {

sort(xs xslength );

}

void sort(int[] xs int l int r) {

int pivot = xs[(l+r)/];

int a = l; int b = r;

while (a <= b)

while (xs[a] < pivot) { a = a + ; }

while (xs > pivot) { b = b 每 ; }

if (a <= b) {

swap(xs a b);

a = a + ;

b = b 每 ;

}

}

if (l < b) sort(xs l b);

if (b < r) sort(xs a r);

}

void swap(int[] arr int i int j) {

int t = arr[i]; arr[i] = arr[j]; arr[j] = t;

}

不必深入太多的细节就可以了解 while 循环的用法它是通过数组中的各种元素进行迭代的先找到一个支点然后依次对每个子元素进行排序毫不令人奇怪的是while 循环也需要一组可变本地变量在这里这些变量被命名为 a 和 b其中存储的是当前支点注意此版本甚至可以在循环自身中使用递归两次调用循环本身一次用于对列表左手边的内容进行排序另一次对列表右手边的内容进行排序

这足以说明清单 中的 quicksort 真的不太容易读取更不用说理解它现在来考虑一下 Scala 中的直接 等同物(这意味着该版本与上述版本尽量接近)

清单 Quicksort(Scala 版)

//This is Scala

def sort(xs: Array[Int]) {

def swap(i: Int j: Int) {

val t = xs(i); xs(i) = xs(j); xs(j) = t

}

def sort(l: Int r: Int) {

val pivot = xs((l + r) / )

var i = l; var j = r

while (i <= j) {

while (xs(i) < pivot) i +=

while (xs(j) > pivot) j =

if (i <= j) {

swap(i j)

i +=

j =

}

}

if (l < j) sort(l j)

if (j < r) sort(i r)

}

sort( xslength )

}

清单 中的代码看起来非常接近于 Java 版也就是说该代码很长很难看并且难以理解(特别是并发性那一部分)明显不具备 Java 版的一些优点

So Ill improve it ……

清单 Quicksort(更好的 Scala 版)

//This is Scala

def sort(xs: Array[Int]): Array[Int] =

if (xslength <= ) xs

else {

val pivot = xs(xslength / )

ncat(

sort(xs filter (pivot >))

xs filter (pivot ==)

sort(xs filter (pivot <)))

}

显然清单 中的 Scala 代码更简单一些注意递归的使用避免完全 while 循环可以对 Array 类型使用 filter 函数从而对其中的每个元素应用 greaterthanequalslessthan 函数事实上在引导装入程序之后因为 if 表达式是返回某个值的表达式所以从 sort() 返回的是 sort() 的定义中的(单个)表达式

简言之我已经将 while 循环的可变状态完全再次分解为传递给各种 sort() 调用的参数 —— 许多 Scala 狂热爱好者认为这是编写 Scala 代码的正确方式

可能值得一提的是Scala 本身并不介意您是否使用 while 代替迭代 —— 您会看到来自编译器的 您在干什么在做蠢事吗? 的警告Scala 也不会阻止您在可变状态下编写代码但是使用 while 或可变状态意味着牺牲 Scala 语言的另一个关键方面即鼓励编写具有良好并行性的代码只要有可能并且可行Scala 式作风 会建议您优先在命令块上执行递归

编写自己的语言结构

我想走捷径来讨论一下 Scala 的控制结构做一些大多数 Java 开发人员根本无法相信的事 —— 创建自己的语言结构

那些通过死读书学习语言的书呆子会发现一件有趣的事while 循环(Scala 中的一个原语结构)可能只是一个预定义函数Scala 文档以及假设的 While 定义中对此进行了解释说明

// This is Scala

def While (p: => Boolean) (s: => Unit) {

if (p) { s ; While(p)(s) }

}

上述语句指定了一个表达式该表达式产生了一个布尔值和一个不返回任何结果的代码块(Unit)这正是 while 所期望的

扩展这些代码行很容易并且可以根据需要使用它们只需导入正确的库即可正如前面提到的这是构建语言的综合方法在下一节介绍 try 结构的时候请将这一点牢记于心

再三尝试

try 结构允许编写如下所示代码

清单 如果最初没有获得成功……

// This is Scala

val url =

try {

new URL(possibleURL)

}

catch {

case ex: MalformedURLException =>

new URL()

}

清单 中的代码与 清单 或 清单 中 if 示例中的代码相差甚远实际上它比使用传统 Java 代码编写更具技巧特别是在您想捕获不可变位置上存储的值的时候(正如我在 清单 中最后一个示例中所做的那样)这是 Scala 的函数特性的又一个优点!

清单 中所示的 case ex 语法是另一个 Scala 结构(匹配表达式)的一部分该表达式用于 Scala 中的模式匹配我们将研究模式匹配这是函数语言的一个常见特性稍后将介绍它现在只把它看作一个将用于 switch/case 的概念那么哪种 C 风格的 struct 将用于类呢?

现在再来考虑一下异常处理众所周知Scala 支持异常处理是因为它是一个表达式但开发人员想要的是处理异常的标准方法并不仅仅是捕获异常的能力在 AspectJ 中是通过创建方面(aspect)来实现这一点的这些方面围绕代码部分进行联系它们是通过切入点定义的如果想让数据库的不同部分针对不同种类异常采取不同行为那么必须小心编写这些切入点 —— SQLExceptions 的处理应该不同于 IOExceptions 的处理依此类推

在 Scala 中这只是微不足道的细节请留神观察!

清单 一个自定义异常表达式

// This is Scala

object Application

{

def generateException()

{

Systemoutprintln(Generating exception);

throw new Exception(Generated exception);

}

def main(args : Array[String])

{

tryWithLogging // This is not part of the language

{

generateException

}

Systemoutprintln(Exiting main());

}

def tryWithLogging (s: => _) {

try {

s

}

catch {

case ex: Exception =>

// where would you like to log this?

// I choose the console window for now

exprintStackTrace()

}

}

}

与前面讨论过的 While 结构类似tryWithLogging 代码只是来自某个库的函数调用(在这里是来自同一个类)可以在适当的地方使用不同的主题变量不必编写复杂的切入点代码

此方法的优点在于它利用了 Scala 的捕获一级结构中横切逻辑的功能 —— 以前只有面向方面的人才能对此进行声明清单 中的一级结构捕获了一些异常(经过检查的和未经检查的都包括)并以特定方式进行处理上述想法的副作用非常多惟一的限制也许就是想象力了您只需记得 Scala 像许多函数语言一样允许使用代码块(aka 函数)作为参数并根据需要使用它们即可

for 生成语言

所有这些都引导我们来到了 Scala 控制结构套件的实际动力源泉for 结构该结构看起来像是 Java 的增强 for 循环的简单早期版但它远比一般的 Java 编程人员开始设想的更强大

让我们来看一下 Scala 如何处理集合上的简单顺序迭代根据您的 Java 编程经验我想您应该非常清楚该怎么做

清单 对一个对象使用 for 循环和对所有对象使用 for 循环

// This is Scala

object Application

{

def main(args : Array[String])

{

for (i < to ) // the leftarrow means assignment in Scala

Systemoutprintln(Counting + i)

}

}

此代码所做的正如您期望的那样循环 并且每次都输出一些值需要小心的是表达式 to 并不意味着 Scala 内置了整数感知(awareness of integer)以及从 的计数方式从技术上说这里存在一些更微妙的地方编译器使用 Int 类型上定义的方法 to 生成一个 Range 对象(Scala 中的任何东西都是对象还记得吗?)该对象包含要迭代的元素如果用 Scala 编译器可以看见的方式重新编写上述代码那么该代码看起来很可能如下所示

清单 编译器看见的内容

// This is Scala

object Application

{

def main(args : Array[String])

{

for (i < to()) // the leftarrow means assignment in Scala

Systemoutprintln(Counting + i)

}

}

实际上Scala 的 for 并不了解那些成员并且并不比其他任何对象类型做得更好它所了解的是 scalaIterablescalaIterable 定义了在集合上进行迭代的基本行为提供 Iterable 功能(从技术上说它是 Scala 中的一个特征但现在将它视为一个接口)的任何东西都可以用作 for 表达式的核心ListArray甚至是您自己的自定义类型都可以在 for 中使用

特殊性

正如上面已经证明的那样for 循环可以做许多事情并不只是遍历可迭代的项列表事实上可以使用一个 for 循环在操作过程中过滤许多项并在每个阶段都产生一个新列表

让 Scala 与英语更接近

您可能已经注意到理解清单 中的 Scala 的 for 循环版本更容易一些这要感谢 Range 对象暗中将两端都包含在内以下英语语言语法比 Java 语言更接近些假如有一条 Range 语句说 from to do this那么这意味着不再产生意外的 offbyone 错误

清单 看一看还有哪些优点

// This is Scala

object Application

{

def main(args : Array[String])

{

for (i < to ; i % == )

Systemoutprintln(Counting + i)

}

}

注意到清单 中 for 表达式的第二个子句了吗?它是一个过滤器实际上只有那些传递给过滤器(即计算 true)的元素 向前传给 了循环主体在这里只输出了 的偶数数字

并不要求 for 表达式的各个阶段都成为过滤器您甚至可以将一些完全平淡无奇的东西(从循环本身的观点来看)放入管道中例如以下代码显示了在下一个阶段进行计算之前的 i 的当前值

清单 让我如何爱上您呢?别那么冗长

// This is Scala

object App

{

def log(item : _) : Boolean =

{

Systemoutprintln(Evaluating + item)

true

}

def main(args : Array[String]) =

{

for (val i < to ; log(i); (i % ) == )

Systemoutprintln(Counting + i)

}

}

在运行的时候范围 中的每个项都将发送给 log它将通过显式计算每个项是否为 true 来 批准 每个项然后for 的第三个子句将对这些项进行筛选过滤出那些满足是偶数的条件的元素因此只将偶数传递给了循环主体本身

简单性

在 Scala 中可以将 Java 代码中复杂的一长串语句缩短为一个简单的表达式例如以下是遍历目录查找所有 scala 文件并显示每个文件名称的方法

清单 Finding scala

// This is Scala

object App

{

def main(args : Array[String]) =

{

val filesHere = (new javaioFile())listFiles

for (

file < filesHere;

if fileisFile;

if filegetNameendsWith(scala)

) Systemoutprintln(Found + file)

}

}

这种 for 过滤很常见(并且在此上下文中分号很让人讨厌)使用这种过滤是为了帮助您做出忽略分号的决定此外Scala 允许将上述示例中的圆括号之间的语句直接作为代码块对待

清单 Finding scala(版本

// This is Scala

object App

{

def main(args : Array[String]) =

{

val filesHere = (new javaioFile())listFiles

for {

file < filesHere

if fileisFile

if filegetNameendsWith(scala)

} Systemoutprintln(Found + file)

}

}

作为 Java 开发人员您可能发现最初的圆括号加分号的语法更直观一些没有分号的曲线括号语法很难读懂幸运的是这两种句法产生的代码是等效的

一些有趣的事

在 for 表达式的子句中可以分配一个以上的项如清单 中所示

清单 名称中有什么?

// This is Scala

object App

{

def main(args : Array[String]) =

{

// Note the arrayinitialization syntax; the type (Array[String])

// is inferred from the initialized elements

val names = Array(Ted Neward Neal Ford Scott Davis

Venkat Subramaniam David Geary)

for {

name < names

firstName = namesubstring( nameindexOf( ))

} Systemoutprintln(Found + firstName)

}

}

这被称为 中途赋值(midstream assignment)其工作原理如下定义了一个新值 firstName该值用于保存每次执行循环后的 substring 调用的值以后可以在循环主体中使用此值

这还引出了嵌套 迭代的概念所有迭代都位于同一表达式中

清单 Scala grep

// This is Scala

object App

{

def grep(pattern : String dir : javaioFile) =

{

val filesHere = dirlistFiles

for (

file < filesHere;

if (filegetNameendsWith(scala) || filegetNameendsWith(java));

line < scalaioSourcefromFile(file)getLines;

if linetrimmatches(pattern)

) println(line)

}

def main(args : Array[String]) =

{

val pattern = *object*

grep pattern new javaioFile()

}

}

在此示例中grep 内部的 for 使用了两个嵌套迭代一个在指定目录(其中每个文件都与 file 连接在一起)中找到的所有文件上进行迭代另一个迭代在目前正被迭代的文件(与 line 本地变量连接在一起)中发现的所有行上进行迭代

使用 Scala 的 for 结构可以做更多的事但目前为止提供的示例已足以表达我的观点Scala 的 for 实际上是一条管道它在将元素传递给循环主体之前处理元素组成的集合每次一个此管道其中的一部分负责将更多的元素添加到管道中(生成器)一部分负责编辑管道中的元素(过滤器)还有一些负责处理中间的操作(比如记录)无论如何Scala 会带给您与 Java 中引入的 增强的 for 循环 不同的体验

匹配

今天要了解的最后一个 Scala 控制结构是 match它提供了许多 Scala 模式匹配功能幸运的是模式匹配会声明对某个值进行计算的代码块首先将执行代码块中最接近的匹配结果因此在 Scala 中可以包含以下代码

清单 一个简单的匹配

// This is Scala

object App

{

def main(args : Array[String]) =

{

for (arg < args)

arg match {

case Java => println(Java is nice)

case Scala => println(Scala is cool)

case Ruby => println(Ruby is for wimps)

case _ => println(What are you a VB programmer?)

}

}

}

刚开始您可能将 Scala 模式匹配设想为支持 String 的 开关带有通常用作通配符的下划线字符而这正是典型开关中的默认情况但是这样想会极大地低估该语言模式匹配是许多(但不是大多数)函数语言中可以找到的另一个特性它提供了一些有用的功能

对于初学者(尽管这没什么好奇怪的)可能认为 match 表达式自身会产生一个值该值可能出现在赋值语句的右边正如 if 和 try 语句所做的那样这一点本身也很有用但匹配的真正威力体现在基于各种类型进行匹配时而不是如上所述匹配单个类型的值或者更多的时候它是两种匹配的组合

因此假设您有一个声明返回 Object 的函数或方法 —— 在这里Java 的 javalangreflectMethodinvoke() 方法的结果可能是一个好例子通常在使用 Java 语言计算结果时首先应该确定其类型但在 Scala 中可以使用模式匹配简化该操作

清单 您是什么?

//This is Scala

object App

{

def main(args : Array[String]) =

{

// The Any type is exactly what it sounds like: a kind of wildcard that

// accepts any type

def describe(x: Any) = x match {

case => five

case true => truth

case hello => hi!

case Nil => the empty list

case _ => something else

}

println describe()

println describe(hello)

}

}

因为 match 的很容易简单明了地描述如何针对各种值和类型进行匹配的能力模式匹配常用于解析器和解释器中在那里解析流中的当前标记是与一系列可能的匹配子句匹配的然后将针对另一系列子句应用下一个标记依此类推(注意这也是使用函数语言编写许多语言解析器编译器和其他与代码有关的工具的部分原因这些函数语言中包括 Haskell 或 ML)

关于模式匹配还有许多可说的东西但这些会将我们直接引导至 Scala 的另一个特性 case 类我想将它留到下次再介绍

结束语

Scala 在许多方面看起来都非常类似于 Java但实际上只有 for 结构存在一些相似性核心语法元素的函数特性不仅提供了一些有用的特性(比如已经提到的赋值功能)还提供了使用新颖有趣的方式扩展语言的能力不必修改核心 javac 编译器本身这使该语言更加符合 DSL 的定义(这些 DSL 是在现有语言的语法中定义的)并且更加符合编程人员根据一组核心原语(a la Lisp 或 Scheme)构建抽象的愿望

关于 Scala有如此多的内容可以谈论但我们这个月的时间已经用完了记得试用最新的 Scala bits(在撰写本文时是 final)并尝试提供的示例感受一下该语言的操作(请参阅 参考资料)请记住到下一次的时候Scala 会将一些有趣的(函数)特性放入编程中!

               

上一篇:环形缓沖器Java实现

下一篇:在Fedora Core上交付Java应用