到目前为止,在本系列文章中,我一直专注于Scala对Java生态系统的忠诚度,向您展示Scala如何整合Java的大部分核心对象功能。 但是,如果Scala只是编写对象的另一种方式,那么它就不会像它那样有趣或如此强大。 Scala的功能和对象概念的结合,以及对程序员效率的强调,使得学习这种语言比Java-cum-Scala程序员可能立即意识到的更为复杂和微妙。
例如,采用Scala的方法来控制诸如if
, while
和for
构造。 尽管这些对您来说看起来像是老式的旧Java构造,但是Scala为它们赋予了一些截然不同的特性。 在让您犯了很多错误(并因此编写了错误的代码)之后,不要冒险冒险发现差异,而是本月的文章介绍了在Scala中使用控件结构时的期望入门。
重新访问Person.scala
在本系列的最后一篇文章中 ,您看到Scala可以通过定义模仿基于POJO的环境所需的传统“获取器和设置器”的方法来定义POJO。 这篇文章发表后,我收到了比尔,即将到来的规范斯卡拉参考的合着者, 在Scala编程电子邮件(参见相关主题 )。 Bill指出了使用scala.reflect.BeanProperty
注释的一种更简单的方法,如下所示:
清单1.修改后的Person.scala
class Person(fn:String, ln:String, a:Int)
{
@scala.reflect.BeanProperty
var firstName = fn
@scala.reflect.BeanProperty
var lastName = ln
@scala.reflect.BeanProperty
var age = a
override def toString =
"[Person firstName:" + firstName + " lastName:" + lastName +
" age:" + age + " ]"
}
清单1中的方法(是我上一篇文章中清单13的修订版)为指定的var
生成get/set
方法对。 唯一的缺点是这些方法实际上并不存在于Scala代码中,因此无法被其他Scala代码调用。 通常这没什么大不了的,因为Scala会在它自己生成的字段周围使用生成的方法。 但如果您不提前知道,可能会感到惊讶。
查看清单1中的代码,最让我印象深刻的是Scala不仅展示了功能和对象概念相结合的功能。 它还展示了在Java最初发布13年后思考对象语言的一些好处。
控制是一种幻想
您将要看到的许多奇特的魔力是由于Scala的功能特性,因此可能需要对功能语言的开发和演化有一些了解。
在功能语言中,通常不直接在语言中构建越来越高级的结构。 相反,该语言由一组核心的原始构造定义。 当与将函数作为对象传递的功能结合在一起时,结果就是高阶函数 ,这些函数可用于定义看起来像是核心语言之外的功能,但实际上只是一个库。 与任何库一样,此功能可以替换,扩充或扩展。
从一组核心原始语言构建语言的这种组成性质具有悠久而丰富的历史,最早可以追溯到1960年代和1970年代的Smalltalk,Lisp和Scheme。 特别是像Lisp和Scheme这样的语言,由于其在较低级别的抽象之上定义较高级别的抽象的能力而引起了极大的关注。 程序员采用了高级抽象,并使用它们来构建更多的高级抽象。 当你听到今天讨论的这个过程中,它通常是参照特定领域语言,或DSL的(参见相关主题 )。 但是,实际上,它只是关于在抽象之上构建抽象。
在Java语言中,唯一的选择是使用API调用来完成此操作。 在Scala中,您可以通过扩展语言本身来实现。 试图扩展Java语言可能会导致严重的情况,从而威胁到整个程序的稳定性。 尝试扩展Scala只是意味着创建一个新的库。
如果只有我的朋友
我们将从传统的if
构造开始-当然,这必须是最简单的地址之一,对吗? 毕竟,从理论上讲, if
仅检查条件,如果条件为true,则执行后面的代码块。
啊,但是这种简单性可能是欺骗。 传统上,Java语言一直在做else
的条款if
可选的,假设你可以直接跳过的代码块,如果条件为假。 但是,用功能语言则不是这样。 为了与功能语言的数学性质保持一致,所有内容都必须评估为表达式,包括if
子句本身。 (对于Java开发人员,这正是三元运算符- ?:
表达式的工作方式。)
在Scala中,必须存在非true块(该块的else
部分),并且必须产生与if
块相同的值。 这意味着无论代码执行哪种方式,总会产生一个值。 考虑以下Java代码,例如:
清单2.哪个配置文件? (Java版本)
// This is Java
String filename = "default.properties";
if (options.contains("configFile"))
filename = (String)options.get("configFile");
因为Scala中的if
构造本身就是一个表达式,所以您可以将上面的代码重写为清单3中所示的更正确的代码段:
清单3.哪个配置文件? (Scala版本)
// This is Scala
val filename =
if (options.contains("configFile"))
options.get("configFile")
else
"default.properties"
但是,真正的胜利是在Scala中,可以编写代码以将结果分配给val
而不是var
。 设置后,就无法更改val
了,就像final
变量以Java语言操作一样。 不变的局部变量最显着的副作用是现在并发变得更容易。 尝试在Java代码中做同样的事情会使您陷入许多人认为好的可读代码的边缘,如清单4所示:
清单4.哪个配置文件? (Java,三元样式)
//This is Java
final String filename =
options.contains("configFile") ?
options.get("configFile") : "default.properties";
在代码审查中解释一个可能很棘手。 也许是正确的,但是许多Java程序员会显得有些ance跷,问:“您这样做是为了什么?”
当你不在的时候 ...
接下来,让我们看一下while
和他的小表亲do-while
。 从根本上讲,它们每个都做完全相同的事情:它测试一个条件,如果为true,则继续执行所提供的代码块。
通常,功能语言避免使用while
循环,因为while
所做的大部分事情都可以通过递归来完成。 函数式语言真的很喜欢递归。 考虑,例如,在quicksort
中的“Scala中通过实施例”中所示关断实现(见相关信息 ),其附带Scala实现:
清单5. Quicksort(Java版本)
//This is Java
void sort(int[] xs) {
sort(xs, 0, xs.length -1 );
}
void sort(int[] xs, int l, int r) {
int pivot = xs[(l+r)/2];
int a = l; int b = r;
while (a <= b)
while (xs[a] < pivot) { a = a + 1; }
while (xs[b] > pivot) { b = b – 1; }
if (a <= b) {
swap(xs, a, b);
a = a + 1;
b = b – 1;
}
}
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) ,用于存储当前数据点。 请注意,此版本甚至利用了自身的递归性,两次调用自身,一次对列表的左侧进行排序,再一次对列表的右侧进行排序。
只需说一下清单5中的quicksort
并不十分容易就可以理解了,不用多说了。 现在,考虑Scala中的直接等效项(意思是,与上述版本尽可能接近):
清单6. 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 sort1(l: Int, r: Int) {
val pivot = xs((l + r) / 2)
var i = l; var j = r
while (i <= j) {
while (xs(i) < pivot) i += 1
while (xs(j) > pivot) j -= 1
if (i <= j) {
swap(i, j)
i += 1
j -= 1
}
}
if (l < j) sort1(l, j)
if (j < r) sort1(i, r)
}
sort1(0, xs.length 1)
}
清单6中的代码本身看起来非常接近Java版本。 这就是说,这很长,很丑陋,很难推理(尤其是在并发性方面),并且显然没有Java版本的优势。
所以,我会改善它...
清单7. Quicksort(更好的Scala版本)
//This is Scala
def sort(xs: Array[Int]): Array[Int] =
if (xs.length <= 1) xs
else {
val pivot = xs(xs.length / 2)
Array.concat(
sort(xs filter (pivot >)),
xs filter (pivot ==),
sort(xs filter (pivot <)))
}
显然,清单7中的Scala代码更简单。 请注意使用递归来完全避免while
循环。 并在Array
类型上使用filter
函数,以将“大于”,“等于”和“小于”功能应用于其中的每个元素。 并且,要启动一个事实,因为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
构造允许您编写如下代码:
清单8.如果最初您没有成功,那么...
// This is Scala
val url =
try {
new URL(possibleURL)
}
catch {
case ex: MalformedURLException =>
new URL("www.tedneward.com")
}
清单8中的代码与清单2或清单3中的if
示例相去甚远。 实际上,用传统的Java代码编写将非常棘手,特别是如果您希望将捕获的值存储在一个不变的位置中(如清单4中的最后一个示例那样)。 Scala的功能性又一个得分!
清单8中的case ex:
语法是另一个Scala构造的一部分, match表达式 ,用于Scala中的模式匹配。 稍后,我们将探讨模式匹配,这是功能语言的常见功能。 现在,只把它当作一个概念,那就是switch/case
什么C风格struct
s为上课。
现在花点时间考虑一下异常处理。 当然,Scala的支持很有趣,因为它是一种表达,但是开发人员经常想要的一件事是处理异常的标准化方法,而不仅仅是捕获异常的能力。 在AspectJ中,这是通过创建围绕代码部分编织的方面来完成的,这些方面由切入点定义,如果您希望在代码库的不同部分中针对不同种类的异常使用不同的行为,则必须仔细编写这些切入点SQLExceptions
应该以不同方式处理从IOExceptions
等等。
在Scala中,这很简单。 看。
清单9.异常的自定义表达式
// This is Scala
object Application
{
def generateException()
{
System.out.println("Generating exception...");
throw new Exception("Generated exception");
}
def main(args : Array[String])
{
tryWithLogging // This is not part of the language
{
generateException
}
System.out.println("Exiting main()");
}
def tryWithLogging (s: => Unit) {
try {
s
}
catch {
case ex: Exception =>
// where would you like to log this?
// I choose the console window, for now
ex.printStackTrace()
}
}
}
就像前面讨论的While
构造一样, tryWithLogging
代码只是从库(或者,在同一类中)发出的函数调用。 在适当的情况下,可以使用该主题的不同变体,而无需编写复杂的切入点代码。
这种方法的优点在于,它利用Scala的能力捕获一流构造中的跨领域逻辑-以前只有面向方面的人群才可以声称。 清单9中的一流构造捕获异常(已检查和未检查),并以特定方式处理它们。 从上述想法衍生出来的内容仅受想象力的限制。 您只需要记住,Scala与许多功能语言一样,允许您将代码块( 也称为函数)作为参数并根据需要应用它们。
一种“ for”代语言
所有这些将我们带到了Scala控件构造套件的真正强大之处: for
构造。 看起来,对Java的for
循环增强功能的简单抛弃远比普通Java程序员可以想象的要强大得多。
让我们先来看一下Scala如何通过一个集合处理简单的顺序迭代,您可以从Java编程中很好地了解这一点:
清单10.一劳永逸
// This is Scala
object Application
{
def main(args : Array[String])
{
for (i <- 1 to 10) // the left-arrow means "assignment" in Scala
System.out.println("Counting " + i)
}
}
这几乎达到了您的预期,循环10次并每次打印出值。 但是,请注意这一点:“ 1到10”表达式并不意味着Scala具有内置的整数意识以及如何从1到10进行计数。从技术上讲,这里发生的事情更加微妙:编译器正在使用在Int
类型to
定义的方法(Scala中的所有对象都是对象,还记得吗?)以生成Range
对象,该对象包含要迭代的元素。 如果按照Scala编译器的方式重写上面的代码,它将看起来像这样:
清单11.编译器看到的内容
// This is Scala
object Application
{
def main(args : Array[String])
{
for (i <- 1.to(10)) // the left-arrow means "assignment" in Scala
System.out.println("Counting " + i)
}
}
因此,Scala的for
实际上并不比其他对象类型更好地理解数字。 它了解的是scala.Iterable
,它定义了在集合中进行迭代的基本行为。 提供Iterable
功能的任何东西(从技术上来说,是Scala的特征 ,但现在将其视为接口)都可以用作for
表达式的核心。 List
, Array
甚至您自己的自定义类型都可以在for
。
为了特异性
事实证明, for
循环不仅可以遍历可迭代的项目列表,还可以做更多的事情。 实际上,可以使用for
循环来过滤项目,并在每个阶段生成一个新列表:
清单12.计算我爱你的方式(不奇怪)
// This is Scala
object Application
{
def main(args : Array[String])
{
for (i <- 1 to 10; i % 2 == 0)
System.out.println("Counting " + i)
}
}
注意清单12中for
表达式的第二个子句吗? 它是一个过滤器,只有通过过滤器的那些元素(即,值为true )实际上会“继承”到循环的主体。 在这种情况下,仅打印从1到10的偶数。
for
表达式的各个阶段也不必是“仅”过滤器。 您甚至可以将完全无关紧要的东西(从循环本身的角度)滑入管道中。 例如,下面的内容仅显示i的当前值,然后在下一阶段对其进行评估:
清单13.我如何爱你? 详细地说,宝贝
// This is Scala
object App
{
def log(item : _) : Boolean =
{
System.out.println("Evaluating " + item)
true
}
def main(args : Array[String]) =
{
for (val i <- 1 to 10; log(i); (i % 2) == 0)
System.out.println("Counting " + i)
}
}
运行时,范围为1到10的每个项目都将发送到log
,该log
将通过显式评估为true来默认“批准”每个项目。 但随后, for
的第三子句就会出现,仅过滤掉那些满足偶数准则的元素。 因此,再次,仅偶数将被传递到循环本身。
为了简洁
Java代码中的一系列复杂语句可以在Scala中简化为一个相当简单的表达式。 例如,这是浏览目录以查找所有.scala文件并显示每个文件名称的一种方法:
清单14.查找.scala
// This is Scala
object App
{
def main(args : Array[String]) =
{
val filesHere = (new java.io.File(".")).listFiles
for (
file <- filesHere;
if file.isFile;
if file.getName.endsWith(".scala")
) System.out.println("Found " + file)
}
}
这种过滤是很常见的(在这种情况下,分号也很烦人),值得做出省略分号的决定。 相反,Scala允许您将上面示例中括号之间的语句视为直接作为代码块:
清单15.查找.scala(版本2)
// This is Scala
object App
{
def main(args : Array[String]) =
{
val filesHere = (new java.io.File(".")).listFiles
for {
file <- filesHere
if file.isFile
if file.getName.endsWith(".scala")
} System.out.println("Found " + file)
}
}
作为Java开发人员,您可能会发现原始的带分号括号语法更加直观,而没有弯头的curlies-no-分号语法更难以阅读。 幸运的是,两种语法方法在它们产生的代码方面都是相同的。
为了娱乐
您可以在for
表达式的子句中分配多个项目,如清单16所示。
清单16.名称是什么?
// This is Scala
object App
{
def main(args : Array[String]) =
{
// Note the array-initialization 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 = name.substring(0, name.indexOf(' '))
} System.out.println("Found " + firstName)
}
}
这称为“中间流分配”,它的工作原理几乎与此处所示的一样:新值firstName
定义为每次通过循环都保存substring
调用的值,您可以在循环体内使用它之后。
这也引起了嵌套迭代的思想,它们都在同一表达式内:
清单17. Scala grep
// This is Scala
object App
{
def grep(pattern : String, dir : java.io.File) =
{
val filesHere = dir.listFiles
for (
file <- filesHere;
if (file.getName.endsWith(".scala") || file.getName.endsWith(".java"));
line <- scala.io.Source.fromFile(file).getLines;
if line.trim.matches(pattern)
) println(line)
}
def main(args : Array[String]) =
{
val pattern = ".*object.*"
grep pattern new java.io.File(".")
}
}
在此示例中, grep
的for
内部使用了两个嵌套的迭代-一次迭代指定目录中找到的所有文件(每个文件都绑定到file
,另一次迭代该文件中找到的所有行)当前正在迭代(绑定到本地line
)。
您可以使用Scala的for
结构做更多的事情,但是到目前为止的示例都希望我能指出:Scala的for
实际上是一个管道,在将元素集合传递到主体之前一次处理它们。 这条管线的部分可以添加更多的元素融入到管道(发电机),编校元素出来的管道(过滤器),或在两者之间(的地方,如日志例子。无论如何,斯卡拉为您带来的“增强型很长的路要走for
Java 5中引入的“循环”。
匹配我
您今天将了解到的最后一个Scala控件构造是match
,它提供了许多Scala的模式匹配功能。 从根本上讲,模式匹配声明根据值评估的代码块。 最接近的第一匹配导致所述代码块被执行。 因此,例如,在Scala中,您可以:
清单18.一个简单的匹配
// 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的java.lang.reflect.Method.invoke()
方法的结果。 通常,使用Java语言来评估结果,您首先要确定其类型和下调; 但是,在Scala中,您可以使用模式匹配来简化此操作:
清单19.您是什么?
//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 5 => "five"
case true => "truth"
case "hello" => "hi!"
case Nil => "the empty list"
case _ => "something else"
}
println describe(5)
println describe("hello")
}
}
由于match具有轻松,简明地描述代码应如何与各种值和类型进行匹配的能力,因此模式匹配通常在解析器和解释器中使用,其中解析流中的当前标记与一系列可能的match子句匹配。 然后将下一个标记应用于另一个序列,依此类推。 (请注意,这部分是为什么许多语言解析器,编译器和其他与代码相关的工具都用Haskell或ML等功能语言编写的原因。)
关于模式匹配,还有很多要说的话,但这将使我们直接转到Scala的另一个功能,即case类 ,我想我会再保留一天。
结论
在很多方面,Scala在外观上与Java类似,并且在for
构造中,这无处可比。 核心语法元素的功能性质提供了一些有用的功能(例如已经提到的赋值功能),还提供了以新颖有趣的方式扩展语言的能力,而无需修改核心javac
编译器本身。 这使得语言都更适合于内部DSL(被定义现有语言的语法里面那些DSL的)的定义,并从程序员一组核心原语, 一拉 Lisp或计划寻求建立抽象起来。
在Scala中有很多谈论,但是我们这个月的时间已经过去了。 记住要抓住最新的Scala语言(在撰写本文时为2.7.0-final),并尝试使用提供的示例来了解这种语言的运行方式(请参阅参考资料 )。 请记住,直到下一次,Scala会将乐趣(功能性)重新投入编程!
翻译自: https://www.ibm.com/developerworks/java/library/j-scala03268/index.html