5.5 Scala的OOP功能
与Java一样,Scala编译器也要求将代码封装在类中。为了满足这种要求,Scala的交互式REPL shell自动将你输入的代码封装在一个在幕后生成的不可见的类中。执行命令scala后,你可以立即开始编写要执行的函数或代码,就像使用Python或其他脚本语言时那样。本节介绍如下主题:
- 定义包和子包;
- 导入成员;
- 定义类;
- 实例方法和实例变量;
- 构造函数;
- 扩展类;
- 重载方法;
- 抽象类;
- 特质;
- 单例对象;
- 运算符重载;
Case类。
5.5.1 定义包和子包
Scala提供了package语句,你可以在文件开头使用它:
package PACKAGENAME
这种语句的工作原理与Java中完全相同:在前述文件中定义的每个类都将放在PACKAGENAME包中。
然而,Scala让你有更大的控制权。稍后你将看到,Scala还支持子包:在Scala中,前缀相同的包将自动关联起来。在源代码文件中,可使用package语句来定义子包:
package com.example.parentclass A {}package subpackage {class B { }class C { }}
上述代码创建了三个公有类,其中类A位于com.example.parent包中,而类B和C位于com.example.parent.subpackage包中:

在REPL shell中,不能使用
package语句,这是因为这个shell将生成一个不可见的类,其中包含你输入的代码。如果源代码包含package语句,就必须使用编译器scalac来编译它。
5.5.2 导入成员
在Scala中,import语句的功能比在Java中更强大,但其基本形式与Java中一样:
import com.example.parent.A
为导入包的所有成员,Scala不使用通配符*,而使用_:
import com.example.parent._
可在同一条语句中导入多个成员:
import com.example.parent.subpackage.{B, C}
也可将导入的成员重命名:
import com.example.parent.subpackage.{C => D}
在前面的示例中,在源代码中引用com.example.parent.subpackage.C类时,必须使用类名D。这在名称发生冲突(不同的成员同名)时提供了极大的便利。
前面说过,Scala支持子包,而子包可访问其父包的私有成员。在前面的示例中,这意味着com.example.parent.subpackage中的代码能够访问com.example.parent的私有成员——甚至都不需要导入这些类。
另一个很不错的功能是可导入包:
import com.example.parent
有了上述代码后,就可在代码中引用parent包的成员:
var c = new parent.subpackage.C()
5.5.3 定义类
从前面的一些示例可知,在Scala中定义类的方式与在Java中很像:
class TheClassName {}
在Scala中,可在一个源代码文件中定义任意数量的公有类。源代码文件的名称不必与其定义的任何类相同。
对于类,Scala支持如下显式的访问限定符:
private
在没有指定访问限定符的情况下,类默认为公有的,这与Java差别很大。在Scala中,不能创建包私有的类,也没有显式的访问限定符public。如果要创建一个空类,可不指定代码块{ }。
5.5.4 实例变量和实例方法
类可包含实例变量和实例方法。
Scala不支持静态成员(类变量和类方法),因此没有static或与之等价的关键字。为填补这种空白,Scala支持单例类,这将在本章后面介绍。
- 实例变量
要添加实例变量,只需在类中定义def和val变量即可。在大多数情况下,可不显式地指定变量的类型,因此Scala将根据初始值推断变量的类型,但如果你愿意,也可以显式地指定:
var anIntegerVariable: Intval anIntegerValue = 0
在显式地指定了类型的情况下,可不立即显式地对实例变量进行初始化,在这种情况下,它将被自动初始化为空值。没有指定类型时,必须在声明变量的同时给它赋值,否则Scala将不知道变量是哪种类型。
- 实例方法
在Scala中,方法声明与Java中很像。如果方法有输入参数,必须指定其类型,另外,方法可什么都不返回,也可返回一个对象实例。对于返回类型,可显式地指定,也可不指定:
def methodName(parameter1: Int, parameter2: Int): Int = {parameter1 + parameter2}
与Java不同的一个点是,可不显式地指定返回类型——除非Scala编译器认为存在二义性。在前面的示例中,编译器知道返回值为两个Int实例的和,因此返回类型必然是Int。因此,将其修改成下面这样也能通过编译:
def methodName(parameter1: Int, parameter2: Int) = {parameter1 + parameter2}
在任何方法中,都将自动返回最后一个表达式的值。Scala提供了显式的return语句,但并非必须使用它,同时推荐不使用它。在Scala中,方法和函数不能提早结束。使用了return语句时,必须显式地指定方法的返回类型。
在Scala中,如果方法什么都没有返回(相当于Java中的void),可将返回类型指定为类名Unit:
def methodWithoutReturnValue(): Unit = {}
如果没有指定返回类型,且方法中没有只包含一个表达式的代码行,Scala将认为返回类型为Unit:
def methodWithoutReturnValue() = {}
如果方法显式地将返回类型指定为Unit,同时又包含只有一个表达式的代码行,编译器将发出警告,同时忽略这个表达式。
如果方法的实现只包含一行代码,则可省略表示代码块的{ },因此下面的代码是合法的:
def helloWorld() = println("Hello world")
- 用于实例成员的访问限定符
与类一样,在没有显式指定访问限定符的情况下,类成员也是公有的。Scala支持的其他访问限定符如下:
protectedprivate
在访问限定符方面,Scala和Java存在一些重要的差别。Scala支持子包的概念。子包中的类可访问其父包的私有成员,这在前面介绍
import语句时讨论过。- 在Scala类中,使用访问限定符
protected的成员对当前包中的其他类来说是不可见的,而在Java中,受保护的成员对当前包中的其他类来说是可见的。
5.5.5 构造函数
主构造函数是通过在类代码块中添加参数和输入类型定义的:
class ClassWithParameterizedConstructor(var parm1: Int, parm2: Int){println("This code is executed as part of the constructor")}
这定义了一个主构造函数以及实例变量parm1和parm2(这两个变量的类型都是Int)。这里有几点需要注意。
- Scala自动创建与参数同名的字段。
- 对于
var字段,将自动为其生成公有的获取方法和设置方法,因此能够访问这个类的代码可随便访问构造函数的参数。 - 没有指定关键字
var或val时,默认为val。 - 类的所有代码都可访问这些变量和值,嵌套的类、方法和函数亦如此。
主构造函数被调用时,将执行类中的语句。构造函数可重载;在Scala中,重载的构造函数被称为辅助构造函数(auxiliary constructor)。要创建其他的构造函数,可使用关键字this:
class ClassWithParameterizedConstructor(var parm1: Int,val parm2: Int) {def this(parm1: Int) = this(parm1, 0)}
与方法一样,如果构造函数包含多行代码,可在等号后面使用{ }将这些代码括起:
class ClassWithParameterizedConstructor(var parm1: Int,val parm2: Int) {def this(parm1: Int) = {this(parm1, 0)}}
对于辅助构造函数,有一条重要的规则:在辅助构造函数中,必须首先调用主构造函数或其他已定义的辅助构造函数。
5.5.6 扩展类
类是使用你熟悉的关键字extends来扩展的:

class ParentClass {}class SubClass extends ParentClass {}
像Java一样,Scala也支持单继承。如果没有显式地继承任何类,将隐式地继承AnyRef类(Any的子类)。
在Scala中,只有子类的主构造函数能够调用父类的构造函数,这是像下面这样完成的:
class ParentClass(param1: Int, param2: Int) {}class SubClass(var param1: Int) extends ParentClass(param1, 10) {}
SubClass的主构造函数(接受一个参数)调用ParentClass的主构造函数(接受两个参数)。如果ParentClass有辅助构造函数,也可以调用它们。
稍后你将看到,关键字extends也用于实现特质(trait)。通过扩展类和实现特质时,必须先指定要扩展的类。
重写方法
要在子类中重写方法,可使用关键字override:
class ParentClass {def test() = print("Hello, from the parent class")}class SubClass extends ParentClass {override def test() = {super.test()print(" and from the child class as well")}}
正如这里演示的,要访问父类的成员,可使用关键字super。
5.5.7 重载方法
Scala支持方法重载,其中的工作原理和遵循的规则与Java中相同,下面是一个这样的示例:
class OverloadExample {def anOverloadedMethod(i: Int) { }def anOverloadedMethod(s: String) { }}
5.5.8 抽象类
要创建抽象类,可在关键字class前面加上abstract:
abstract class AbstractClassName {def methodWithNoImplementationYetdef methodWithImplementation() { }}
不同于Java,抽象方法无需使用关键字abstract指定,而只需不给它提供实现。
5.5.9 特质
特质很像Java接口。与Java 8接口一样,特质可定义抽象方法,也可定义包含实现的方法。下面来看一个示例:
trait TraitName {def methodWithImplementation() {// 代码在这里……}def methodWithoutImplementation()}
Scala使用关键字extends来扩展父类,还使用它来实现特质。在Java中,类可以实现任意数量的接口;同样,在Scala中,类也可以扩展任意数量的特质。

有趣的是,extends列表中的条目是使用关键字with分隔的:
trait TraitA { def method1() }trait TraitB { def method2() }trait TraitC { def method3() }class TraitsDemoClass extends TraitA with TraitB with TraitC {def method1() { }def method2() { }def method3() { }}
注意,同时扩展一个类以及一个或多个特质时,必须先指定要扩展的类,否则将出现编译错误。
扩展一个或多个特质的抽象类可以给特质的抽象方法提供实现,但并非必须这样做。具体类必须提供所有特质的实现,这可以是直接提供的(如前面所示),也可以是间接提供的(如扩展其他提供了特质实现的类)。
5.5.10 单例对象
Scala提供了一种便利的对象。这种对象很像类定义,但不同之处在于,它不仅创建类,还创建一个可通过指定名称引用的对象实例。这就是单例类,即确保只创建其一个实例,如下例所示:
object SingletonObjectName {var x = 100def printX() = println(x)}
这个对象不需要实例化,因为Scala将自动完成这种工作。要访问实例SingletonObjectName,只需使用其名称即可:
SingletonObjectName.x = 250SingletonObjectName.printX()
这将打印250。当然,在单例对象中添加可修改的变量不是什么好主意,在它将被多个线程使用时尤其如此。任何时候,使用可修改的全局变量都是馊主意。
鉴于Scala不支持静态类成员,可将单例对象作为替代品。由于这种类在任何情况下都只有一个实例,因此使用它与将数据存储在静态变量中或定义静态方法的效果相同。
5.5.11 运算符重载
前面讨论AnyVal子类时说过,Scala支持运算符重载。在Java中,不能重写+和*等运算符,你只能将它们用于基本类型(将它们用于包装类时,将在内部将包装类拆箱为基本类型,执行完计算后再装箱为包装类)。
在Java中执行1 + 1时,Java编译器将二进制Java字节码编译成知道如何将两个整数值相加的JVM命令。不能将Java运算符用于自定义类,如果你对自定义类的实例调用+,编译器将拒绝编译代码。例如,下面的Java代码不能通过编译:
class A {public static void main(String[] args) {// 编译错误:二元运算符+的操作数的类型不正确A result = new A() + new A();}}
而Scala以普通方法的方式实现了运算符。Scala类Int包含方法+,因此在Scala中,可在自定义类中重写并实现运算符。如果你在自定义类中重写了运算符+,则当你在代码中使用运算符+将两个自定义类的实例(甚至两个不同类的实例)相加时,方法+将决定如何做。下面是一个简单的Scala自定义类,它实现了运算符+:
class CustomClass(var x: Int) {def + (other: CustomClass) = {new CustomClass(x + other.x)}}val result = new CustomClass(400) + new CustomClass(155)print(result.x)
这将打印555。
5.5.12 Case类
Scala支持一种特殊的类——Case类。作为程序员,你可能见过很像但又存在细微差别的数据结构。为处理这些数据结构,通常必须编写难以维护的switch… case(C、C#、Java和JavaScript)、case…when(Ruby)、Select Case(Visual BASIC)或if …else块(其他语言),从中获取正确的数据并做相应的处理。
Case类为处理这种问题提供了优雅的途径,我们来看一个简单的示例:

Case类Rectangle、Circle和Line扩展了抽象类Figure:
abstract class Figurecase class Rectangle(x1: Int, y1: Int, x2: Int, y2: Int) extendsFigurecase class Circle(x: Int, y: Int, diameter: Int) extends Figurecase class Line(x1: Int, y1: Int, x2: Int, y2: Int) extends Figure
首先,创建了空的抽象类Figure,以便能够将Case类进行逻辑分组。我们声明了一些Case类,它们表示绘图程序能够绘制的各种图形,其中每个Case类都包含相应图形所需的字段。创建这些类的实例易如反掌:
val rectangle = Rectangle(10, 20, 80, 50)val circle = Circle(100, 200, 30)
有趣的是,实例化Case类时没有使用关键字new。要处理这些类,需要使用一种名为模式匹配的技术:
def drawFigure(figure: Figure): Unit = {figure match {case Rectangle(x1, y1, _, _) => _draw(x1, y1)case Circle(x, y, _) => _draw(x, y)case Line(x1, y1, _, _) => _draw(x1, y1)}def _draw(x: Int, y: Int): Unit = println("Start drawing at "+ x + ", " + y)}drawFigure(rectangle)drawFigure(circle)
根据约定,使用下划线(_)来表示当前不需要的字段。
