这一讲,你会学到何时使用Scala的object语法结构。在你需要某个类的单个实例时,或者想为其他值或函数找一个可以挂靠的地方时,就会用到它。
要点:
- 用对象作为单例或存放工具方法
- 类可以拥有一个同名的伴生对象
- 对象可以扩展类或特质
- 对象的apply方法通常用来构造伴生类的新实例
- 如果不想显式定义main方法,可以用扩展App特质的对象
- 你可以通过扩展Enumeration对象来实现枚举
6.1 单例对象
Scala没有静态方法或静态字段,你可以用object这个语法结构来达到同样的目的。对象定义了某个类的单个实例,包含了你想要的特性。例如:
object Accounts{
private var lastNumber=0
def newUniqueNumber()= { lastNumber +=1; lastNumber}
}
当你在应用程序中许哟啊一个新的唯一账号时,调用Account.newUniqueNumber()即可。
对象的构造器,在该对象第一次被调用时执行。如果一个对象从未被使用,那么其构造器也不会被执行。
对象本质上可以拥有类的所有特性——他甚至可以扩展其他类或特质(参见6.3节)。只有一个例外:你不能提供构造器参数。
- 作为存放工具函数或常量的地方
- 高效地共享单个不可变实例
- 需要用单个实例来协调某个服务时(参考单例模式)
6.2 伴生对象
在Java中,你通常既会用到实例方法又有静态方法地类。在Scala中,你可以通过类和与类同名的“伴生”(companion)对象来达到同样目的。例如:
class account{
val id= account.newUniqueNumber()
private var balance=0.0
def deposit (amount:Double){
banlance+=amount)
...
}
object account{
private var lastNumeber=0
private def newUniqueNumber()={ lastNumber+=1; lastNumber}
}
注意,类的伴生对象的功能特性并不在类的作用域内。举例来说,account类必须通过account.newUniqueNumber()而不是直接用newUniqueNumber()来调用伴生对象的方法。
提示: 在REPL中,要同时定义类和对象,必须使用粘贴模式,即输入:paste
说明: 伴生对象包含与类密切相关的功能特性。在第7讲,你将看到如何使用包对象(package object) 给包添加功能特性。
6.3 扩展类或特质的对象
一个object可以扩展类以及一个或多个特质。其结果是一个扩展了指定类以及特质的类的对象,同事拥有在对象定义中给出的所有特性。
一个有用的场景是给出可被共享的默认对象。
举例来说,考虑程序中引入一个可以撤销动作的类。
abstract class undoAbleAction(val description:String){
def undo(): Unit
def redo(): Unit
}
默认情况下可以是“什么都不做”。当然了,对于这个行为我们只需要一个实例即可。
object DoNothing extends undoAbleAction("Do nothing"){
override def undo() {}
override def redo() {}
}
DoNothing对象可以被所有需要这个默认行为的地方共用。
val actions= Map("open"->DoNothing, "save"->DoNothing,.......)
//打开和保存功能尚未实现
6.4 apply方法
我们通常会定义和使用兑现过的apply方法。当遇到如下形式的表达式时,apply方法就会被调用:
object(参数1,......,参数n)
通常,这样一个apply方法返回的时伴生类的对象。
举例来说,Array对象定义了apply方法,让我们可以用下面这样的表达式来创建数组:
Array("Mary","Chris","Bob")
为什么不用构造器来创建数组?
对于嵌套表达式而言,省去new关键字会方便很多:
Array(Array(1,2), Array(3,5)) //这是一个多维数组
注意: Array(10)和new Array(10)很容易搞混。前一个表达式调用的时apply(10),交出一个单元素(整数10)的Array[Int];而第二个调用的时构造器this(10),结果时Array[Nothing],包含了10个null元素。
scala> val b=new Array[Int](10)
b: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
scala> val c =Array(10)
c: Array[Int] = Array(10)
scala> b(9)=9
scala> b
res6: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 9)
scala> c(9)=9
java.lang.ArrayIndexOutOfBoundsException: 9 //报错,因为c的只有一个元素,那就是10
... 32 elided
scala> c.length
res10: Int = 1
6.5 应用程序对象
每个Scala程序都必须从一个对象的main方法开始,这个方法的类型为Array[String]=>Unit:
object Hello{
def main(args:Array[String]){
print("Hello, World!")
}
}
用scalac 命令执行.scala
文件:
C:\Users\Desktop\scala_practice>scalac helloWorld.scala
Picked up JAVA_TOOL_OPTIONS: -Duser.home=C:\Users\45072885
C:\Users\Desktop\scala_practice>scala -Dscala.time Hello Chris
Picked up JAVA_TOOL_OPTIONS: -Duser.home=C:\Users\45072885
Hello, World!
除了每次都提供自己的main方法,你还可以扩展App特质(你可能不明白),然后讲程序代码放入构造器方法体内:
object Hello extend App{
print("Hello, World!")
}
如果你需要命令行参数,则可以通过args属性得到:
object Hello extends App{
if(args.length>0)
println(f"Hello, ${args(0)}")
else
println("Hello, World!")
}
如果你在调用该应用程序设置了scala.time选项的话,程序退出时会显示逝去的时间。
C:\Users\Desktop\scala_practice>scalac scalatime.scala
Picked up JAVA_TOOL_OPTIONS: -Duser.home=C:\Users\45072885
C:\Users\Desktop\scala_practice>scala -Dscala.time Hello Chris
Picked up JAVA_TOOL_OPTIONS: -Duser.home=C:\Users\45072885
Hello, Chris
[total 109ms]
所有这些操作涉及一些小小的魔法。App特质扩展自另一个特质DelayedInit,编译器对该特质有特殊处理。所有带有该特质有特殊处理。所有带有该特质的类,其初始化方法都会被挪到delayedInit方法中。App特质的main方法捕获到命令行参数,调用delayInit方法,并且根据要求打印出逝去的时间。
6.6 枚举
和Java中不同,Scala并没有枚举类型。不过,标准库提供了一个Enumeration
助手类,可以用于产出枚举。
定义一个扩展Enueration类的对象并以Value
方法调用初始化枚举中的所有可选值。例如:
object TraficLightColor extends Enumeration {
val Red,Yellow,Green= Value
}
在这里我们定义了三个字段:Red、Yellow和Green,然后用Value调用将它们初始化。这是如下代码的简写:
val Red= Value
val Yellow=Value
val Green=Value
每次调用Value方法都返回内部类的新实例,该内部类也叫做Value
。
或者你也可以向Value方法传入ID、名称,或两个参数都传:
val Red= Value(0,"stop")
val Yellow=Value(10) //名称为”Yellow“
val Green=Value("go") //ID为11
如不指定,则ID将在前一个枚举值的基础上加一,从0开始。默认名称为字段名。
完成定义后,你就可以用TraficLightColor.Red、TraficLightColor.Yellow、TraficLightColor.Green来引用枚举值了。你可以用如下语句直接引入枚举值:
import TraficLightColor._
(关于引入类或对象的成员更多信息,参加下一讲)
记住枚举的类型时TraficLightColor.Value而不是TraficLightColor——后者是握有这些值的对象。
有人推荐增加一个类型别名:
object TraficLightColor extends Enumeration {
type TraficLightColor
val Red,Yellow,Green= Value
}
现在枚举的类型变成了TraficLightColor.TraficLightColor,但需要你使用import语句才有意义。例如:
import TraficLightColor._
def dowhat(color: TraficLightColor)={
if (color==Red) "stop"
else if (color==Yellow) "hurry up"
else "go"
}
枚举值的ID可以通过id方法返回,名称通过toString
方法返回。对TraficLightColor.values
的调用将交出所有枚举的集:
for(c<-TraficLightColor.values) println(s"${c.id}: $c")
最后你可以通过枚举的ID或名称来进行查找定位,以下两段代码都将交出TraficLightColor.Red对象:
TraficLightColor(0) //将调用Enumeration.apply
TraficLightColor.withName("Red")