sacla 类
如果你是学java或者是c++的话,你不会觉得有难度了,并且你会享受Scala
更加精简的表示法带来的便利。
简单类和无参方法
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("value:" + myCounter.current)
调用无参方法(比如current)时,你可以加括号,也可以不加。
myCounter.increment() // ok
myCounter.increment // 同样可以
你可以通过不带()
的方式来强制这种风格。
class Counter{
.....
def current = value // 定义不带()
}
带getter和setter属性
编写Java类时,我们并不喜欢共有字段:
/**
* 这是java类
*/
public class Person{
public int age; // java不鼓励这么做
}
使用共有字段的花,任何人都可以修改age的值,无法限制age的值是否合理,这就是为什么我们更倾向于永getter和setter方法:
public class Person{
public int age; // java不鼓励这么做
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
像这么一对getter/setter
通常被称为属性(property),我们会说Person有一个age属性。
这到底有哪里好的呢。打个比方,比如说Person的age属性,最少不得小于18岁。
修改上面的setAge(int age)
方法。
public void setAge(int age) {
// 不能小于18岁
if (age >= 18)
this.age = age;
}
之所以getter和setter方法比共有字段好,是因为可以从简单的get/set机制出发,并在需要的时候最出改进。
仅仅因为getter和setter方法比共有字段更好并不意味着它们总是好的。通常,如果每个客户端都可以对一个对象的状态数据进行获取和设置,这明显是很糟糕的。在本节中,我会向你们展示如何用Scala实现属性。但要靠你自己决定可以取值和改值的字段是否是合理的设计。
Scala对每个字段都提供了getter和setter方法,我们定义一个共有的字段。
class Person{
var age = 0
}
Scala生成面向JVM的类,其中有一个私有的age字段以及相应的getter和setter方法。这两个方法是公有的,因为我们没有将age声明为private。(对私有字段而言,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 org.dreams.scala.Person {
private int age;
public int age();
public void age_$eq(int);
public org.dreams.scala.Person();
}
正如你看到的那样,编译器创建了age和age_$eq方法。(=号被翻译成了$eq,是因为JVM不允许在方法名种出现=。)
在Scala中,getter和setter方法并非命名为getXxx和setXxx,不过他们的用意是相同的。
在任何时候你都可以自己重新定义getter和setter方法,例如:
class Person {
private var privateage = 0
def age = privateage
def age_=(newAge: Int) {
if (newAge > privateage) privateage = newAge // 不能变年轻
}
}
你的类的使用者仍然可以访问fred.age
,但现在Fred
不能变年轻了。
val p = new Person();
p.age = 21
p.age = 18
println(p.age) // 输出 30
颇具有影响的
Eiffel
语言的发明者Bertand Meyer
提出统一访问原则,内容如下 “某个模块提供的所有服务都应该能通过统一的表示法访问到”,至于他们是通过存储还是通过计算来实现的,从访问方式上应无从获知。在Scala中,fred.age的调用者并不知道age是通过字段还是通过方法来实现的。(当然了,在JVM中,该服务总是通过方法来实现的,要么是编译器合成,要么由程序员提供)
提示:Scala对每个字段生成getter和setter方法听上去有些恐怖。不过你可以控制这个过程。
- 如果字段是私有的,则getter和setter方法也是私有的。
- 如果字段是val,则只有getter方法被生产。
- 如果你不需要任何getter或setter,你可以将字段声明为
private[this]
只带getter的属性
有时候你需要一个只读属性,只有getter但没有setter。如果属性的值在对象构建完成后就不再改变,则可以使用val字段:
class Message{
val timeStamp = new java.util.Date
}
Scala会生成一个私有的final字段和一个getter方法,但没有setter。
不过,有时你需要这样一个属性,客户端不能随意改值,但它可以通过某种其他的方式被改变。
上面Counter类就是个很好的例子。从概念上讲,counter有一个current属性,当increment方法被调用时更新,但并没有对应的setter。
你不能通过val来实现这样一个属性---- val永不改变。你需要提供一个私有字段和一个属性的getter方法,像这样:
class Counter{
private var value = 0
def increment(){value+=1}
def current = value // 声明中没有()
}
注意,在getter方法的定义中并没有()。因此,你必须以不带圆括号的方式来调用:
val n = myCounter.current // myCounter.current()
那这样的调用方式就是语法错误了
总结一下,在实现属性时你有如下四个选择:
1、var foo:Scala自动合成一个getter和一个setter。
2、val foo:Scala自动合成一个getter。
3、由你来定义foo和foo_=方法。
4、由你来定义foo方法。
对象私有字段
在Scala中(Java和C++也一样),方法可以访问该类的所有对象的私有字段。例如:
class Counter{
private var value = 0
def increment(){value+=1}
def current = value // 声明中没有()
def isLess(other:Counter) = value < other.value
// 可以访问另一个对象的私有字段
}
之所以访问other.value是合法的,是因为other也同样是Counter对象。
Scala允许我们定义更加严格的访问限制,通过private[this]这个修饰来实现。
private[this] var value=0 // 类是某个对象.value这样的访问将不被允许
这样一来,Counter类的方法智能访问到当前对象爱那个的value字段,而不能访问同样是Counter类型
的其他对象的该字段。这样的访问有时被称为对象私有的,这在某些OO语言,比如SmallTalk中十分常见。
对于类私有的字段,Scala生成私有的getter和setter方法。但对于对象私有的字段,Scala根本不会生成getter和setter方法。
说明:Scala允许你将访问权赋予指定的类。private[
类名
]修饰符可以定义仅有指定类的方法可以访问指定的字段。这里的类名
必须是当前定义的类,或者是包含该类的外部类。
在这种情况下,编译器会生成铺助的getter和setter方法,允许外部类访问该字段。这些类将会是共有的,因为JVM并没有更细粒度的访问控制系统,并它们的名称也会随着JVM实现不同而不同。
Bean属性
正如你在前面看到的,Scala对于你定义的字段提供了getter和setter方法,不过,这些方法的名称并不是Java工具所预期的。JavaBean规范把Java属性定义为一对getFoo/setFoo方法(或者对于只读属性生成单个getFoo方法)。许多Java工具都依赖这样的命名习惯。
当你将Scala字段标注为@BeanProperty
时,这样的方法会自动生成。例如:
class Person{
@BeanProperty var name:String = _
}
将会生成四个方法:
1.name:String
2.name_(newValue:String):Unit
3.getName():String
4.setName(newValue:String):Unit
说明:如果你以主构造器参数的方式定义了某字段,并且你需要JavaBeans版的getter和setter方法,像如下这样给构造器参数加上注解即可:
Scala字段 | 生成的方法 | 何时使用 |
---|---|---|
val/var name | 公有的name name_(仅限于var) |
实现一个可以被公开访问 并且背后是以字段形式保存的属性 |
private val/var name | 私有的name name_=(仅限于var) |
用于将访问限制在本类的方法,就和Java一样。 尽量使用private---除非你真的需要一个公有的属性 |
private[this] val/var name | 无 | 用于将字段访问限制在 同一个对象上调用的方法。并不经常用到 |
private[类名] val/var name | 依赖于具体实现 | 将访问权赋予外部类。并不经常用到 |
主构造器
在Scala中,每个类都有主构造器。主构造器并不以this
方法定义,而是与类定义交织在一起。
1、主构造器的参数直接放置在类名之后。
class Person(val name: String, val age: Int) {
// (....) 中的内容就是主构造器的参数
}
主构造器的参数被编译成字段,其值被初始化成构造时传入的参数。在本例中,name和age成为Person类的字段。如new Person("Fred",42)
这样的构造器调用将设置name和age字段。
我们只用半行Scala就完成了七行Java代码的工作:
public class Person { // 这是java
private String name;
private int age; // java不鼓励这么做
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name(){return name;}
public int age(){return age;}
}
2、主构造器会执行类定义中的所有语句。例如在以下的类中:
class Person(val name: String, val age: Int) {
println("Just constructed another 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, private 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字段的效果。否则,该参数将不被保存为字段,它仅仅是一个可以被主构造器中的代码访问的普通参数。(严格地说,这是一个具体实现相关的优化。)
在Scala中,类也接收参数,像方法一样。
说明:如果想让主构造器变成私有的,可以像这样前面放置private关键字:
class Person private(val id:Int) { .... }
这样一来类用户就必须通过辅助构造器来构造Person对象了。
嵌套类
在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
}
}
考虑有如下两个网络
val chatter = new Network
val myFace = new NetWork
在Scala中,每个实例都有它自己的Member类,就和它们有自己的members字段一样。也就是说,chatter.Member
和MyFace.Member
是不同的两个类。
说明:这和java不同,在Java中内部类从属外部类。
Scala采用的方式更符合常规。举例来说,要构建一个新的内部对象,你只需要简单的new这个类名:new chatter.Member。而在Java中,你需要使用一个特殊的语法:chatter.new Member()。
拿我们的网络示例来讲,你可以在各自的网络中添加成员,但不能跨网添加成员。
var fred = chatter.join("Fred")
var wilma = chatter.join("Wilma")
fred.contacts += wilma //ok
val barney = myFace.join("Barney") // 类型为myFace.Member
fred.contacts += barney
// 不可以这样做 ---- 不能将一个myFace.Member添加到chatter.Member元素缓冲当中。
对于社交网络而言,这样的行为是讲的通的。如果你不希望是这个效果,有两种解决方式。
首先,你可以将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 description = name + " inside " + outer.name
}
}
Class Network{ outer => 语法使得outer变量指向Network.this
。对这个变量,你可以用任何合法的名称。self这个名称很常见。但用在嵌套类中可能会引发歧义。
这样的语法和“自身类型”语法相关。