Scala中的类 |
摘要:
在本篇中,你将会学习如何用Scala实现类。如果你了解Java或C++中的类,你不会觉得这有多难,并且你会很享受Scala更加精简的表示法带来的便利。本篇的要点包括:
1. 类中的字段自动带有getter方法和setter方法
2. 你可以用定制的getter/setter方法替换掉字段的定义,而不必修改使用类的客户端,这就是所谓的"统一访问原则"
3. 用@BeanProperty注解来生成JavaBeans的getXxx/setXxx()方法
4. 每个类都有一个主要的构造器,这个构造器和类定义"交织"在一起。它的参数直接成为类的字段。主构造器执行类体中所有的语句
5. 辅助构造器是可选的,它们叫做this
简单类和无参方法 |
简单类
Scala类最简单的形式看上去和Java或c+++的很相似:
class Counter {
private var value=0 //你必须初始化字段
def increment() {value+=1} //方法默认是公有的
def current()=value
}
在Scala中,类并不声明为public。Scala源文件可以包含多个类,所有这些类都具有公有可见性。使用该类需要做的就是构造对象并按照通常的方式来调用方法:
val myCounter=new Counter // 或new Counter()
myCounter.increment()
println (myCounter.current) // 1
无参方法
调用无参方法比如current时,你可以写上圆括号,也可以不写:
myCounter.current //OK
myCounter.current() //同样OK
应该用哪一种形式呢,我们认为对于改值器方法,即改变对象状态的方法使用(),而对于取值器方法不会改变对象状态的方法去掉()是不错的风格。这也是我们在示例中的做法:
myCounter.increment() //对改值器使用()
println (myCounter.current) //对取值器不使用()
你可以通过以不带()的方式声明current来强制这种风格:
class Counter {
def current=value //定义中不带()
}
这样一来类的使用者就必须用myComter.current,不带圆括号。
带getter和setter的属性 |
Java中的字段属性
编写Java类时,我们并不喜欢使用公有字段:
public class Person{ // 这是Java
public int age; //Java中不鼓励这样做
}
使用公有字段的话,任何人都可以写入fred.age,让Fred更年轻或更老。这就是为什么我们更倾向于使用getter和setter方法:
public class Person{ //这是Java
private int age;
public int getAge() { return age; }
public void setAge{int age) { this.age=age; }
}
像这样的一对getter/setter通常被称做属性(property),我们会说Person类有一个age属性。这到底好在哪里呢,仅从它自身来说,并不比公有字段来得更好。任何人都可以调用fred.setAge(21),让他永远停留在21岁。不过如果这是个问题,我们可以防止它发生:
public void setAge (nt newValue) {// 不能变年轻
if ( newValue>age )
age=newValue;
}
之所以说getter和setter方法比公有字段更好,是因为它们让你可以从简单的get/set机制出发,并在需要的时候做改进。需要注意的是:仅仅因为getter和setter方法比公有字段更好,并不意味着它们总是好的。通常,如果每个客户瑞都可以对一个对象的状态数据进行获取和设置,这明显是很糟糕的。下面,会向展示如何用Scala实现属性。但要靠你自己决定,可以取值和改值的字段是否是合理的设计
Scala中的字段属性
Scala对每个字端都提供getter和setter方法。在这里,我们定义一个公有字段:
class Person {
var age=0
}
Scala生成面向JVM的类,其中有一个私有的age字段以及相应的getter方法和setter方法。这两个方法是公有的,因为我们没有将age声明为private。而对私有字段而言,getter和setter方法也是私有的。
Scala中的getter和setter
在Scala中,getter和setter分别叫做age和age_=例如:
println (fred.age) // 将调用方fred.age()
fred.age= 21 // 将调用fred.age=(21)
如果想亲眼看到这些方法,可以编译Person类,然后用javap查看字节码:
scalac Person.scala
javap -private Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements scala.ScalaObject {
private int age;
public int age()
public void age_$eq(int)
public Person()
}
正如你看到的那样,编译器创建了age和age_$eq方法。=号被翻译成$eq,是因为JVM不允许在方法名中出现=
说明:在Scala中,getter和setter方法并非被命名为getXxx和setXxx,不过它们的用意是相同的。后面会介绍如何生成Java风格的getXxx和setXxx方法,以使得你的Scala类可以与Java工具实现互操作
Scala中的自定义getter和setter
在任何时候你都可以自己重新定义getter和setter方法。例如:
class Person {
private var privateAge =0 // 变成私有并改名
def age = privateAge
def age_= (newValue: Int) {
if (newValue > privateAge)
privateAge=newValue // 不能变年轻
}
}
你的类的使用者仍然可以访问fred.age,但现在Fred不能变年轻了:
fred.age = 30
fred.age = 21
println (fred.age) // 30
颇具影响的Eiffel语言的发明者Bertrand Meyer提出了统一访问原则,内容如下:"某个模块提供的所有服务都应该能通过统一的表示法访问到,至于它们是通过存储还是通过计算来实现的,从访问方式上应无从获知"。在Scala中,fred.age的调用者并不知道age是通过字段还是通过方法来实现的。当然了,在JVM中,该服务总是通过方法来实现的,要么是编译器合成,要么由程序员提供。
还需注意的是:Scala对每个字段生成getter和setter方法听上去有些恐怖,不过你可以控制这个过程如下:
■ 如果字段是私有的,则getter和setter方法也是私有的
■ 如果字段是val,则只有getter方法被生成
■ 如果你不需要任何getter或setter,可以将字段声明为private[this]
只带getter的属性 |
Scala类中的常量
有时候你需要一个只读属性,有getter但没有setter。如果属性的值在对象构建完成后就不再改变,则可以使用val字段:
class Message {
val timeStamp=new java.util.Date
……
}
Scala会生成一个私有的final字段和一个getter方法,但没有setter。
私有字段的getter和setter
不过,有时你需要这样一个属性,客户端不能随意改值,但它可以通过某种其他的方式被改变。前面中的Counter类就是个很好的例子,从概念上讲,counter有一个current属性,当increment方法被调用时更新,但并没有对应的setter
class Counter {
private var value=0 //你必须初始化字段
def increment() {value+=1} //方法默认是公有的
def current()=value
}
需要注意的是,你不能通过val来实现这样一个属性,val永不改变。你需要提供一个私有字段和一个属性的getter方法,像这样:
class Counter {
private var value=0 //你必须初始化字段
def increment() {value+=1} //方法默认是公有的
def current=value //声明中没有()
}
在getter方法的定义中并没有(),因此,你必须以不带圆括号的方式来调用:
val n=myCounter.current // myCounter.current()这样的调用方式是语法错误
总结
总结一下,在实现属性时你有如下四个选择:
■ var foo: Scala自动合成一个getter和一个setter
■ val foo: Scala自动合成一个getter
■ 由你来定义foo和foo_=方法
■ 由你来定义foo方法
但在Scala中,你不能实现只写属性,即带有setter但不带getter的属性。当你在Scala类中看到字段的时候,记住它和Java或c++中的字段不同。它是一个私有字段,加上getter方法(对val字段而言)或者getter和setter了法(对var字段而言)
对私有字段 |
类私有字段
在Scala中Java和C++也一样,方法可以访问该类的所有对象的私有字段。例如:
class Counter {
private var value=0
def increment () {value+=1 }
def isLess (other: Counter) = value < other.value // 可以访问另一个对象的私有字段
}
之所以访问other.value是合法的,是因为othert也同样是Cormter对象。
对象私有字段
除此之外Scala允许我们定义更加严格的访问限制,通过private[this]这个修饰符来实现:
private [this] var value=0 // 类似某个对象.value这样的访问将不被允许
这样一来,Counter类的方法只能访问到当前对象的value字段,而不能访问同样是Counter类型的其他对象的该字段。这样的访问有时被称为对象私有的,这在某些OO语言,比如SmaIITalk中十分常见。对于类私有的字段,Scala生成私有的getter和setter方法。但对于对象私有的字段,Scala根本不会生成getter或setter方法。
权限指定
Scala允许你将访问权赋予指定的类,private[类名]修饰符可以定义仅有指定类的方法可以访问给定的字段。这里的类名必须是当前定义的类,或者是包含该类的外部类。在这种情况下,编译器会生成辅助的getter相setter方法,允许外部类访问该字段。这些类将会是公有的,因为JVM并没有更细粒度的访问控制系统,并且它们的名称也会随着JVM实现不同而不同。
Bean属性 |
正如你在前面所看到的,Scala对于你定义的字段提供了getter和setter方法。不过,这些方法的名称并不是Java工具所预期的。JavaBeans规范
www.oracle.com/technetwork/java/javase/tech/index-jsp-138795.html
把Java属性定义为一对getFoo/setFoo方法或者对于只读属性而言单个getFoo方法。许多Java工具都依赖这样的命名习惯。当你将Scala字段标注为@BeanProperty时,这样的方法会自动生成。例如:
import scala.reflect.BeanProperty
class Person {
@BeanProperty var name: String=_
}
将会生成四个方法:
■ name:String
■ name_=(newValue: Strmg):Unit
■ getName():String
■ setName(newValue: String): Unit
下表显示了在各种情况下哪些方法会被生成:
如果你以主构造器参数的方式定义了某字段,并且你需要JavaBeans版的getter和setter方法,像如下这样给构造器参数加上注解即可:
class Person (@BeanProperty var name: String)
辅助构造器 |
和Java或C++一样,Scala可以有任意多的构造器。不过Scala类有一个构造器比其他所有构造器都更为重要,它就是主构造器(primary constructor)。除了主构造器之外,类还可以有任意多的辅助构造器( auxiliary constructor)我们将首先讨论辅助构造器,这是因为它们更容易理解。它们同Java或C++的构造器十分相似,只有两处不同。
■ 辅助构造器的名称为this。在Java或C++中,构造器的名称和类名相同,当你修改类名时就不那么方便了
■ 每一个辅助构造器都必须以一个对先前已定义的其他辅助构造器或主构造器的调用开始
这里有一个带有两个辅助构造器的类。
class Person {
private var name=""
private var age=0
def this(name: String){ //一个辅助构造器
this() // 调用主构造器
this.name=name
}
def this (name: String,age: Int) { // 另一个辅助构造器
this (name) //调用前一个辅助构造器
this.age=age
}
}
和Java、C++一一样,类如果没有显式定义主构造器则自动拥有一个无参的主构造器即可。你可以以三种方式构建对象:
val p1 = new Person //主构造器
val p2 = new Person("Fred")//第一个辅助构造器
val p3 = new Person ("Fred",42) //第二个辅助构造器
主构造器 |
主构造器的参数直接放置在类名之后
在Scala中,每个类都有主构造器。主构造器并不以this方法定义,而是与类定义交织在一起
class Person ( val name:String, val aqe:Int) {
// (…)中的内容就是主构造器的参数
}
主构造器的参数被编译成字段,其值被初始化成构造时传入的参数。在本例中name和age成为Person类的字段。如new Person("Fred",42)这样的构造器调用将设置name和age字段。我们只用半行Scala就完成了七行Java代码的工作:
public class Person{ //这是Java
private String name;
private int age;
public Person(String name,int age) {
this.name=name
this.age=age
}
public String name() {return this.name;}
public int age() {raturn this.age;}
}
主构造器会执行类定义中的所有语句。例如在以下类中:
class Person (val name: String, val age: Int) {
println ("Just constructed anther person")
def description=name+"is"+age+"years old"
}
println语句是主构造器的一部分。每当有对象被构造出来时,上述代码就会被执行。当你需要在构造过程当中配置某个字段时这个特性特别有用。例如:
class MyProg {
private val props=new Properties
props.load ( new FileReader ( "myprog.properties" ) ) // 上述语句是主构造器的一部分
}
类名之后没有参数
如果类名之后没有参数,则该类具备一个无参主构造器。这样一个构造器仅仅是简单地执行类体中的所有语句而已。你通常可以通过在主构造器中使用默认参数来避免过多地使用辅助构造器。例如:
class Person (val name:String="",val age: Int =0 )
主构造器参数
主构造器的参数可以采用下表中列出的任意形态
例如:
class Person (val name : String, privite var age: Int)
这段代码将声明并初始化如下字段:
val name: String
private var age: Int
构造参数也可以是普通的方法参数,不带val或var,这样的参数如何处理取决于它们在类中如何被使用。如果不带val或var的参数至少被一个方法所使用,它将被升格为字段。例如:
class Person(name: String, age: Int) {
def description=name+"is"+age+"years old"
}
上述代码声明并初始化了不可变字段name和age,而这两个字段都是对象私有的。类似这样的字段等同于private[this] val字段的效果。否则,该参数将不被保存为字段。它仅仅是一个可以被主构造器中的代码访问的普通参数。严格地说,这是一个具体实现相关的优化。
主构造器参数生成字段
下表总结了不同类型的主构造器参数对应会生成的字段和方法:
如果主构造器的表示法让你困惑,你不需要使用它。你只要按照常规的做法提供一个或多个辅助构造器即可,不过要记得调用this(),如果你不和其他辅助构造器串接的话。话虽如此,许多程序员还是喜欢主构造器这种精简的写法。Martin Odersky建议这样来看待主构造器:在Scala中,类也接受参数,就像方法一样。当你把主构造器的参数看做是类参数时,不带val或var的参数就变得易于理解了,这样的参数的作用域涵盖了整个类。因此,你可以在方法中使用它们。而一旦你这样做了,编译器就自动帮你将它保存为字段。
类定义与主构造器
Scala设计者们认为每敲一个键都是珍贵的,因此他们让你可以把类定义和主构造器结合在一起。当你阅读一个Scala类时,你需要将它们分开。举例来说,当你看到如下代码时:
class Person (val name: String) {
var age=0
def description=name+"is"+age+"years old"
}
把它拆开成一个类定义:
class Person (val name: String) {
var age = 0
def description = name+"is"+age+"years old"
}
和一个构造器定义:
class Person(val name: String) {
var age = 0
daf description= nama+"is"+age+"years old"
}
如果想让主构造器变成私有的,可以像这样放置private关键字:
class Person private ( val id: Int ) { … }
这样一来类用户就必须通过辅助构造器来构造Person对象了
嵌套类 |
Scala内嵌类
在Scala中,你几乎可以在任何语法结构中内嵌任何语法结构。你可以在函数中定义函数,在类中定义类。以下代码是在类中定义类的一个示例:
import scala.collection.mutable.ArrayBuffer
class Network {
class Member(val name: String) {
val contacts = new ArrayBuffer[Member]
}
private val members=new ArrayBuffer[Member]
def join(name: String) ={
val m=new Member(name)
members+=m
m
}
}
在Scala中,每个实例都有它自己的Member类,就和它们有自己的members字段一样,考虑有如下两个网络:
val chatter = new Network
val myFace = new Network
也就是说,chatter.Member和myFace.Member是不同的两个类。这和Java不同,在Java中内部类从属于外部类。Scala采用的方式更符合常规,举例来说,要构建一个新的内部对象,你只需要简单的new这个类名:new chatter.Member。而在Java中,你需要使用一个特殊语法:chatter.new Member()。拿我们的网络示例来讲,你可以在各自的网络中添加成员,但不能跨网添加成员:
val fred = chatter.join("Fred")
val wilma=chatter.join ("Wilma")
fred.contacts+=wilma //OK
val barney=myFace.join ("Barney") // 类型为myFace .Member
fred.contacts+=barney // 不可以这样做,不能将一个myFace.Member添加到chatter.Member元素缓冲当中
Scala内嵌类访问
对于社交网络而言,这样的行为是讲得通的。如果你不希望是这个效果,有两种解决方式。首先,你可以将Member类移到别处,一个不错的位置是Network的伴生对象。
object Network {
class Member (val name: String) {
val contacts=new ArrayBuffer[Member]
}
}
class Network{
private val members = new ArrayBuffer[Network.Member]
}
或者,你也可以使用类型投影Network#Member,其含义是"任何Network的Member"。例如:
class Network {
class Member (val name: String) {
val contacts = new ArrayBuffer[Network#Member]
}
}
如果你只想在某些地方,而不是所有地方,利用这个细粒度的"每个对象有自己的内部类"的特性,则可以考虑使用类型投影。
内嵌类访问外部类
在内嵌类中,你可以通过外部类.this的方式来访问外部类的this引用,就像Java那样。如果你觉得需要,也可以用如下语法建立一个指向该引用的别名:☆☆
class Network(val name: String){ outer=>
class Member (val name: String) {
def dascription=name+"inside"+outer.name
}
}
class Network { outer=>语法使得outer变量指向Network.this。对这个变量,你可以用任何合法的名称。self这个名称很常见,但用在嵌套类中可能会引发歧义。这样的语法和"自身类型"语法相关,将会后面内容继续介绍