【连载】Scala程序设计:Java虚拟机多核编程实战——Scala步入正轨

【连载】 Scala程序设计: Java虚拟机多核编程实战—— Scala步入正轨
2010年8月13日
名人名言:人生活在希望之中,旧的希望实现了,或者泯灭了,新的希望的烈焰随之燃烧起来。如果一个人只管活一天算一天,什么希望也没有,他的生命实际上也就停止了。——莫泊桑



     你可以基于自己已有的 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在 JavaScala里都等于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可以对可见性进行细粒度的控制。
?    Javaprotected很宽容。它包括了任何包的派生类加上当前包的任何类。 Scalaprotected与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  ScalaProtected

 
      在 Scala里,用 protected修饰的成员只对本类及派生类可见。同一个包的其他类无法访问这些成员。而且,派生类只可以访问本类内的 protected成员。我们通过一个例子看一下:
 
      编译上面代码,会得到如下错误:
 

      在上面的代码里,Vehicle的checkEngine()是 protected方法。 Scala允许我们从派生类Car的实例方法(start())访问这个方法,也可以在Car的实例方法(tow())里用Car的实例访问。不过, Scala不允许我们在Car里面用Vehicle实例访问这个方法,其他与Vehicle同包的类(GasStation)也不行。这个行为不同于 Java对待 protected访问的方式。 Scalaprotected成员访问的保护更加严格。

 

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)的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值