1.类和构造函数
Scala中的类,基本概念与其他面向对象语言是一致的,不过在语法上有些不一样的地方。与Java等语言相比,Scala的类语法更简洁,使用起来也更方便。
1.1.类的基本语法
我们先来看一个简单的类定义和使用的代码。
class ScoreCalculator { private var total, count = 0 def report(score: Int) { total += score count += 1 } def score = if (count == 0) 0 else total / count } val sc = new ScoreCalculator() sc.report(80) sc.report(90) sc.report(92) sc.report(86) println("The average score is " + sc.score)
从以上代码可见,定义类的方式,与其他语言相似,使用class关键字即可。
请注意
- Scala中,默认的访问修饰符是public,也就是说,没有指定访问修饰符的成员,都是public的。
- 一个源文件可包含多个public的类,或接口等。这与Java不一样。
1.2.构造函数
1.2.1.主构造函数
以上示例中,没有定义构造函数,但是可以使用new来创建对象,这是因为Scala的类都有默认的基本构造函数,而且该构造函数是跟类标识符出现在一起的,并没有显示的定义。 基本构造函数的参数就是类参数,类内部除属性方法之外的其他语句,组成了基本构造函数的函数体。以上示例的类没有参数,我们可以将运动员的名字放入计分类中,将类修改如下。
class ScoreCalculator(athlete: String) { private var total, count = 0 println("This is in the primary constructor") def report(score: Int) { total += score count += 1 } def score = if (count == 0) 0 else total / count override def toString() = athlete + "‘s score is " + score } val sc = new ScoreCalculator("Yao") println("\nJust created an object, now you can use it.\n") sc.report(80) sc.report(90) println(sc)
以上class ScoreCalculator(athlete: String)
相当于下面的Java代码。
class ScoreCalculator { private String athlete; public ScoreCalculator(String athlete) { this.athlete = athlete; } }
这也从一个侧面体现了Scala的简洁。
请注意
在类定义中,所有不属于方法和字段的语句,都属于主构造函数,比如上面的println("This is in the primary constructor")
。 类参数(或默认构造函数参数)默认的访问级别是对象私有,即private[this] val,若想要在类外部也能使用,只需显示注明为val或var,比如class ScoreCalculator(val athlete: String)
。
1.2.2.私有构造函数
在某些情况下,我们不希望外界访问构造函数,比如,仅允许通过提供的工厂方法来构造对象,这时候,我们就需要将构造函数私有化,以防止外界访问。
在Java或C#中,由于构造函数都有显示的定义,将其定义为私有的就可以了。Scala基本构造函数没有显示的定义,私有的标志放在哪里呢? 很简单,放在类参数列表之前,如下所示。
class ScoreCalculator private(val athlete: String) val sc = new ScoreCalculator("Yao")
1.2.3.辅助构造函数
与其他语言一样,Scala也允许类有多个构造函数,除了主构造函数外,还可以定义若干个辅助构造函数。如下例所示。
class ScoreCalculator { var athlete = "" var degreeOfDifficulty = 0.0 def this(athlete: String) { this() //Call primary constructor this.athlete = athlete } def this(athlete: String, degreeOfDifficulty: Double) { this(athlete) //Call another auxiliary constructor this.degreeOfDifficulty = degreeOfDifficulty } override def toString = "Athlete: " + athlete + ", degree of difficulty: " + degreeOfDifficulty } val sc1 = new ScoreCalculator("Gao Min") sc1.degreeOfDifficulty = 3.7 println(sc1) val sc2 = new ScoreCalculator("Fu Mingxia", 3.5) println(sc2)
你发现了以上代码与你熟悉的语言有什么不同之处吗?是的,Scala中辅助构造函数与Java或者C++等语言有明显区别。
请注意
- 构造函数用this标识,而不是类名。
- 辅助构造函数必须先调用主构造函数,或者在它之前定义的其他构造函数。这意味着,主构造函数是唯一创建对象的途径,不论你调用的是哪个构造函数。
- 辅助构造函数不能调用父类的构造函数,只有主构造函数可以。这一点将在继承的章节介绍到。
2.类的属性
类的属性,语法上比Java和C#更简洁一些,不过基本概念与其他面向对象语言是一致的。
2.1.属性的基本语法
在Java中,定义属性是比较麻烦的,必须遵从下面的格式。
// This is Java private String name; public String getName() { return name; } public void setName(String name) { this.name = name; }
调用时,必须得使用getName和setName。不得不说,这是一种巨大的浪费。到C#时,稍有改进。
//This is C# private string name; public string Name { get {return name} set {name = value} }
调用时,set和get都直接用Name,这比Java有进步,但是,在get和set都遵从默认行为时,还是有浪费。而且,实践表明,大部分情况下,属性都没有特别的处理。 这种情况下,Scala就简洁多了。
var name = ""
这就是一个合法的属性,编译器会自动生成一个private字段和getter,setter方法。
val name = ""
这是一个只读的属性,编译器会自动生成getter方法,但是不会生成setter方法。
请注意
Scala中没有只写(write-only)的属性。要实现类似的功能,需要在自定义的getter中加入特殊的处理,比如抛出异常。
2.2.自定义getter和setter
如果你想自己写特殊的getter或setter方法,就像Java或C#里那样,Scala也提供了这样的途径,但是语法更紧凑。
private var _name = "" def name = { _name.toUpperCase } def name_= (newName: String) {_name = newName.trim()}
以上代码,在getter和setter中实现了一些特殊的处理,get到的是全大写的姓名,set的时候,会去掉前后空格。
请注意
- Scala的getter方法,语法有些特别,格式如
name_= (parameters)
,名称,下划线和等号是一个整体,之间不能有空格。 - setter方法必须与getter成对出现,也就是不能只写不读。相反,getter可以单独出现,也就是说只读是可能的。
一个实际的例子如下:
class Address { val country = "China" //readonly private var _city = "" def city = _city //readonly, since no setter defined var address = "" //will have both getter and setter //Self-defined getter and setter private var _zipCode = "" def zipCode = _zipCode def zipCode_= (v: String) { if (v.length == 6) { _zipCode = v _city = if(v(0) == ‘1‘) "Beijing" else "Shanghai" } else { _zipCode = "000000" _city = "Other" } } } val addr = new Address addr.zipCode = "100128" addr.address = "Suite 101" println(addr.address) println(addr.city)
3.单例对象
单例对象是Scala中特有的概念,用来消除像其他面向对象语言那样对静态(static)的需求。 静态,Java和C#等语言中,一直被视为不纯粹面向对象的标志之一,因而,消除静态,算是Scala更面向对象的一个标志。
单例对象可分为两种,不与某个类共享源文件和名称的,称为独立对象(Standalone Object),与此相反,与某个类共享名称的,称为伴生对象(Companion Object)。
3.1.独立对象
独立对象类似于静态类,在一个运行环境中,只会有唯一一个这个类型的对象,它是单例设计模式的天然实现。其定义方式与类相似,只是将关键字换成object。 其他的方面也跟类相似,比如可以继承其他类和特质。只是有一个区别,不能有类参数,也就是构造函数参数。这一点也容易理解,因为你不能显示的实例化他们。
单例对象在第一次被使用的时候,由运行环境将它实例化,如果一直没有被用到,那么就一直不会被实例化。也就是说,单例对象最多只会实例化一次。
import scala.collection.mutable.ListBuffer object Logger { private val list = new ListBuffer[String]() println("list is created") def log(msg: String) { list += msg } def flush() { list.foreach(println) } } println("Process started.") Logger.log("The first call") //This is when Logger got created Logger.log("The second call") Logger.log("The third call") println("Let‘s print out what the logger gets") Logger.flush()
3.2.伴生对象
如果一个单例对象跟类有相同的名字,而且它们在同一个源文件里,那么就称之为这个类的伴生对象。 在Java或C#中,经常会有一个类,既有静态成员,又有实例成员,伴生对象就是用来放置静态成员的地方。
伴生对象的特别之处是,它和伴生类能互相访问对方的私有成员。这使得它能完全实现静态成员的功能,而不会带来额外的风险。
object Connection { private val connString = "localhost" //Better do lazy init in real project private val items = Array(new Connection("C1"),new Connection("C2")) def get(): Connection = { for (c <- items) { if (c.isFree) { //accessed private member isFree println("Dispatch " + c.name) return c } } //Should handle this properly, not this rude way println("Force release and dispatch C1") items(0).release() items(0) } } class Connection private(val name: String) { private var isFree = true def connect() { //accessed private member connString println(name + " connected to " + Connection.connString) isFree = false } def release() { isFree = true } } val conn1 = Connection.get() conn1.connect() val conn2 = Connection.get() conn2.connect() val conn3 = Connection.get() conn3.connect()
请注意 虽然伴生对象和类之间可以互相访问对方的私有成员,但是他们并不处于同一范围,因此访问时需要提供访问对象,比如
- 在伴生对象里访问伴生类对象,需要使用c.isFree,而不是isFree。这一点很容易理解。
- 在伴生类中访问伴生对象的私有成员,需要写明对象,如Connection.connString,而不能直接使用connString
3.3.apply方法
apply方法是对象的一类特有方法,一般可用于创建伴生类。apply方法可以用简洁的方式调用,形如Object(args..)
, 当然,你也可以跟其他方法一样调用,Object.apply(args...)
,这两种写法的结果是一样的。
现在,当你看到List(1,2,3)
这样的语句就不会感到奇怪了,这只是List.apply(1,2,3)
的简写而已。
应用程序
[Scala Level: A1]
要运行一个Scala程序,与Java一样,需要一个程序入口,就是main方法。也与Java的main方法类似,Scala的main方法可接受一系列字符串作为参数。 比Java方便的是,你可以通过继承App特质(Trait)来省去自己定义main方法的麻烦。
应用程序规范
要使一个Scala程序可运行,需要一个符合如下格式的main方法作为应用程序入口。
def main(args: Array[String])
你知道,在Java中,main方法得是static的,Scala中也需要类似的实现,那就是包含main方法的,要求是独立对象(Standalone Object)。
以下示例就是一个合法的可运行的应用程序了。
object MyApplication { def main(args: Array[String]) { args.foreach(println) } }
App特质
App特质(在2.9以前是Application特质)可以帮我们省去定义main方法的麻烦。以上MyApplication可改写为:
object MyApplication extends App { args.foreach(println) }
在整个对象的内部,独立的语句都会被放入main方法中而执行。当然,如果你想要自己写main方法,也是可以的,不过要注意,main方法之外的字段,不会被初始化。 我们看下面的例子。
object AppWithMain extends App { //Fields will not be initiated before the main method val plainVal = "plain val will not be initiated here" lazy val lazyVal = "lazy val will be initiated the first time accessed" final val finalVal = "final val will be initiated" def method = "Of cause, method will be evaluated every time being called" override def main(args: Array[String]) { println(plainVal) //will be null println(lazyVal) println(finalVal) println(method) } }
以上plainVal不会被初始化,因此会打印出null。如果有需要,可以使用lazy或者final字段,或定义为方法。
请注意
虽然可以重载main方法,但是,非必要情况下,不建议这么做。直接将语句写在函数体内,即简洁又不容易出错。
参考文献:
http://meetfp.com/zh/scala-basic/application