3.7 Scala带给Java程序员的惊奇
当你开始欣赏Scala设计的优雅与简洁时,也该小心Scala的一些细微差别——花些时间了解它们,可以避免出现意外。
3.7.1 赋值的结果
在Scala中,赋值运算(a=b
)的结果是Unit
。在Java里,赋值的结果是a
的值,因此类似于a = b = c
;这样成串的多重赋值可以出现Java里,但是不会出现在Scala里。因为赋值的结果是Unit
,所以把这个结果赋给另一个变量必然导致类型不匹配。看看下面这个例子:
ScalaForTheJavaEyes/SerialAssignments.scala
var a, b, c = 1
a= b=c
尝试执行上面的代码,我们会得到这样的编译错误:
(fragment of SerialAssignments.scala):3: error: type mismatch;
found : Unit
required: Int
a = b = c
^
one error found
!!!
discarding <script preamble>
这一行为同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()
方法。下面是个例子:
⑩知易行难。在继承体系中实现
equals()
是困难的,正如Joshua Bloch所著的Effective Java里所讨论的那样。
ScalaForTheJavaEyes/Equality.scala
val str1 = "hello"
val str2 = "hello"
val str3 = new String("hello")
println(str1 == str2) // Equivalent to Java's str1.equals(str2)
println(str1 eq str2) // Equivalent to Java's str1 == str2
println(str1 == str3)
println(str1 eq str3)
str1
和str2
引用了String
的同一个实例,因为Java会对第二个字符串"hello"
进行了intern
处理。不过,第三个字符串引用的是另一个新创建的String
实例。所有这三个引用指向的对象都持有相等的值(hello
)。str1
和str2
在身份上是相等的,因此,它们的值也是相等的。而str1
和str3
只是值相等,但身份不等。下面的输出说明了上面代码所用的==
和eq
方法/运算符的语义:
true
true
true
false
对于所有的类型来说,Scala的==
处理都是一致的,避免了在Java里使用==
的混淆。然而,你必须认识到这与Java在语义上的差异,以防意外发生。
3.7.3 分号是半可选的
在语句终结的问题上,Scala是很宽容的——分号(;
)是可选的,这会让代码看起来更简洁。当然,你也可以在语句末尾放置分号,尤其是想在同一行里放多个语句的时候。但要小心一些,在同一行上放多个语句也许会降低可读性,就像后面这样:val sample = new Sample; println(sample)
。
如果语句并不是以中缀(像+
,*
或是.
)结尾,或不在括号或方括号里,Scala可以推断出分号。如果下一个语句开头的部分是可以开启语句的东西,它也可以推断出分号。
然而,Scala需要在{之前有个分号。不放的结果可能会让你大吃一惊。我们看个例子:
ScalaForTheJavaEyes/OptionalSemicolon.scala
val list1 = new java.util.ArrayList[Int];
{
println("Created list1")
}
val list2 = new java.util.ArrayList[Int]
{
println("Created list2")
}
println(list1.getClass())
println(list2.getClass())
这会给出下面的输出:
Created list1
Created list2
class java.util.ArrayList
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节,“独立对象和伴生对象”)。如果想把成员变成private
或protected
,只要用对应的关键字标记一下即可,像这样:
ScalaForTheJavaEyes/Access.scala
class Microwave {
def start() = println("started")
def stop() = println("stopped")
private def turnTable() = println("turning table")
}
val microwave = new Microwave
microwave.start()
microwave.turnTable() //ERROR
上面的代码里,把start()
和stop()
两个方法定义成public
。通过任何Microwave
实例都可以访问这两个方法。另一方面,显式地把turnTable()
定义为private
,这样就不能在类外访问这个方法。像上面的例子一样,试一下就会得到下面的错误:
(fragment of Access.scala):9: error:
method turnTable cannot be accessed in this.Microwave
microwave.turnTable() //ERROR
^
one error found
!!!
discarding <script preamble>
public
字段和方法可以省去访问修饰符。而其他成员就要显式放置访问修饰符,按需求对访问进行限制。
3.7.6 Scala的Protected
在Scala里,用protected
修饰的成员只对本类及派生类可见。同一个包的其他类无法访问这些成员。而且,派生类只可以访问本类内的protected
成员。我们通过一个例子看一下:
ScalaForTheJavaEyes/Protected.scala
Line 1 package automobiles
-
- class Vehicle {
- protected def checkEngine() {}
5 }
-
- class Car extends Vehicle {
- def start() { checkEngine() /*OK*/ }
- def tow(car: Car) {
10 car.checkEngine() //OK
- }
- def tow(vehicle: Vehicle) {
- vehicle.checkEngine() //ERROR
- }
15 }
-
- class GasStation {
- def fillGas(vehicle: Vehicle) {
- vehicle.checkEngine() //ERROR
20 }
- }
编译上面代码,会得到如下错误:
Protected.scala:13: error: method checkEngine cannot be accessed in
automobiles.Vehicle
vehicle.checkEngine() //ERROR
^
Protected.scala:19: error: method checkEngine cannot be accessed in
automobiles.Vehicle
vehicle.checkEngine() //ERROR
^
two errors found
在上面的代码里,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
,那么仅有当前实例可以访问这个成员。
⑪我们会在第4章“Scala的类”中讨论伴生对象。
我们看一个细粒度访问控制的例子:
ScalaForTheJavaEyes/FineGrainedAccessControl.scala
Line 1 package society {
-
- package professional {
- class Executive {
5 private[professional] var workDetails = null
- private[society] var friends = null
- private[this] var secrets = null
-
- def help(another : Executive) {
10 println(another.workDetails)
- println(another.secrets) //ERROR
- }
- }
- }
15
- package social {
- class Acquaintance {
- def socialize(person: professional.Executive) {
- println(person.friends) // OK
20 println(person.workDetails) // ERROR
- }
- }
- }
- }
编译上面的代码,会得到如下错误:
FineGrainedAccessControl.scala:11: error: value secrets is not a member of
society.professional.Executive
println(another.secrets) //ERROR
^
FineGrainedAccessControl.scala:20: error: variable workDetails cannot be
accessed in society.professional.Executive
println(person.workDetails) // ERROR
^
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
标记private
和protected
。比如,上面的代码里,secret
标记为private[this]
,在实例方法中,只有隐式的那个对象(this
)才可以访问——无法通过其他实例访问。类似的,标记为protected[this]
的字段只有派生类的实例方法可以访问,但仅限于当前实例。
3.7.8 避免显式return
在Java里,用return
从方法中返回结果。在Scala里,这不是个好做法。Scala见到return
就会跳出方法。至少,它会影响到Scala推演返回类型的能力。
ScalaForTheJavaEyes/AvoidExplitReturn.scala
def check1() = true
def check2() : Boolean = return true
println(check1)
println(check2)
在上面代码里,对于使用了return
的方法,就需要显式提供返回类型;如果不这么做,会有编译错误。最好避免显式使用return
语句。我倾向于让编译推演返回类型,就像方法check1()
那样。
本章,从Java程序员角度快速领略了Scala,见识到了Scala类似于Java的方面,同时,也看到了它的不同之处。你已经开始感受Scala的力量,本章应该已经为你全面学习Scala做好了准备。在下一章里,你会看到Scala是如何支持OO范式(paradigm)的。