博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【Scala之旅】类与对象
阅读量:6368 次
发布时间:2019-06-23

本文共 7542 字,大约阅读时间需要 25 分钟。

本节翻译自

综述:本节中你将会学习如何使用Scala实现类,以及Scala相比Java更加精简的表示法带来的便利。同时介绍了object的语法结构(Scala没有静态方法或静态字段,但object可以达到同样的效果)。

Scala 中的类是创建对象的模板。它们可以包含统称为成员的方法、值、变量、类型、对象、特征和类。之后将介绍类型、对象和特征。

定义类

最小的类定义就是关键字 class 和标识符。类名应该大写。

class Userval 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  // 2println(point1)  // prints (x, y)

这个 Point 类有四个成员:变量 xy,以及方法 movetoString。与其他语言不同的是,主构造函数在类签名中 (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 0val point1 = new Point(1)println(point1.x)  // prints 1

在这个版本的 Point 类中,xy 的默认值为 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 Pointpoint1.x = 99point1.y = 101 // prints the warning

在这个版本的 Point 类中,数据存储在私有变量 _x_y 中。而定义的方法 def xdef y 则可以访问私有数据。def x_=def y_= 用于验证和设置 _x_y 的值。请注意 setter 的特殊语法:方法将 _= 附加到 getter 的标识符后面,并且跟着参数。

使用 valvar 的主构造函数参数是公开的。但是,因为 val 是不可变的,所以不能写下面的内容。

class Point(val x: Int, val y: Int)val point = new Point(1, 2)point.x = 3  // <-- does not compile

没有 valvar 的参数是私有值,只在类中可见。

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 Cval d = new Dd.message  // I'm an instance of class Bd.loudMessage  // I'M AN INSTANCE OF CLASS B

D 有一个超类 B 和一个混入类 C。类只能有一个超类但可以有很多混入类(分别使用关键字 extendwith)。混入类和超类可能具有相同的超类型。

现在让我们从一个抽象类开始,来看一个更有趣的例子:

abstract class AbsIterator {  type T  def hasNext: Boolean  def next(): T}

这个类有一个抽象类型 T 和一个标准的迭代器方法。

接下来,我们将实现一个具体类(所有的抽象成员 ThasNextnext 都被实现):

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 里的抽象成员。

我们希望将 StringIteratorRichIterator 的功能合并到一个类中。

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 Graphval node1: graph1.Node = graph1.newNodeval node2: graph1.Node = graph1.newNodeval node3: graph1.Node = graph1.newNodenode1.connectTo(node2)node3.connectTo(node1)

为了清楚起见,我们明确声明了 node1node2node3 的类型为 graph1.Node,但是编译器可以推断出它。这是因为当我们调用 graph1.newNode,它再调用 new Node 时,该方法使用特定于实例 graph1Node 实例。

如果我们现在有两个 Graph,那么 Scala 的类型系统不允许将一个 Graph 中定义的 Node 与另一个 Graph 的 Node 混合,因为另一个 Graph 的 Node 具有不同的类型。 这是一个非法程序:

val graph1: Graph = new Graphval node1: graph1.Node = graph1.newNodeval node2: graph1.Node = graph1.newNodenode1.connectTo(node2)      // legalval graph2: Graph = new Graphval node3: graph2.Node = graph2.newNodenode1.connectTo(node3)      // illegal!

graph1.Node 类型与 graph1.Node 类型不同。在 Java 中,前一个示例程序中的最后一行是正确的。对于这两个 Graph 的 Node,Java 将分配相同类型的 graph.nodeNode 的前缀是 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 loggingobject Logger {  def info(message: String): Unit = println(s"INFO: $message")}

方法 info 可以从程序中的任何地方导入。像这样创建实用程序方法是单例对象的常见用例。

让我们看看如何在另一个包中使用 info

import logging.Logger.infoclass 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.infoinfo 方法是可见的。

导入需要++导入符号++的“稳定路径”,并且对象是稳定的路径。

注意:如果一个 object 不是顶层的,而是嵌套在另一个类或对象中,那么该对象就像任何其他成员一样是“路径依赖的”。这意味着给定 class Milkclass OrangeJuice 两个饮料类型,一个类成员 class NutritionInfo “取决于”封闭的实例,牛奶或橙汁。milk.NutritionInfooj.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.area

class 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("scala.center@epfl.ch")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)。 即使您没有自己定义伴生类,也会发生这种情况。

转载地址:http://vsrma.baihongyu.com/

你可能感兴趣的文章
Tomcat 系统架构与设计模式_ 设计模式分析
查看>>
本地串口TCP/IP 映射到远端串口
查看>>
锁机制探究
查看>>
硬盘直接引导启动Manjaro Linux iso
查看>>
CodeSmith代码生成工具介绍
查看>>
几个常用且免费的接口
查看>>
jQuery文件上传插件 Uploadify更改错误提示的弹出框
查看>>
RHEL6下Apache与Tomcat整合
查看>>
Heartbeat+DRBD+MFS高可用
查看>>
要感谢那些曾经慢待你的人
查看>>
常见的global cache等待事件
查看>>
第 7 章 多主机管理 - 047 - 管理 Machine
查看>>
CentOS5和6的系统启动流程
查看>>
怎么看域客户端是否继承了组策略
查看>>
linux防止DDoS***
查看>>
6.4 Linked List 重做
查看>>
小米路由
查看>>
QT 学习 之 窗口拖拽 实现
查看>>
PHP的ftp文件,多文件上传操作类
查看>>
js中清空数组的方法
查看>>