【连载】Scala程序设计:Java虚拟机多核编程实战——Scala步入正轨
你可以基于自己已有的Java技能学习Scala。在本章中,我们从熟悉的地方——Java代码——出发,向Scala前进。Scala在一些地方同Java类似,但差异之处更是不胜枚举。Scala偏爱纯粹的面向对象,但是它也会尽可能的把类型映射为Java类型。Scala支持类Java的命令式编程风格,同时也支持函数式风格。启动你最喜爱的编辑器,我们要开启Scala之旅了!
3.1 把Scala当作简洁的Java
Scala拥有非常高的代码密度——键入更少却获得更多。我们从一段Java代码的例子开始:
输出如下:
Scala使上面的代码变得更简洁。首先,它并不关心是否使用分号。其次,在如此简单的例子里,把代码放到Greetings类里并没有什么真正的益处,因此,可以去掉。再次,没有必要指定变量i的类型。Scala很聪明,足以推演出i是一个整数。最后,Scala使用println输出字符串,不必键入System.out.println。把上面的代码简化成Scala,如下:
运行上面的Scala脚本,键入scala Greetings.scala,或是在IDE里运行。
你应该看到这样的输出:
Scala的循环结构相当轻量级。只要说明索引i的值是从1到3即可。箭头(<-)左边定义了一个val,而不是var(参见下面的注解),右边是一个生成器表达式(generator expression)。每次循环都会创建一个新的val,用产生出来的连续值进行初始化。
val vs. var
不管是val还是var,都可以用来定义变量。用val定义的变量是不可变的,初始化之后,值就固定下来了。用var定义的变量是可变的,修改多少次都行。
这里的不变性指的是变量本身,而不是变量所引用的实例。比如说,如果写val buffer = new StringBuffer(),就不能把buffer指向其他的引用。但我们依然可以用诸如append()之类的方法来修改StringBuffer的实例。
另外,如果用val str = "hello"定义了一个String的实例,就不能再做修改了,因为String本身也是不可变的。要想让一个类的实例不可变,可以把它的所有字段都定义为val,然后只提供读取实例状态的方法,不提供修改的方法。
在Scala里,应该尽量优先使用val,而不是var;这可以提升不变性和函数式风格。
上面代码产生的范围包含了下界(1)和上界(3)。用until()方法替换to()方法,就可以从范围内排除上界。
你会看到如下输出:
是的,你没听错。我确实说to()是方法了。实际上,to()和until()都是RichInt的方法 ,这个类型是由Int隐式转换而来的,而Int是变量i的推演类型。这两个函数返回的是一个Range的实例。因此,调用1 to 3等价于1.to(3),但前者更优雅。在下面的注解中,我们会更多的讨论这一迷人的特性。
点和括号是可选的
如果方法有0或1个参数,点和括号是可以丢掉的。如果方法的参数多于一个,就必须使用括号,但是点仍然是可选的。你已经看到这样做的益处:a+b实际上是a.+(b),1 to 3实际上是1.to(3)。
利用这样轻量级语法的优势,可以创建出读起来更自然的代码。比如,假设为类Car定义了一个turn()方法:
用轻量级语法调用上面的方法,如下:
享受可选的点和括号,削减代码中的杂乱吧!
在上面的例子里,看上去是在循环迭代中给i重新赋了值。然而,i并不是一个var,而是一个val。每次循环都创建一个不同的val,名字叫做i。注意,我们不可能由于疏忽在循环里修改了i的值,因为i是不变的。这里,我们已经悄然向函数式风格迈进了一步。
使用foreach(),还可以用更贴近函数式的风格执行循环:
输出如下:
上面的例子很简洁,没有赋值。我们用到了Range类的foreach()方法。这个方法以一个函数值作为参数。所以,要在括号里面提供一段代码体,接收一个实参,在这个例子里面命名为i。=>将左边的参数列表和右边的实现分离开来。
3.2 Java基本类型对应的Scala类
Java世界呈现出一个割裂的现象,有对象,有基本类型,比如int、double等。Scala把一切都视为对象。
Java把基本类型同对象区分对待。从Java 5开始,自动装箱可以为对象方法传递基本类型。然而,Java不支持在基本类型上调用方法,像这样:2.toString()。
与之不同的是,Scala把一切都视为对象。也就是说可以在字面量上调用对象,就像调用对象方法一样。下面的代码创建了一个Scala的Int实例,将其传给java.util. ArrayList的ensureCapacity()方法,这个方法需要传入一个Java基本类型int。
在上面的代码里 ,Scala悄悄地把Scala.Int当作Java的基本类型Int。其结果是不会在运行时因为类型转换而带来性能损耗。
类似的魔法还有,对Int调用类似于to()这样的方法,比如,1.to(3)或是1 to 3。当Scala确定Int无法满足要求时,就会悄悄地应用intWrapper()方法把Int转化 为scala.runtime.RichInt,然后调用它的to()方法。
诸如RichInt,RichDouble,RichBoolean之类的类,叫做富封装类。这些类表示Java的基本类型和字符串,它们提供了一些便捷方法,可以在Scala类里使用。
3.3 元组与多重赋值
假定有个函数要返回多个值。比如,返回一个人的名、姓和email地址。如果使用Java的话,一种方式是返回一个PersonInfo类的实例,其中包括与那些数据对应的字段。另一种方式是返回一个包含这些值的String[]或ArrayList,然后对结果进行循环,取出这些值。Scala提供了一种更简单的方式做这件事:元组和多重赋值。
元组是一个不变的对象序列,可以用逗号分隔的值进行创建。比如,下面表示一个有3个对象的元组:("Venkat", "Subramaniam", "venkats@agiledeveloper.com")。
元组元素可以同时赋给多个var或val,如下面这个例子所示:
执行这段代码的输出如下:
如果尝试将方法结果赋给数量不一致的变量会怎么样呢?Scala会密切地关注你,一旦这种情况发生,它就会报错。如果是编译代码,而不是作为脚本执行,Scala在编译时就会提示错误。比如,下面这个例子,将方法调用的结果赋值给数量少于元组个数的变量。
Scala会报告这样的错误:
就算不赋值,也可以访问元组里的单个元素。比如,如果执行了val info = getPer- sonInfo(1),就可以用这样的语法info._1,访问第一个元素,第二个用info._2,以此类推。
元组不仅仅对多重赋值中有用。在并发编程里,使用元组可以把一组数据值作为消息在Actor之间传递(它们不变的属性刚好在这里派得上用场)。如此简洁的语法会让消息发送端的代码极为精炼。在接收端,使用模式匹配会让接收和处理消息变得简单,在9.3节“匹配元组和list”中会详细介绍。
3.4 字符串与多行原始字符串
Scala的字符串只不过是java.lang.String,可以按照Java的方式使用字符串。不过,Scala还为使用字符串提供了一些额外的便利。
Scala可以自动把String转换成scala.runtime.RichString——这样你就可以无缝地使用诸如capitalize()、lines()和reverse这样一些便捷的方法 。
在Scala里,创建多行字符串真的很容易,只要把多行字符串放在3个双引号间("""...""")即可。这是Scala对于here document,或者叫heredoc的支持。这里,我们创建了一个3行长的字符串:
输出如下:
Scala允许在字符串里嵌入双引号。Scala会将三个双引号里的内容保持原样,在Scala里,称为原始字符串。实际上,Scala处理字符串有些望文生义;如果不想把代码里的缩进带到字符串里,可以用RichString的便捷方法stripMargin(),像这样:
stripMargin()会去掉先导管道符(|)前所有的空白或控制字符。如果出现在其他地方,而不是每行的开始,就会保留管道符。如果出于某种原因,这个符号有特殊的用途,可以用stripMargin()方法的变体,接收你所选择的其他边缘(margin)字符。上面代码的输出如下:
创建正则表达式时,你会发现原始字符串非常有用。键入和阅读"""\d2:\d2"""可比"\\d2:\\d2"容易。
3.5 自适应的默认做法
Scala有一些默认做法,会让代码更简洁、更易读写。下面列了几个这样的特性:
? 它支持脚本,无需将所有的代码都放到类里。如果脚本可以满足需求,就把可执行代码直接放到文件里,而不必弄出一个没必要的垃圾类。
? return是可选的。方法调用会自动返回最后求值的表达式,假定它符合方法声明的返回类型。不显式地放置return会使代码更简洁,特别是传闭包做方法参数时。
? 分号(;)是可选的。不必在每个语句的后面都写上分号 ,这会使代码更简洁。如果想在同一行内放多条语句,可以用分号进行分隔。Scala很聪明,能识别出语句是否完整,如果语句包含多行可以在下一行继续输入。
? 类和方法默认是public,因此不必显式使用public关键字。
? Scala提供了轻量级的语法创建JavaBean——用更少的代码创建变量和final属性(参见4.1节,“创建类”)。
? 不会强制捕获一些不关心的异常(参见13.1节,“异常处理”),降低了代码规模,也避免了不恰当的异常处理。
另外,默认情况下,Scala会导入两个包和scala.Predef对象,以及相应的类和成员。只要用类名,就可以引用这些预导入的包。Scala按如下顺序将内容全部导入:
? java.lang
? scala
? scala.Predef
包含java.lang让你无需在脚本中导入任何东西就可以使用常用的Java类型。比如,使用String时,无需加上java.lang的包名,也不必导入。
因为scala包中的所有内容都导入了,所以也可以很容易地使用Scala的类型。
Predef对象包含了类型,隐式转换,以及Scala中常用的方法。既然这些类是默认导入的,不需要任何前缀,也无需导入,即可使用这些方法和转换。这些操作非常便捷,以至于刚开始,你会把它们当作是语言的一部分,实际上,它们是Scala程序库的一部分。
Predef对象也为诸如scala.collection.immutable.Set和scala.collection. immutable.Map这样的东西提供了别名。比如,引用Set或Map,实际上引用的是他们在Predef中的定义,它们会依次转换为其在scala.collection.immutable包里的定义。
3.6 运算符重载
从技术的角度来看,Scala没有运算符,提及“运算符重载”时,指的是重载像+,+-等这样的符号。在Scala里,这些实际上是方法名:运算符利用了Scala灵活的方式调用语法——在Scala里,对象引用和方法名之间的点(.)不是必需的。
这两个特性给了我们运算符重载的错觉。这样,调用ref1 + ref2,实际上写的是ref1.+(ref2),这是在调用ref1.的+()方法。看个+运算符的例子,来自Complex类,这个类表示复数 :
如果执行上面的代码会看到:
在第一个语句中,创建了一个名为Complex的类,定义一个构造函数,接收两个参数。在4.1节中,我们会看到如何用Scala富有表现力的语法创建类。
在+方法里创建了一个新的Complex类实例。结果的实部和虚部分别对应两个运算数实部和虚部的和。语句c1 + c2会变成一个方法调用,以c2为实参调用c1的+()方法,也就是c1.+(c2)。
我们讨论了Scala对运算符重载简单而优雅的支持。不过,Scala没有运算符,这个事实也许会让人有点头痛。或许,你会对运算符优先级感到困惑。Scala没有运算符,所以它无法定义运算符优先级,对吗?恐怕不是,因为24 - 2 + 3 * 6在Java和Scala里都等于40。Scala确实没有定义运算符优先级,但它定义了方法的优先级。
方法名的第一个字符决定了它的优先级 。如果表达式里有两个具有相同优先级的字符,那么左边的运算符优先级更高。下面从低到高列出了首字符的优先级 :
我们看个运算符/方法优先级的例子。下面的代码里,我们为Complex既定义了加法方法,又定义了乘法方法:
调用*()前,会先调用了在左边的+(),但是因为*()优先,它会先执行,如下所示:
3.7 Scala带给Java程序员的惊奇
当你开始欣赏Scala设计的优雅与简洁时,也该小心Scala的一些细微差别——花些时间了解它们,可以避免出现意外。
3.7.1 赋值的结果
在Scala中,赋值运算(a=b)的结果是Unit。在Java里,赋值的结果是a的值,因此类似于a = b = c;这样成串的多重赋值可以出现Java里,但是不会出现在Scala里。因为赋值的结果是Unit,所以把这个结果赋给另一个变量必然导致类型不匹配。看看下面这个例子:
尝试执行上面的代码,我们会得到这样的编译错误:
这一行为同Scala提供的运算符重载差不多,会让人觉得有那么一点心烦。
3.7.2 Scala的==
对于基本类型和对象,Java处理==的方式截然不同。对于基本类型,==表示基于值的比较,而对于对象来说,这是基于身份的比较 。因此,假设a和b都是int,如果二者变量值相等,那么a==b的结果就是true。然而,如果它们都是指向对象的引用,那么只有在两个引用都指向相同的实例时,结果才是true,也就是说它们具有相同的身份。Java的equals()方法为对象提供了基于值的比较,假定这个方法被恰当的类正确地改写过。
Scala对==的处理不同于Java;不过,它对于所有类型的处理是一致的。在Scala里,无论类型如何,==都表示基于值的比较。这点由Any类(Scala所有类都是从它派生而来)把==()实现成final得到了保证。这个实现用到了完美的旧equals()方法。
因此,如果想为某个类的对比方法提供特定的实现,就要改写equals() 。如果要实现基于值的比较,可以使用简洁的==,而不是equals()方法。如果想对引用执行基于身份的比较,可以使用eq()方法。下面是个例子:
str1和str2引用了String的同一个实例,因为Java会对第二个字符串"hello"进行了intern处理。不过,第三个字符串引用的是另一个新创建的String实例。所有这三个引用指向的对象都持有相等的值(hello)。str1和str2在身份上是相等的,因此,它们的值也是相等的。而str1和str3只是值相等,但身份不等。下面的输出说明了上面代码所用的==和eq方法/运算符的语义:
对于所有的类型来说,Scala的==处理都是一致的,避免了在Java里使用==的混淆。然而,你必须认识到这与Java在语义上的差异,以防意外发生。
3.7.3 分号是半可选的
在语句终结的问题上,Scala是很宽容的——分号(;)是可选的,这会让代码看起来更简洁。当然,你也可以在语句末尾放置分号,尤其是想在同一行里放多个语句的时候。但要小心一些,在同一行上放多个语句也许会降低可读性,就像后面这样:val sample = new Sample; println(sample)。
如果语句并不是以中缀(像+,*或是.)结尾,或不在括号或方括号里,Scala可以推断出分号。如果下一个语句开头的部分是可以开启语句的东西,它也可以推断出分号。
然而,Scala需要在{之前有个分号。不放的结果可能会让你大吃一惊。我们看个例子:
这会给出下面的输出:
定义list1时,放了一个分号。因此,紧随其后的{开启了一个新的代码块。然而,定义list2时没有放分号,Scala假定我们要创建一个匿名内部类,派生自ArrayList [Int]。这样,list2指向了这个匿名内部类的实例,而不是一个直接的ArrayList[Int]实例。如果你的意图是创建实例之后开启一个新的代码块,请放一个分号。
Java程序员习惯于使用分号。是否应该在Scala里继续使用分号呢?在Java里,你别无选择。Scala给了你自由,我推荐你去利用它。少了这些分号,代码会变得简洁而清爽。丢弃了分号,你可以开始享受优雅的轻量级语法。当不得不解决潜 在歧义时,请恢复使用分号。
3.7.4 默认的访问修饰符
Scala的访问修饰符不同于Java:
? 如果不指定任何访问修饰符,Java默认为包内可见。而Scala默认为public。
? Java提供的是一个超然物外的语言。要么对当前包所有的类可见,要么对任何一个都不可见。Scala可以对可见性进行细粒度的控制。
? Java的protected很宽容。它包括了任何包的派生类加上当前包的任何类。Scala的protected与C++或C#同源——只有派生类可以访问。不过,Scala也可以给予protected更自由、更灵活的解释。
? 最后,Java的封装是在类一级。在实例方法里,可以访问任何类实例的私有字段和方法。这也是Scala的默认做法;不过,也可以限制为当前实例,就像Ruby所提供的一样。
我们用一些例子来探索这些不同于Java的变化。
3.7.5 默认的访问修饰符以及如何修改
默认情况下,如果没有访问修饰符,Scala会把类、字段和方法都当作public(4.2节,“定义字段、方法和构造函数”)。把主构造函数变成private也是相当容易。(4.5节,“独立对象和伴生对象”)。如果想把成员变成private或protected,只要用对应的关键字标记一下即可,像这样:
上面的代码里,把start()和stop()两个方法定义成public。通过任何Microwave实例都可以访问这两个方法。另一方面,显式地把turnTable()定义为private,这样就不能在类外访问这个方法。像上面的例子一样,试一下就会得到下面的错误:
public字段和方法可以省去访问修饰符。而其他成员就要显式放置访问修饰符,按需求对访问进行限制。
3.7.6 Scala的Protected
在Scala里,用protected修饰的成员只对本类及派生类可见。同一个包的其他类无法访问这些成员。而且,派生类只可以访问本类内的protected成员。我们通过一个例子看一下:
编译上面代码,会得到如下错误:
在上面的代码里,Vehicle的checkEngine()是protected方法。Scala允许我们从派生类Car的实例方法(start())访问这个方法,也可以在Car的实例方法(tow())里用Car的实例访问。不过,Scala不允许我们在Car里面用Vehicle实例访问这个方法,其他与Vehicle同包的类(GasStation)也不行。这个行为不同于Java对待protected访问的方式。Scala对protected成员访问的保护更加严格。
3.7.7 细粒度访问控制
一方面,Scala对待protected修饰符比Java更严格。另一方面,就设定访问的可见性而言,它提供了极大的灵活性以及更细粒度的控制。private和protected修饰符可以指定额外的参数。这样,相比于只用private修饰成员,现在可以用private[Access- Qualifier]修饰,其中AccessQualifier可以是this(表示只有实例可见),也可以是外围类的名字或包的名字。读作“这个成员对所有类都是private,当前类及其伴生对象 例外。如果AccessQualifier是个类名,则例外情况还要包括AccessQualifier所表示的外部类及其伴生对象。如果AccessQualifier是一个外围包名,那么这个包里的类都可以访问这个成员。如果AccessQualifier是this,那么仅有当前实例可以访问这个成员。
我们看一个细粒度访问控制的例子:
编译上面的代码,会得到如下错误:
先来观察一下Scala怎样定义嵌套包。就像C++或C#的命名空间一样,Scala允许在一个包中嵌套另一个包。因此,可以按照Java的风格定义包(使用点号,比如package society.professional;),也可以用C++或C#的嵌套风格。如果 要在一个文件里放同一个包层次结构的多个小类(又一个偏离Java的地方),你会发现后一种风格方便些。
上面的代码里,Executive的私有字段workDetails对外围包professional里的类可见,私有字段friends对外围包society里的类可见。这样,Scala允许Acquaintance类——在society包里——访问friends字段,而不能访问workDetails。
private默认的可见性在类一级——可在类的实例方法里访问同一个类中标记为private的成员。不过,Scala也支持用this标记private和protected。比如,上面的代码里,secret标记为private[this],在实例方法中,只有隐式的那个对象(this)才可以访问——无法通过其他实例访问。类似的,标记为protected[this]的字段只有派生类的实例方法可以访问,但仅限于当前实例。
3.7.8 避免显式return
在Java里,用return从方法中返回结果。在Scala里,这不是个好做法。Scala见到return就会跳出方法。至少,它会影响到Scala推演返回类型的能力。
在上面代码里,对于使用了return的方法,就需要显式提供返回类型;如果不这么做,会有编译错误。最好避免显式使用return语句。我倾向于让编译推演返回类型,就像方法check1()那样。
本章,从Java程序员角度快速领略了Scala,见识到了Scala类似于Java的方面,同时,也看到了它的不同之处。你已经开始感受Scala的力量,本章应该已经为你全面学习Scala做好了准备。在下一章里,你会看到Scala是如何支持OO范式(paradigm)的。
---------------------------------------------------------------------------------------
1.诚邀合作: 如果您想翻译、写作或是推荐IT类图书,我们将竭诚为您提供最专业的服务平台。
2.联系我们: yuany@turingbook.com(E-Mail)
3.欢迎加入图灵社区与作译者、业内人士进行交流: