【Scala之旅】类与对象
本节翻译自
综述:本节中你将会学习如何使用Scala实现类,以及Scala相比Java更加精简的表示法带来的便利。同时介绍了object的语法结构(Scala没有静态方法或静态字段,但object可以达到同样的效果)。
类
Scala 中的类是创建对象的模板。它们可以包含统称为成员的方法、值、变量、类型、对象、特征和类。之后将介绍类型、对象和特征。
定义类
最小的类定义就是关键字 class 和标识符。类名应该大写。
class User val user = new User
关键字 new 用于创建类的一个实例。User 有一个不带参数的默认构造函数,因为没有定义构造函数。但是,你通常需要构造函数和类体。下面是一个示例类的定义:
class Point(var x: Int, var y: Int) {
def move(dx: Int, dy: Int): Unit = {
x = x + dx
y = y + dy
}
override def toString: String = s"($x, $y)"
}
val point1 = new Point(2, 3)
point1.x // 2
println(point1) // prints (x, y)这个 Point 类有四个成员:变量 x 和 y,以及方法 move 和 toString。与其他语言不同的是,主构造函数在类签名中 (var x:Int, var y:Int)。move 方法接受两个整数参数,并返回 Unit:不包含任何信息的值 ()。这大致相当于java类语言中的 void。另一方面,toString 不接受任何参数,但返回一个字符串值。由于 toString 覆盖了 AnyRef 中的 toString,它被关键字 override 所标记。
构造器
通过提供默认值,构造函数可以具有可选参数,如下所示:
class Point(var x: Int = 0, var y: Int = 0) val origin = new Point // x and y are both set to 0 val point1 = new Point(1) println(point1.x) // prints 1
在这个版本的 Point 类中,x 和 y 的默认值为 0,所以不需要参数。但是,因为构造函数会读取从左到右的参数,如果你只想传递一个 y 值,那么你需要加上该参数的名称。
class Point(var x: Int = 0, var y: Int = 0) val point2 = new Point(y=2) println(point2.y) // prints 2
这也是一种提高清晰度的好习惯。
私有成员和getter/setter语法
默认情况下,成员是公开的。使用 private 修饰符可使得它们对类外部来说是不可见的。
class Point {
private var _x = 0
private var _y = 0
private val bound = 100
def x = _x
def x_= (newValue: Int): Unit = {
if (newValue < bound) _x = newValue else printWarning
}
def y = _y
def y_= (newValue: Int): Unit = {
if (newValue < bound) _y = newValue else printWarning
}
private def printWarning = println("WARNING: Out of bounds")
}
val point1 = new Point
point1.x = 99
point1.y = 101 // prints the warning在这个版本的 Point 类中,数据存储在私有变量 _x 和 _y 中。而定义的方法 def x 和 def y 则可以访问私有数据。def x_= 和 def y_= 用于验证和设置 _x 和 _y 的值。请注意 setter 的特殊语法:方法将 _= 附加到 getter 的标识符后面,并且跟着参数。
使用 val 和 var 的主构造函数参数是公开的。但是,因为 val 是不可变的,所以不能写下面的内容。
class Point(val x: Int, val y: Int) val point = new Point(1, 2) point.x = 3 // <-- does not compile
没有 val 或 var 的参数是私有值,只在类中可见。
class Point(x: Int, y: Int) val point = new Point(1, 2) point.x // <-- does not compile
混入类
“混入”是用来组成类的特性。
abstract class A {
val message: String
}
class B extends A {
val message = "I'm an instance of class B"
}
trait C extends A {
def loudMessage = message.toUpperCase()
}
class D extends B with C
val d = new D
d.message // I'm an instance of class B
d.loudMessage // I'M AN INSTANCE OF CLASS B类 D 有一个超类 B 和一个混入类 C。类只能有一个超类但可以有很多混入类(分别使用关键字 extend 和 with)。混入类和超类可能具有相同的超类型。
现在让我们从一个抽象类开始,来看一个更有趣的例子:
abstract class AbsIterator {
type T
def hasNext: Boolean
def next(): T
}这个类有一个抽象类型 T 和一个标准的迭代器方法。
接下来,我们将实现一个具体类(所有的抽象成员 T、hasNext 和 next 都被实现):
class StringIterator(s: String) extends AbsIterator {
type T = Char
private var i = 0
def hasNext = i < s.length
def next() = {
val ch = s charAt i
i += 1
ch
}
}StringIterator 接受一个 String 并且可以对字符串进行遍历(例如:要查看字符串是否包含某个字符)。
现在,让我们创建一个也扩展了 AbsIterator 的特质。
trait RichIterator extends AbsIterator {
def foreach(f: T => Unit): Unit = while (hasNext) f(next())
}只要还有其他元素(while(hasNext)),此特征通过不断调用提供的函数 f: T => Unit 在下一个元素(next())上来实现 foreach。因为 RichIterator 是一个特质,它不需要去实现 AbsIterator 里的抽象成员。
我们希望将 StringIterator 和 RichIterator 的功能合并到一个类中。
object StringIteratorTest extends App {
class RichStringIter extends StringIterator(args(0)) with RichIterator
val richStringIter = new RichStringIter
richStringIter foreach println
}新的 Iter 类有一个作为超类的 StringIterator 和一个作为混入类的 RichIterator。
只有单一继承的话,我们就无法达到这样的灵活性。
嵌套类
在 Scala 中,可以让类作将其他类作为自己的成员。与java语言不同,嵌套类是封闭类的成员,在 Scala 中,嵌套类被绑定到外部对象。假设我们希望编译器在编译时阻止我们混合哪些 Node、属于哪些 Graph。路径依赖类型提供了一个解决方案。
为了说明这一差异,我们快速地概述了 Graph 数据类型的实现:
class Graph {
class Node {
var connectedNodes: List[Node] = Nil
def connectTo(node: Node) {
if (connectedNodes.find(node.equals).isEmpty) {
connectedNodes = node :: connectedNodes
}
}
}
var nodes: List[Node] = Nil
def newNode: Node = {
val res = new Node
nodes = res :: nodes
res
}
}这个程序表示一个 Graph 作为 Node 列表(List[Node])。每个 Node 都有一个它连接到的其他 Node 的列表(connectedNodes)。class Node 是路径依赖类型,因为它嵌套在 class Graph 中。因此,connectedNodes 中的所有节点必须使用来自 newNode 同一实例的 Graph 创建。
val graph1: Graph = new Graph val node1: graph1.Node = graph1.newNode val node2: graph1.Node = graph1.newNode val node3: graph1.Node = graph1.newNode node1.connectTo(node2) node3.connectTo(node1)
为了清楚起见,我们明确声明了 node1,node2 和 node3 的类型为 graph1.Node,但是编译器可以推断出它。这是因为当我们调用 graph1.newNode,它再调用 new Node 时,该方法使用特定于实例 graph1 的 Node 实例。
如果我们现在有两个 Graph,那么 Scala 的类型系统不允许将一个 Graph 中定义的 Node 与另一个 Graph 的 Node 混合,因为另一个 Graph 的 Node 具有不同的类型。 这是一个非法程序:
val graph1: Graph = new Graph val node1: graph1.Node = graph1.newNode val node2: graph1.Node = graph1.newNode node1.connectTo(node2) // legal val graph2: Graph = new Graph val node3: graph2.Node = graph2.newNode node1.connectTo(node3) // illegal!
graph1.Node 类型与 graph1.Node 类型不同。在 Java 中,前一个示例程序中的最后一行是正确的。对于这两个 Graph 的 Node,Java 将分配相同类型的 graph.node。Node 的前缀是 Graph 类。在 Scala 中,这样的类型也可以表达,它被写成 Graph#Node。如果我们想要连接不同 Graph 的 Node,我们必须按照以下方式改变我们初始 Graph 实现的定义:
class Graph {
class Node {
var connectedNodes: List[Graph#Node] = Nil
def connectTo(node: Graph#Node) {
if (connectedNodes.find(node.equals).isEmpty) {
connectedNodes = node :: connectedNodes
}
}
}
var nodes: List[Node] = Nil
def newNode: Node = {
val res = new Node
nodes = res :: nodes
res
}
}注意,这个程序不允许我们将一个 Node 附加到两个不同的 Graph 上。如果我们想要删除这个限制,我们必须将变量 Node 的类型更改为 Graph#Node对象
一个对象是一个只有一个实例的类。它被引用时被懒惰地创建,就像懒惰的val一样。
作为顶级的值,一个对象是一个单例。
作为封闭类或本地值的成员,它的行为完全像一个懒惰的val。
定义对象
一个对象是一个值。定义一个对象看起来和定义一个类一样,但使用的关键字是 object:
object Box
下面是一个带有方法的对象的例子:
package logging
object Logger {
def info(message: String): Unit = println(s"INFO: $message")
}方法 info 可以从程序中的任何地方导入。像这样创建实用程序方法是单例对象的常见用例。
让我们看看如何在另一个包中使用 info:
import logging.Logger.info
class Project(name: String, daysToComplete: Int)
class Test {
val project1 = new Project("TPS Reports", 1)
val project2 = new Project("Website redesign", 5)
info("Created projects") // Prints "INFO: Created projects"
}由于 import 语句,import logging.Logger.info,info 方法是可见的。
导入需要++导入符号++的“稳定路径”,并且对象是稳定的路径。
注意:如果一个 object 不是顶层的,而是嵌套在另一个类或对象中,那么该对象就像任何其他成员一样是“路径依赖的”。这意味着给定 class Milk 和 class OrangeJuice 两个饮料类型,一个类成员 class NutritionInfo “取决于”封闭的实例,牛奶或橙汁。milk.NutritionInfo 与 oj.NutritionInfo 完全不同.
伴生对象
名称与某个类相同的对象称为伴生对象。相反,该类是对象的伴生类。但伴生类或对象可以访问其伴生的私人成员。在伴生类实例里使用伴生对象的方法和值是没有效果的。
import scala.math._
case class Circle(radius: Double) {
import Circle._
def area: Double = calculateArea(radius)
}
object Circle {
private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
}
val circle1 = new Circle(5.0)
circle1.areaclass Circle 有一个特定于每个实例的成员 area,而单例 object Circle 有一个可用于每个实例的方法 calculateArea。
伴生对象也可以包含工厂方法:
class Email(val username: String, val domainName: String)
object Email {
def fromString(emailString: String): Option[Email] = {
emailString.split('@') match {
case Array(a, b) => Some(new Email(a, b))
case _ => None
}
}
}
val scalaCenterEmail = Email.fromString("[email protected]")
scalaCenterEmail match {
case Some(email) => println(
s"""Registered an email
|Username: ${email.username}
|Domain name: ${email.domainName}
""")
case None => println("Error: could not parse email")
}object Email 包含从一个 String 可以创建一个 Email的工厂 fromString。在可能解析错误的情况下,我们将其作为 Option[Email] 返回。
注意:如果类或对象具有伴生,则两者必须在同一个文件中定义。 要在REPL中定义伴生,请将它们定义在同一行上或输入 :paste 模式。
Java 程序员的注意事项
Java 中的 static 成员被模仿为 Scala 中伴生对象的普通成员。
当使用Java代码中的伴生对象时,成员将在具有 static 修饰符的伴随类中定义。这称为静态转发(static forwarding)。 即使您没有自己定义伴生类,也会发生这种情况。