3.Scala面向对象编程
3.1类和对象
3.1.1类
类是用class开头的代码定义,定义完成后可以用new+类名的方式构造一个对象,对象的类型是这个类。类中定义的var和val类型变量称为字段,用def定义的函数称为方法。字段也称为实例变量,因为每个被构造出来的对象都有自己的字段,但所有的对象公用同样的方法。也就是说,定义一个类之后,每个对象的变量存储在独立的空间,互不相同;每个对象的方法是一样的,存储在同样的空间。使用new创建的对象可以赋值给一个变量,赋值给用val定义的变量,则这个变量之后不得被赋值给新的对象。代码如下:
scala> class Students {| var name = "None"| def register(n:String) = name = n| }
// defined class Students
scala> val stu = new Students
val stu: Students = Students@4fc454d5
scala> stu.name
val res29: String = None
scala> stu.register("Bob")
scala> stu.name
val res30: String = Bob
scala> stu.register("Jia")
scala> stu.name
val res31: String = Jia
scala> stu = new Students
-- [E052] Type Error: --------------------------------------------------------------------------------------------------
1 |stu = new Students|^^^^^^^^^^^^^^^^^^|Reassignment to val stu|| longer explanation available when compiling with `-explain`
1 error found
类的成员都是公共的,在 外部都可以被访问。如果不想被其他对象访问,可以在变量前加上private
关键字。
3.1.2类的构造方法
主构造方法
Scala不同于C++,Python等语言,Scala没有专门的主构造语法,在类名后可以定义若干参数列表用于传递参数,参数会在初始化对象是传给类中的变量。类内部非字段,非方法的代码都被当做“主构造方法”。
scala> class Students(n:String) {| val name = n| println("A student named" + n + " has been registered.")| }
// defined class Students
scala> val stu = new Students("Tom")
A student namedTom has been registered.
val stu: Students = Students@33004de5
这里println
既不是字段,也不是方法,所以被当成主构造函数的一部分。
辅助构造方法
在类的内部使用 def this(...)
定义一个辅助构造方法,其第一步的行为必须是调用该类的另一个构造方法,即第一句必须是this(...)
要么是主构造方法,要么是之前的另一个辅助构造方法。这种规则让任何构造方法最终都会调用类的主构造方法,使得主构造方法成为类的单一入口。
scala> class Students(n:String) {| val name = n| def this() = this("None")| println("A student named " + n + " has been registered.")| }
// defined class Students
scala> val stu = new Students
A student named None has been registered.
val stu: Students = Students@6f319f62
scala> val stu2c= new Students("Jia")
A student named Jia has been registered.
val stu2c: Students = Students@5a2e250b
这时存在两个构造函数,主构造函数需要传入一个String类型的量用于初始化,辅助析构函数不需要传入,因为它已经把这个量定义为了None。去掉这个析构函数,新创建对象时不传入String会导致报错:
scala> class Students(n:String) {| val name = n| println("A student named " + n + " has been registered.")| }
// defined class Students
scala> val stu = new Students
-- [E171] Type Error: --------------------------------------------------------------------------------------------------
1 |val stu = new Students| ^^^^^^^^^^^^| missing argument for parameter n of constructor Students in class Students: (n: String): Students
1 error found
不能定义两个有歧义的辅助构造函数,这样写编译器无法确认在无参构造时究竟该调用哪一个构造函数:
scala> class Students(n:String) {| val name = n| def this() = this("None")| def this() = this("Jia")| println("A student named " + n + " has been registered.")| }
-- [E120] Naming Error: ------------------------------------------------------------------------------------------------
4 |def this() = this("Jia")| ^| Double definition:| def <init>(): Students in class Students at line 3 and| def <init>(): Students in class Students at line 4| have the same type after erasure.|| Consider adding a @targetName annotation to one of the conflicting definitions| for disambiguation.
1 error found
析构函数
Scala不需要定义析构函数。
私有主构造方法
在类的定义时加入private
关键字,则主构造函数是私有的,外部无法调用主构造函数创建对象,只能使用其他公有的辅助构造方法或者工厂方法(专门用于构造对象的方法)
scala> class Students private (n:String,m:Int) {| val name = n| val score = m| def this(n:String) = this(n,100)| println(n + "'s score is" + m)| }
// defined class Students
scala> val stu = new Students("Bill",90)
-- [E007] Type Mismatch Error: -----------------------------------------------------------------------------------------
1 |val stu = new Students("Bill",90)| ^^^^^^^^^| Found: (String, Int)| Required: String|| longer explanation available when compiling with `-explain`
1 error found
scala> val stu = new Students("Bill")
Bill's score is 100
val stu: Students = Students@45f675a4
主构造方法需要传入两个参数,但它是用private
修饰的,所以外部无法调用。定义的辅助析构函数指定了第二个参数是100,只需要传入一个参数,因此外部可以调用这个辅助析构方法。
3.1.3重写toString方法
在构造一个Students类的对象时,Scala解释器打印了一串信息Students@45f675a4
,这其实来自Students类的toString方法,这个方法返回一个字符串,构造完一个对象时被自动调用,这个方法是由所有Scala类隐式继承而来的,默认的toString方法将会简单地打印类名,一个“@”符号和一个十六进制数。如果向打印更多有用信息,可以自定义toString方法,但需要加上override
关键字。
scala> class Students(n:String) {| val name = n| override def toString = "A student named " + n + "."| }
// defined class Students
scala> val stu = new Students("Jia")
val stu: Students = A student named Jia.
3.1.4方法重载
与C++类似,Scala支持方法重载,即在类内可以定义多个同名的方法,但参数(主要是参数类型)不一样,这个方法有很多不同版本,这称为方法重载。函数真正的特征是它的参数,而非函数名或返回类型。它与重写定义不同,重写是子类覆盖了超类的某个方法。
3.1.5类参数
Scala允许在类参数前加上val或var来修饰,这样可以在类的内部生成一个与参数同名的公有字段。还可以使用private
、protected
、override
来表面字段的权限。如果参数没有任何关键字,那么它就仅仅是参数,不是类的成员,只能用初始化字段赋值,内部无法修改,外部无法访问。(如前文中的参数,加上val也算加上了关键字,可以从外部访问,内部可以修改)
scala> class Students(val name: String, var score: Int) {| def exam(s:Int) = score = s| override def toString = name + "'s score is " + score + "."| }
// defined class Students
scala> val stu = new Students("Tim",90)
val stu: Students = Tim's score is 90.
scala> stu.exam(100)
scala> stu.score
val res32: Int = 100
3.1.6单例对象与伴生对象
除了使用new创建一个对象,也可以使用object,但它没有参数和构造方法,且数量只能有一个,因此被称为单例对象。如果某个单例对象和某个类同名,则称单例对象是这个类的伴生对象,这个类称为这个单例对象的伴生类。伴生类和伴生对象必须在同一个文件中,并且可以互相访问对方的所有成员。单例对象和类一样,也可以定义字段和方法,也可以包含别的类和单例对象的定义。单例对象也可以用于大宝某方面功能的函数系列成为一个工具集,或者包含主函数成为程序的入口。
object后面定义的单例对象名可以认为是这个单例对象的名称标签,因此可以通过据点符号访问单例对象的成员,也可以赋值给一个变量。
scala> object B {val b = "a singleton object"}
// defined object B
scala> B.b
val res33: String = a singleton object
scala> val x = B
val x: B.type = B$@3c4f0087
scala> x.b
val res34: String = a singleton object
定义一个类,就定义了一个类型;但定义单例对象并没有新产生一种类型,根本上来说,每个单例对象的类型都是object.type
,伴生对象也没有定义类型,而是由伴生类定义的。但是,不同的object定义之间并不能互相赋值,它可以继承自超类或混入特质,因此定义的两个object对象并不是相同的类型:
scala> object X
// defined object X
scala> object Y
// defined object Y
scala> var x = X
var x: X.type = X$@304b2629
scala> x = Y
-- [E007] Type Mismatch Error: -----------------------------------------------------------------------------------------
1 |x = Y| ^| Found: Y.type| Required: X.type|| longer explanation available when compiling with `-explain`
1 error found
3.1.7工厂对象与工厂方法
如果定义一个专门用来构造某一个类的对象的方法,那么这种方法被称为工厂方法,包含这些工厂方法集合的单例对象,称为工厂对象。一般工厂对象会定义在伴生对象中,使用工厂方法的好处是可以不用直接使用new来实例化对象,改用方法调用,并且方法名是任意的,对外隐藏了类的实现细节。
首先我们创建一个students.scala
的文件,在里面写入以下内容:
class Students(val name: String, var score: Int) {def exam(s:Int) = score = soverride def toString = name + "'s score is " + score + "."
}
object Students {def registerStu(name: String, score:Int) = new Students(name,score)
}
之后,编译这个文件:
jia@J-MateBookEGo:~/scala_test$ vim students.scala
jia@J-MateBookEGo:~/scala_test$ scalac students.scala
按照书上的方法,直接启动scala并使用import Students._
导入会报错:
scala> import Students._
-- [E006] Not Found Error: ---------------------------------------------------------------------------------------------
1 |import Students._| ^^^^^^^^| Not found: Students|| longer explanation available when compiling with `-explain`
1 error found
这个时候我们需要添加class的路径,假设我们现在处于students.scala文件的路径下,经过编译会生成后缀是.class的文件,如果我们在这个文件的同路径下,使用这个代码导入class:
scala -classpath .
如果要添加其他路径的类,使用如下代码:
scala -classpath /path/to/classes
执行完这个指令后会直接进入scala解释器,使用如下代码调用:
scala> import Students._
scala> val stu = registerStu("Tim",100)
val stu: Students = Tim's score is 100.
其中,object定义的对象称为工厂对象,其中def定义的函数registerStu是工厂方法。
3.1.8 apply方法
apply方法是一个特殊名字的方法,如果定义了这个方法,可以显式调用 对象.apply(参数),也可以隐式调用 对象(参数),如果apply是无参方法,应该写出空括号。通常在伴生对象中定义名为apply的工厂方法,就能通过 伴生对象名(参数)来构造一个对象。也经常在类中定义一个与类相关,具有特定行为的apply方法。
我们在students2.scala中编写如下代码:
class Students2(val name:String, var score: Int) {def apply(s:Int) = score = sdef display() = println("Current core is " + score + ".")override def toString = name + "'s score is '" + score + "."
}
object Students2 {def apply(name: String, score: Int) = new Students2(name, score)
}
执行编译指令,并添加库路径:(这一步在之后不再赘述)
jia@J-MateBookEGo:~/scala_test$ vim students2.scala
jia@J-MateBookEGo:~/scala_test$ scalac students2.scala
jia@J-MateBookEGo:~/scala_test$ scala -classpath .
在解释器中执行如下代码:
scala> val stu2 = Students2("Jack",60)//隐式调用伴生对象中的工厂方法
val stu2: Students2 = Jack's score is 60.
scala> stu2(80)//隐式调用类的apply方法
scala> stu2.display()
Current core is 80.
3.1.9主函数
主函数是Scala程序的唯一入口,程序是从主程序开始执行的。要提供这样的入口,则必须在某个单例对象中定义一个名为main的函数,而且该函数只有一个参数,类型为字符串Array[String],函数的返回类型是Uint。任何符合条件的单例对象都能成为程序的入口。
编写一个main.scala文件,将其与之前编写的students2.scala一起编译:
//Start.scala
object Start {def main(args:Array[String]) = {try {val score = args(1).toIntval s = Students2(args(0), score)println(s.toString)} catch {case ex: ArrayIndexOutOfBoundsException => println("Arguments are deficient!")case ex: NumberFormatException => println("Second argument must be a Int!")}}
}
编译和执行指令:
jia@J-MateBookEGo:~/scala_test$ scala Start.scala students2.scala -- Tom
Arguments are deficient!
jia@J-MateBookEGo:~/scala_test$ scala Start.scala students2.scala -- Tom aaa
Second argument must be a Int!
jia@J-MateBookEGo:~/scala_test$ scala Start.scala students2.scala -- Tom 100
Tom's score is 100.