类和对象

本章内容

  • 类、字段、方法
  • 单例对象

4.1 类、字段、方法

一旦你定义好一个类,就可以用new关键字来定义来创建对象。

1
2
3
4
class ChecksumAccumulator {
//类的定义
}
new ChecksumAccumulator

类的定义中,包含成员变量(field)和方法(method),统称为成员(menber).通过var或val 定义的field是指向对象的变量,通过def 定义成员方法。
当你实例化一个类,运行时候回分配一些内存来保存变量的数据。例如定义一个ChecksumAccumulator类并给它一个sum的var类型字段。

1
2
3
class ChecksumAccumulator {
var sum = 0
}

然后你实例化了2次:
val acc = new ChecksumAccumulator
val csa = new ChecksumAccumulator

那么内存中这两个对象看上去可能是这个样子的:

image.png
由于sum这个对象定义在ChecksumAccumulator类中的字段是var,而不是val,可以在后续的代码中进行重新赋值,如:

1
acc.sum =3

如此一来,内存中的对象看上去就如同:
image.png

这张图共存有2个sum变量,一个位于acc指向的对象里,一个位于csa指向的对象里。每个实例都有自己的变量(实例变量),这些变量合在一起就构成了对象在内存中的映像。从图中可以看出,改变其中一个实例的sum变量值,另一个并不受影响。
由于acc和csa都是val而不是var,你不能做的是将他们重新赋值指向别的对象。例如,下面的代码会报错:

1
2
//不能编译,因为acc 是一个val类型
acc = new ChecksumAccumulator

acc 永远只能指向你在初始化的时候用的ChecksumAccumulator对象,但对象中的字段是可能会改变的。
追求健壮性的一个重要手段是确保对象的状态(她的实例变量值)在其整个生命周期都是有效的。 首先是将字段标记为私有类型(private)来防止外部直接访问字段。因为私有字段只能被定义在同一个类中的方法访问,所有对状态的更新操作的代码,都在类的内部。要将某个字段声明为私有,可以在字段前加上private 这个变量修饰符。如

1
2
3
class ChecksumAccumulator {
private var sum = 0
}

有了ChecksumAccumulator的定义,任何试图通过外部访问sum的sum操作都会失败:

1
2
val acc = new ChecksumAccumulator
acc.sum=5 //不能编译,因为sum 是私有的

在scala 中,默认的访问修饰符是public,不用显示的声明。换句话说,对于那些在Java中可能会用到”public”的地方,在Scala中都不用显示的声明,声明都不用说了,直接上!公共访问是Scala的默认访问级别。

由于sum是私有的,唯一能访问sum的代码都定义在类自己里面,因此,ChecksumAccumulator对于别人来说没什么用,除非给它定义一些方法。

1
2
3
4
5
6
7
8
9
class ChecksumAccumulator{
private var sum=0
def add(b:Byte):Unit = {
sum+=b
}
def checksum():Int = {
return ~(sum&0xFF)+1
}
}

ChecksumAccumulator现在有2个方法,add 和 checksum ,都是函数定义的基本形式。
传递给方法的任何参数都能在方法内部使用,Scala方法参数一个重要特征是他们都是val类型的变量,而不是var。采用val的原因是val更容易推敲,而不需要像var一样进行查证val是不是被重新赋值过。
隐私如果你试图在Scala的方法中对入参重新复制,则编译会报错。

1
2
3
4
5
6
7
8
9
10
class ChecksumAccumulator{
private var sum=0
def add(b:Byte):Unit = {
b=1
sum+=b
}
def checksum():Int = {
return ~(sum&0xFF)+1
}
}

报错信息: error: reassignment to val

在Scala方法中,在没有任何显示的return语句时,Scala方法返回的该方法最后一个表达式的值。所以checksum方法最后一个return是多余的。
函数式编程风格推荐避免任何显示的return语句,尤其是多个return语句。尽量将每个方法当作是最终交出某个值的表达式。这样的哲学思想鼓励你编写短小的方法,将一个复杂方法分解成成小的方法,每个方法专注本身功能。另外一方面,设计的选择取决于上下文,取决于你的需求,如果你觉得你是非常必须的,Scala是允许你编写多个显示的return方法。

当一个方法只会计算一个返回结果的表达式时,可以不用写花括号。如果这个表达式很短也可以写在一行。
我们还可以选择省略返回类型,Scala会自动帮助我们做类型推断。

做出这些修改后,ChecksumAccumulator类看上去是这个样子的。

1
2
3
4
5
class ChecksumAccumulator{
private var sum=0
def add(b:Byte) = sum+=b
def checksum() = ~(sum&0xFF)+1
}

关于分号

在Scala 程序中,每条语句最后的分号通常是可选的。你想要的话可以键入一个,但如果当前行只有这条语句,分号并不是必需的。另一方面,如果想
在同一行包含多条语句,那么分号就有必要了:

1
val s = "hello" ; println(s)

单例对象

Scala不允许有静态(static) 成员。对于此类使用场景,Scala提供了单例对象(singleton object)。单例对象的定义看上去和类的定义很想。只不过class关键词被换成object关键词。
当单例对象跟某个类共同使用一个名字时候,它被称为该类的伴生对象(companion object)。必须在同一个文件中定义类和类的伴生对象。同时类又叫做这个单例对象的伴生类(companion class)。类和它的伴生对象可以互相访问对方的私有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
import scala.collection.mutable
object ChecksumAccumulator{
private val cache = mutable.Map.empty[String,Int]
def calculate(s:String):Int =
if (cache.contains(s)) cache(s)
else {
val acc = new ChecksumAccumulator
for (c <-s) acc.add(c.toByte)
val cs = acc.checksum()
cache+=(s->cs)
cs
}
}

在上面代码中,else后面中,定义了一个ChecksumAccumulator类型的实例acc,当只有ChecksumAccumulator单例对象定义时候,并不能定义一个定义了一个ChecksumAccumulator类型的实例变量。确切的说类型为ChecksumAccumulator的实例变量,只能由单例对象的伴生类来定义。

类和单例对象的一个区别是单例对象不能接受参数,而类可以,由于你没法用new 来实例化单例对象,也就没法用任何手段来向它传入参数。单例对象只会在第一次访问的时候才会被初始化。

没有同名伴生类的单例对象称为孤立对象(standalone object)。孤立对象有很多种用途,包括将工具方法归集在一起,或者定义Scala应用程序的入口等。

App特质

Scala 提供了一个特质scala.App ,帮助你节省敲键盘的动作。要用这个特质,首先要在你的单例对象后加上 extend App 。 然后,并不是直接编写main方法,而是将你打算放在main方法的代码直接写在单例对象的花括号中。可以通过名为args 的字符串数组来访问命令行参数。就这么简单,可以像任何其他应用程序一样来编译和运行它。

1
2
3
4
5
import ChecksumAccumulator.calculate
object FallwinterSpringSummer extends App {
for (season <- List("fall","winter","spring"))
println( season+":"+calculate(season))
}