3.7 Scala带给Java程序员的惊奇

当你开始欣赏Scala设计的优雅与简洁时,也该小心Scala的一些细微差别——花些时间了解它们,可以避免出现意外。

3.7.1 赋值的结果

在Scala中,赋值运算(a=b)的结果是Unit。在Java里,赋值的结果是a的值,因此类似于a = b = c;这样成串的多重赋值可以出现Java里,但是不会出现在Scala里。因为赋值的结果是Unit,所以把这个结果赋给另一个变量必然导致类型不匹配。看看下面这个例子:

ScalaForTheJavaEyes/SerialAssignments.scala

  1. var a, b, c = 1
  2. a= b=c

尝试执行上面的代码,我们会得到这样的编译错误:

  1. (fragment of SerialAssignments.scala):3: error: type mismatch;
  2. found : Unit
  3. required: Int
  4. a = b = c
  5. ^
  6. one error found
  7. !!!
  8. discarding <script preamble>

这一行为同Scala提供的运算符重载差不多,会让人觉得有那么一点心烦。

3.7.2 Scala的==

对于基本类型和对象,Java处理==的方式截然不同。对于基本类型,==表示基于值的比较,而对于对象来说,这是基于身份的比较⑨。因此,假设ab都是int,如果二者变量值相等,那么a==b的结果就是true。然而,如果它们都是指向对象的引用,那么只有在两个引用都指向相同的实例时,结果才是true,也就是说它们具有相同的身份。Java的equals()方法为对象提供了基于值的比较,假定这个方法被恰当的类正确地改写过。

⑨这里指的就是引用。——译者注

Scala对==的处理不同于Java;不过,它对于所有类型的处理是一致的。在Scala里,无论类型如何,==都表示基于值的比较。这点由Any类(Scala所有类都是从它派生而来)把==()实现成final得到了保证。这个实现用到了完美的旧equals()方法。

因此,如果想为某个类的对比方法提供特定的实现,就要改写equals()⑩。如果要实现基于值的比较,可以使用简洁的==,而不是equals()方法。如果想对引用执行基于身份的比较,可以使用eq()方法。下面是个例子:

⑩知易行难。在继承体系中实现equals()是困难的,正如Joshua Bloch所著的Effective Java里所讨论的那样。


ScalaForTheJavaEyes/Equality.scala

  1. val str1 = "hello"
  2. val str2 = "hello"
  3. val str3 = new String("hello")
  4. println(str1 == str2) // Equivalent to Java's str1.equals(str2)
  5. println(str1 eq str2) // Equivalent to Java's str1 == str2
  6. println(str1 == str3)
  7. println(str1 eq str3)

str1str2引用了String的同一个实例,因为Java会对第二个字符串"hello"进行了intern处理。不过,第三个字符串引用的是另一个新创建的String实例。所有这三个引用指向的对象都持有相等的值(hello)。str1str2在身份上是相等的,因此,它们的值也是相等的。而str1str3只是值相等,但身份不等。下面的输出说明了上面代码所用的==eq方法/运算符的语义:

  1. true
  2. true
  3. true
  4. false

对于所有的类型来说,Scala的==处理都是一致的,避免了在Java里使用==的混淆。然而,你必须认识到这与Java在语义上的差异,以防意外发生。

3.7.3 分号是半可选的

在语句终结的问题上,Scala是很宽容的——分号(;)是可选的,这会让代码看起来更简洁。当然,你也可以在语句末尾放置分号,尤其是想在同一行里放多个语句的时候。但要小心一些,在同一行上放多个语句也许会降低可读性,就像后面这样:val sample = new Sample; println(sample)

如果语句并不是以中缀(像+*或是.)结尾,或不在括号或方括号里,Scala可以推断出分号。如果下一个语句开头的部分是可以开启语句的东西,它也可以推断出分号。

然而,Scala需要在{之前有个分号。不放的结果可能会让你大吃一惊。我们看个例子:

ScalaForTheJavaEyes/OptionalSemicolon.scala

  1. val list1 = new java.util.ArrayList[Int];
  2. {
  3. println("Created list1")
  4. }
  5. val list2 = new java.util.ArrayList[Int]
  6. {
  7. println("Created list2")
  8. }
  9. println(list1.getClass())
  10. println(list2.getClass())

这会给出下面的输出:

  1. Created list1
  2. Created list2
  3. class java.util.ArrayList
  4. class Main$$anon$2$$anon$1

定义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节,“独立对象和伴生对象”)。如果想把成员变成privateprotected,只要用对应的关键字标记一下即可,像这样:

ScalaForTheJavaEyes/Access.scala

  1. class Microwave {
  2. def start() = println("started")
  3. def stop() = println("stopped")
  4. private def turnTable() = println("turning table")
  5. }
  6. val microwave = new Microwave
  7. microwave.start()
  8. microwave.turnTable() //ERROR

上面的代码里,把start()stop()两个方法定义成public。通过任何Microwave实例都可以访问这两个方法。另一方面,显式地把turnTable()定义为private,这样就不能在类外访问这个方法。像上面的例子一样,试一下就会得到下面的错误:

  1. (fragment of Access.scala):9: error:
  2. method turnTable cannot be accessed in this.Microwave
  3. microwave.turnTable() //ERROR
  4. ^
  5. one error found
  6. !!!
  7. discarding <script preamble>

public字段和方法可以省去访问修饰符。而其他成员就要显式放置访问修饰符,按需求对访问进行限制。

3.7.6 Scala的Protected

在Scala里,用protected修饰的成员只对本类及派生类可见。同一个包的其他类无法访问这些成员。而且,派生类只可以访问本类内的protected成员。我们通过一个例子看一下:

ScalaForTheJavaEyes/Protected.scala

  1. Line 1 package automobiles
  2. -
  3. - class Vehicle {
  4. - protected def checkEngine() {}
  5. 5 }
  6. -
  7. - class Car extends Vehicle {
  8. - def start() { checkEngine() /*OK*/ }
  9. - def tow(car: Car) {
  10. 10 car.checkEngine() //OK
  11. - }
  12. - def tow(vehicle: Vehicle) {
  13. - vehicle.checkEngine() //ERROR
  14. - }
  15. 15 }
  16. -
  17. - class GasStation {
  18. - def fillGas(vehicle: Vehicle) {
  19. - vehicle.checkEngine() //ERROR
  20. 20 }
  21. - }

编译上面代码,会得到如下错误:

  1. Protected.scala:13: error: method checkEngine cannot be accessed in
  2. automobiles.Vehicle
  3. vehicle.checkEngine() //ERROR
  4. ^
  5. Protected.scala:19: error: method checkEngine cannot be accessed in
  6. automobiles.Vehicle
  7. vehicle.checkEngine() //ERROR
  8. ^
  9. two errors found

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

3.7.7 细粒度访问控制

一方面,Scala对待protected修饰符比Java更严格。另一方面,就设定访问的可见性而言,它提供了极大的灵活性以及更细粒度的控制。privateprotected修饰符可以指定额外的参数。这样,相比于只用private修饰成员,现在可以用private[Access- Qualifier]修饰,其中AccessQualifier可以是this(表示只有实例可见),也可以是外围类的名字或包的名字。读作“这个成员对所有类都是private,当前类及其伴生对象⑪例外。如果AccessQualifier是个类名,则例外情况还要包括AccessQualifier所表示的外部类及其伴生对象。如果AccessQualifier是一个外围包名,那么这个包里的类都可以访问这个成员。如果AccessQualifierthis,那么仅有当前实例可以访问这个成员。

⑪我们会在第4章“Scala的类”中讨论伴生对象。

我们看一个细粒度访问控制的例子:

ScalaForTheJavaEyes/FineGrainedAccessControl.scala

  1. Line 1 package society {
  2. -
  3. - package professional {
  4. - class Executive {
  5. 5 private[professional] var workDetails = null
  6. - private[society] var friends = null
  7. - private[this] var secrets = null
  8. -
  9. - def help(another : Executive) {
  10. 10 println(another.workDetails)
  11. - println(another.secrets) //ERROR
  12. - }
  13. - }
  14. - }
  15. 15
  16. - package social {
  17. - class Acquaintance {
  18. - def socialize(person: professional.Executive) {
  19. - println(person.friends) // OK
  20. 20 println(person.workDetails) // ERROR
  21. - }
  22. - }
  23. - }
  24. - }

编译上面的代码,会得到如下错误:

  1. FineGrainedAccessControl.scala:11: error: value secrets is not a member of
  2. society.professional.Executive
  3. println(another.secrets) //ERROR
  4. ^
  5. FineGrainedAccessControl.scala:20: error: variable workDetails cannot be
  6. accessed in society.professional.Executive
  7. println(person.workDetails) // ERROR
  8. ^
  9. two errors found

先来观察一下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标记privateprotected。比如,上面的代码里,secret标记为private[this],在实例方法中,只有隐式的那个对象(this)才可以访问——无法通过其他实例访问。类似的,标记为protected[this]的字段只有派生类的实例方法可以访问,但仅限于当前实例。

3.7.8 避免显式return

在Java里,用return从方法中返回结果。在Scala里,这不是个好做法。Scala见到return就会跳出方法。至少,它会影响到Scala推演返回类型的能力。

ScalaForTheJavaEyes/AvoidExplitReturn.scala

  1. def check1() = true
  2. def check2() : Boolean = return true
  3. println(check1)
  4. println(check2)

在上面代码里,对于使用了return的方法,就需要显式提供返回类型;如果不这么做,会有编译错误。最好避免显式使用return语句。我倾向于让编译推演返回类型,就像方法check1()那样。

本章,从Java程序员角度快速领略了Scala,见识到了Scala类似于Java的方面,同时,也看到了它的不同之处。你已经开始感受Scala的力量,本章应该已经为你全面学习Scala做好了准备。在下一章里,你会看到Scala是如何支持OO范式(paradigm)的。